POTION_DETAILS = new HashSet<>();
-
- /**
- * Resets all item names, custom item names included.
- * */
- public static void resetPotionNames() {
- POTION_DETAILS.clear();
- }
-
- /**
- * Loads item names from configurable files and requests any custom item names programmatically from plugins.
- * */
- public static void loadPotionNames() {
- resetPotionNames();
- InputStream enchantmentsCSV = CivModCorePlugin.class.getResourceAsStream("/potions.csv");
- if (enchantmentsCSV != null) {
- try {
- BufferedReader reader = new BufferedReader(new InputStreamReader(enchantmentsCSV));
- String line = reader.readLine();
- while (line != null) {
- String[] values = line.split(",");
- // If there's not at least three values (slug, abbreviation, display name) then skip
- if (values.length != 3) {
- LOGGER.warn("[Config] This potion row is corrupted: " + line);
- // Go to the next line
- line = reader.readLine();
- continue;
- }
- // If the potion type cannot be found by the given slug, then skip
- PotionType type;
- try {
- type = PotionType.valueOf(values[0]);
- }
- catch (Exception ignored) {
- LOGGER.warn("[Config] Could not find a potion type on this line: " + line);
- // Go to the next line
- line = reader.readLine();
- continue;
- }
- // If the line specifies an effect type, then try to find it or skip if not found
- PotionEffectType effectType = null;
- if (!Strings.isNullOrEmpty(values[1])) {
- effectType = PotionEffectType.getByName(values[1]);
- if (effectType == null) {
- LOGGER.warn("[Config] Could not find potion effect type type on this line: " + line);
- // Go to the next line
- line = reader.readLine();
- continue;
- }
- }
- // Get the potion's name
- String name = values[2];
- if (Strings.isNullOrEmpty(name)) {
- LOGGER.warn("[Config] Could not find potion name on this line: " + line);
- // Go to the next line
- line = reader.readLine();
- continue;
- }
- // Put the enchantment and name into the system
- POTION_DETAILS.add(new SearchResult(type, effectType, name));
- line = reader.readLine();
- }
- reader.close();
- }
- catch (IOException error) {
- LOGGER.warn("[Config] Could not load potions from potions.csv");
- error.printStackTrace();
- }
- }
- else {
- LOGGER.warn("[Config] Could not load potions from potions.csv as the file does not exist.");
- }
- }
-
- /**
- * Attempts to match a potion type with a set of details.
- *
- * @param type The potion type to search with.
- * @return The potion details, or null.
- */
- public static SearchResult findByType(PotionType type) {
- if (type == null) {
- return null;
- }
- for (SearchResult details : POTION_DETAILS) {
- if (details.type == type) {
- return details;
- }
- }
- return null;
- }
-
- /**
- * Attempts to match a potion effect type with a set of details. This is a less desirable search as some potion
- * do not have an effect. If you're checking with a static value, best to take a look at {@link PotionType}.
- *
- * @param type The potion effect type to search with.
- * @return The potion details, or null.
- */
- public static SearchResult findByEffect(PotionEffectType type) {
- if (type == null) {
- return null;
- }
- for (SearchResult details : POTION_DETAILS) {
- if (details.effectType == type) {
- return details;
- }
- }
- return null;
- }
-
- /**
- * Attempts to match a potion name with a set of details.
- *
- * @param name The potion name to search with.
- * @return The potion details, or null.
- */
- public static SearchResult findByName(String name) {
- if (Strings.isNullOrEmpty(name)) {
- return null;
- }
- for (SearchResult details : POTION_DETAILS) {
- if (TextUtil.stringEqualsIgnoreCase(details.name, name)) {
- return details;
- }
- }
- return null;
- }
-
- /**
- * This class represents a data set for a particular enchantment.
- */
- public static final class SearchResult {
-
- private final PotionType type;
-
- private final PotionEffectType effectType;
-
- private final String name;
-
- private SearchResult(PotionType type, PotionEffectType effectType, String name) {
- this.type = type;
- this.effectType = effectType;
- this.name = name;
- }
-
- /**
- * @return Returns the potion type.
- */
- public PotionType getPotionType() {
- return this.type;
- }
-
- /**
- * @return Returns the potion's effect type.
- */
- public PotionEffectType getEffectType() {
- return this.effectType;
- }
-
- /**
- * @return Returns the potion's name.
- */
- public String getName() {
- return this.name;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(this.type, this.effectType, this.name);
- }
-
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/RecipeAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/api/RecipeAPI.java
deleted file mode 100644
index 2ef837a7..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/RecipeAPI.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package vg.civcraft.mc.civmodcore.api;
-
-import java.util.Iterator;
-import org.bukkit.Bukkit;
-import org.bukkit.Keyed;
-import org.bukkit.inventory.Recipe;
-import vg.civcraft.mc.civmodcore.inventory.RecipeManager;
-import vg.civcraft.mc.civmodcore.util.NullCoalescing;
-
-/**
- * Class of utility functions for Recipes.
- *
- * @deprecated Use {@link RecipeManager} instead.
- */
-@Deprecated
-public final class RecipeAPI {
-
- /**
- * Determines whether a recipe matches another recipe.
- *
- * Note: This matcher pretty much assumes that all recipes are keyed. If other kinds of recipes come up that
- * aren't keyed, then support here can be added. But until then? /shrug
- *
- * @param base The base recipe to base the matching upon.
- * @param other The other recipe, the unknown.
- * @return Returns true if the other recipe matches the base.
- *
- * @deprecated Use {@link RecipeManager#matchRecipe(Recipe, Recipe)} instead.
- */
- @Deprecated
- public static boolean matchRecipe(Recipe base, Recipe other) {
- if (base == null) {
- return false;
- }
- if (base instanceof Keyed && other instanceof Keyed) {
- return NullCoalescing.equalsNotNull(
- ((Keyed) base).getKey(),
- ((Keyed) other).getKey());
- }
- return false;
- }
-
- /**
- * Registers a recipe to the Bukkit server.
- *
- * This is to prevent thrown exceptions for re-registered recipes. Since registrations often happen within a
- * plugin's onEnable() method, an exception here will actually disable the plugin entirely, which is a bit
- * disproportionate. You should check the returned boolean to see whether the registration was successful and
- * handle that accordingly.
- *
- * @param recipe The recipe to register.
- * @return Returns true if the recipe was registered.
- *
- * @deprecated Use {@link RecipeManager#registerRecipe(Recipe)} instead.
- */
- @Deprecated
- public static boolean registerRecipe(Recipe recipe) {
- if (recipe == null) {
- return false;
- }
- try {
- return Bukkit.getServer().addRecipe(recipe);
- }
- catch (Exception exception) {
- return false;
- }
- }
-
- /**
- * Removes a recipe from Bukkit's registered recipes list.
- *
- * @param recipe The shaped recipe to deregister.
- * @return Returns true if the recipe was de-registered, or wasn't ever registered.
- *
- * @deprecated Use {@link RecipeManager#registerRecipe(Recipe)} instead.
- */
- @Deprecated
- public static boolean removeRecipe(Recipe recipe) {
- if (recipe == null) {
- return false;
- }
- Iterator iterator = Bukkit.getServer().recipeIterator();
- while (iterator.hasNext()) {
- if (!matchRecipe(recipe, iterator.next())) {
- continue;
- }
- iterator.remove();
- return true;
- }
- return true;
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/SpawnEggAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/api/SpawnEggAPI.java
deleted file mode 100644
index d4a5f88d..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/SpawnEggAPI.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package vg.civcraft.mc.civmodcore.api;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableBiMap;
-import javax.annotation.Nullable;
-import org.bukkit.Material;
-import org.bukkit.entity.EntityType;
-import vg.civcraft.mc.civmodcore.inventory.items.SpawnEggUtils;
-
-/**
- * Class of static APIs for Spawn Eggs.
- *
- * @deprecated Use {@link SpawnEggUtils} instead.
- */
-@Deprecated
-public final class SpawnEggAPI {
-
- private SpawnEggAPI() {}
-
- private static final BiMap spawnEggs = ImmutableBiMap.builder().
- put(Material.BAT_SPAWN_EGG, EntityType.BAT).
- put(Material.BEE_SPAWN_EGG, EntityType.BEE).
- put(Material.BLAZE_SPAWN_EGG, EntityType.BLAZE).
- put(Material.CAT_SPAWN_EGG, EntityType.CAT).
- put(Material.CAVE_SPIDER_SPAWN_EGG, EntityType.CAVE_SPIDER).
- put(Material.CHICKEN_SPAWN_EGG, EntityType.CHICKEN).
- put(Material.COD_SPAWN_EGG, EntityType.COD).
- put(Material.COW_SPAWN_EGG, EntityType.COW).
- put(Material.CREEPER_SPAWN_EGG, EntityType.CREEPER).
- put(Material.DOLPHIN_SPAWN_EGG, EntityType.DOLPHIN).
- put(Material.DONKEY_SPAWN_EGG, EntityType.DONKEY).
- put(Material.DROWNED_SPAWN_EGG, EntityType.DROWNED).
- put(Material.ELDER_GUARDIAN_SPAWN_EGG, EntityType.ELDER_GUARDIAN).
- put(Material.ENDERMAN_SPAWN_EGG, EntityType.ENDERMAN).
- put(Material.ENDERMITE_SPAWN_EGG, EntityType.ENDERMITE).
- put(Material.EVOKER_SPAWN_EGG, EntityType.EVOKER).
- put(Material.FOX_SPAWN_EGG, EntityType.FOX).
- put(Material.GHAST_SPAWN_EGG, EntityType.GHAST).
- put(Material.GUARDIAN_SPAWN_EGG, EntityType.GUARDIAN).
- put(Material.HOGLIN_SPAWN_EGG, EntityType.HOGLIN).
- put(Material.HORSE_SPAWN_EGG, EntityType.HORSE).
- put(Material.HUSK_SPAWN_EGG, EntityType.HUSK).
- put(Material.LLAMA_SPAWN_EGG, EntityType.LLAMA).
- put(Material.MAGMA_CUBE_SPAWN_EGG, EntityType.MAGMA_CUBE).
- put(Material.MOOSHROOM_SPAWN_EGG, EntityType.MUSHROOM_COW).
- put(Material.MULE_SPAWN_EGG, EntityType.MULE).
- put(Material.OCELOT_SPAWN_EGG, EntityType.OCELOT).
- put(Material.PANDA_SPAWN_EGG, EntityType.PANDA).
- put(Material.PARROT_SPAWN_EGG, EntityType.PARROT).
- put(Material.PHANTOM_SPAWN_EGG, EntityType.PHANTOM).
- put(Material.PIG_SPAWN_EGG, EntityType.PIG).
- put(Material.PIGLIN_SPAWN_EGG, EntityType.PIGLIN).
- put(Material.PILLAGER_SPAWN_EGG, EntityType.PILLAGER).
- put(Material.POLAR_BEAR_SPAWN_EGG, EntityType.POLAR_BEAR).
- put(Material.PUFFERFISH_SPAWN_EGG, EntityType.PUFFERFISH).
- put(Material.RABBIT_SPAWN_EGG, EntityType.RABBIT).
- put(Material.RAVAGER_SPAWN_EGG, EntityType.RAVAGER).
- put(Material.SALMON_SPAWN_EGG, EntityType.SALMON).
- put(Material.SHEEP_SPAWN_EGG, EntityType.SHEEP).
- put(Material.SHULKER_SPAWN_EGG, EntityType.SHULKER).
- put(Material.SILVERFISH_SPAWN_EGG, EntityType.SILVERFISH).
- put(Material.SKELETON_HORSE_SPAWN_EGG, EntityType.SKELETON_HORSE).
- put(Material.SKELETON_SPAWN_EGG, EntityType.SKELETON).
- put(Material.SLIME_SPAWN_EGG, EntityType.SLIME).
- put(Material.SPIDER_SPAWN_EGG, EntityType.SPIDER).
- put(Material.SQUID_SPAWN_EGG, EntityType.SQUID).
- put(Material.STRAY_SPAWN_EGG, EntityType.STRAY).
- put(Material.STRIDER_SPAWN_EGG, EntityType.STRIDER).
- put(Material.TRADER_LLAMA_SPAWN_EGG, EntityType.TRADER_LLAMA).
- put(Material.TROPICAL_FISH_SPAWN_EGG, EntityType.TROPICAL_FISH).
- put(Material.TURTLE_SPAWN_EGG, EntityType.TURTLE).
- put(Material.VEX_SPAWN_EGG, EntityType.VEX).
- put(Material.VILLAGER_SPAWN_EGG, EntityType.VILLAGER).
- put(Material.VINDICATOR_SPAWN_EGG, EntityType.VINDICATOR).
- put(Material.WANDERING_TRADER_SPAWN_EGG, EntityType.WANDERING_TRADER).
- put(Material.WITCH_SPAWN_EGG, EntityType.WITCH).
- put(Material.WITHER_SKELETON_SPAWN_EGG, EntityType.WITHER_SKELETON).
- put(Material.WOLF_SPAWN_EGG, EntityType.WOLF).
- put(Material.ZOGLIN_SPAWN_EGG, EntityType.ZOGLIN).
- put(Material.ZOMBIE_HORSE_SPAWN_EGG, EntityType.ZOMBIE_HORSE).
- put(Material.ZOMBIE_SPAWN_EGG, EntityType.ZOMBIE).
- put(Material.ZOMBIE_VILLAGER_SPAWN_EGG, EntityType.ZOMBIE_VILLAGER).
- put(Material.ZOMBIFIED_PIGLIN_SPAWN_EGG, EntityType.ZOMBIFIED_PIGLIN).
- build();
-
- /**
- * Tests if a material is that of a spawn egg.
- *
- * @param material The material to test.
- * @return Returns true if the material is that of a spawn egg.
- *
- * @deprecated Use {@link SpawnEggUtils#isSpawnEgg(Material)} instead.
- */
- @Deprecated
- public static boolean isSpawnEgg(Material material) {
- if (material == null) {
- return false;
- }
- return spawnEggs.containsKey(material);
- }
-
- /**
- * Gets the spawned entity type for a spawn egg.
- *
- * @param material The material, must be a spawn egg otherwise it's a guaranteed null.
- * @return Returns the entity type that will be spawned from the spawn egg, or null.
- *
- * @deprecated Use {@link SpawnEggUtils#getEntityType(Material)} instead.
- */
- @Deprecated
- @Nullable
- public static EntityType getEntityType(Material material) {
- if (material == null) {
- return null;
- }
- return spawnEggs.get(material);
- }
-
- /**
- * Gets the spawn egg material from an entity type.
- *
- * @param entityType The type of entity to match to the spawn egg.
- * @return Returns a spawn egg material, or null.
- *
- * @deprecated Use {@link SpawnEggUtils#getSpawnEgg(EntityType)} instead.
- */
- @Deprecated
- @Nullable
- public static Material getSpawnEgg(EntityType entityType) {
- if (entityType == null) {
- return null;
- }
- return spawnEggs.inverse().get(entityType);
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/ToolAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/api/ToolAPI.java
deleted file mode 100644
index 077e4a75..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/ToolAPI.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package vg.civcraft.mc.civmodcore.api;
-
-import org.bukkit.Material;
-
-@Deprecated(forRemoval = true)
-public final class ToolAPI {
-
- /**
- * @deprecated Use {@link MaterialAPI#isSword(Material)} instead.
- */
- @Deprecated
- public static boolean isSword(Material material) {
- return MaterialAPI.isSword(material);
- }
-
- /**
- * @deprecated Use {@link MaterialAPI#isShovel(Material)} instead.
- */
- @Deprecated
- public static boolean isShovel(Material material) {
- return MaterialAPI.isShovel(material);
- }
-
- /**
- * @deprecated Use {@link MaterialAPI#isPickaxe(Material)} instead.
- */
- @Deprecated
- public static boolean isPickaxe(Material material) {
- return MaterialAPI.isPickaxe(material);
- }
-
- /**
- * @deprecated Use {@link MaterialAPI#isAxe(Material)} instead.
- */
- @Deprecated
- public static boolean isAxe(Material material) {
- return MaterialAPI.isAxe(material);
- }
-
- /**
- * @deprecated Use {@link MaterialAPI#isHoe(Material)} instead.
- */
- @Deprecated
- public static boolean isHoe(Material material) {
- return MaterialAPI.isHoe(material);
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/TreeTypeAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/api/TreeTypeAPI.java
deleted file mode 100644
index 4a5cde30..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/TreeTypeAPI.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package vg.civcraft.mc.civmodcore.api;
-
-import org.bukkit.Material;
-import org.bukkit.TreeType;
-import vg.civcraft.mc.civmodcore.inventory.items.TreeTypeUtils;
-
-/**
- * @deprecated Use {@link TreeTypeUtils} instead.
- */
-@Deprecated
-public final class TreeTypeAPI {
-
- /**
- * @deprecated Use {@link TreeTypeUtils#getMatchingTreeType(Material)} instead.
- */
- @Deprecated
- public static TreeType getMatchingTreeType(Material material) {
- if (material == null) {
- return null;
- }
- switch (material) {
- case ACACIA_SAPLING:
- case ACACIA_WOOD:
- case ACACIA_LOG:
- case ACACIA_LEAVES:
- case STRIPPED_ACACIA_LOG:
- case STRIPPED_ACACIA_WOOD:
- return TreeType.ACACIA;
- case BIRCH_SAPLING:
- case BIRCH_WOOD:
- case BIRCH_LOG:
- case BIRCH_LEAVES:
- case STRIPPED_BIRCH_LOG:
- case STRIPPED_BIRCH_WOOD:
- return TreeType.BIRCH;
- case OAK_SAPLING:
- case OAK_WOOD:
- case OAK_LOG:
- case OAK_LEAVES:
- case STRIPPED_OAK_LOG:
- case STRIPPED_OAK_WOOD:
- return TreeType.TREE;
- case JUNGLE_SAPLING:
- case JUNGLE_WOOD:
- case JUNGLE_LOG:
- case JUNGLE_LEAVES:
- case STRIPPED_JUNGLE_LOG:
- case STRIPPED_JUNGLE_WOOD:
- return TreeType.JUNGLE;
- case DARK_OAK_SAPLING:
- case DARK_OAK_WOOD:
- case DARK_OAK_LOG:
- case DARK_OAK_LEAVES:
- case STRIPPED_DARK_OAK_LOG:
- case STRIPPED_DARK_OAK_WOOD:
- return TreeType.DARK_OAK;
- case SPRUCE_SAPLING:
- case SPRUCE_WOOD:
- case SPRUCE_LOG:
- case SPRUCE_LEAVES:
- case STRIPPED_SPRUCE_LOG:
- case STRIPPED_SPRUCE_WOOD:
- return TreeType.REDWOOD;
- case CHORUS_FLOWER:
- case CHORUS_PLANT:
- return TreeType.CHORUS_PLANT;
- case RED_MUSHROOM:
- case RED_MUSHROOM_BLOCK:
- return TreeType.RED_MUSHROOM;
- case BROWN_MUSHROOM:
- case BROWN_MUSHROOM_BLOCK:
- return TreeType.BROWN_MUSHROOM;
- case COCOA:
- return TreeType.COCOA_TREE;
- default:
- return null;
- }
- }
-
- /**
- * @deprecated Use {@link TreeTypeUtils#getMatchingSapling(TreeType)} instead.
- */
- @Deprecated
- public static Material getMatchingSapling(TreeType type) {
- if (type == null) {
- return null;
- }
- switch(type) {
- case ACACIA:
- return Material.ACACIA_SAPLING;
- case BIG_TREE:
- case TREE:
- case SWAMP:
- return Material.OAK_SAPLING;
- case BIRCH:
- case TALL_BIRCH:
- return Material.BIRCH_SAPLING;
- case BROWN_MUSHROOM:
- return Material.BROWN_MUSHROOM;
- case CHORUS_PLANT:
- return Material.CHORUS_PLANT;
- case COCOA_TREE:
- return Material.COCOA;
- case DARK_OAK:
- return Material.DARK_OAK_SAPLING;
- case JUNGLE:
- case SMALL_JUNGLE:
- case JUNGLE_BUSH:
- return Material.JUNGLE_SAPLING;
- case MEGA_REDWOOD:
- case REDWOOD:
- case TALL_REDWOOD:
- return Material.SPRUCE_SAPLING;
- case RED_MUSHROOM:
- return Material.RED_MUSHROOM;
- default:
- return null;
- }
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/WorldAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/api/WorldAPI.java
deleted file mode 100644
index 3cac1652..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/WorldAPI.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package vg.civcraft.mc.civmodcore.api;
-
-import java.util.Objects;
-import org.bukkit.Bukkit;
-import org.bukkit.Chunk;
-import org.bukkit.Location;
-import org.bukkit.World;
-import vg.civcraft.mc.civmodcore.world.WorldUtils;
-
-/**
- * Class of utility functions for Worlds.
- *
- * @deprecated Use {@link WorldUtils} instead.
- */
-@Deprecated
-public final class WorldAPI {
-
- /**
- * Determines if a world is currently loaded.
- *
- * @param world World to test.
- * @return Returns true if the world is loaded.
- *
- * @deprecated Use {@link WorldUtils#isWorldLoaded(World)} instead.
- */
- @Deprecated
- public static boolean isWorldLoaded(World world) {
- if (world == null) {
- return false;
- }
- // Same method in Location.isWorldLoaded()
- return Bukkit.getWorld(world.getUID()) != null;
- }
-
- /**
- * Determines if a chunk is loaded in an efficient manner without loading any chunks.
- *
- * @param world The world the target chunk is located within.
- * @param x The (CHUNK) X coordinate.
- * @param z The (CHUNK) Z coordinate.
- * @return Returns true if the chunk is loaded.
- *
- * @deprecated Use {@link WorldUtils#isChunkLoaded(World, int, int)} instead.
- */
- @Deprecated
- public static boolean isChunkLoaded(World world, int x, int z) {
- if (!isWorldLoaded(world)) {
- return false;
- }
- return world.isChunkLoaded(x, z);
- }
-
- /**
- * Retrieves a chunk only if it's loaded.
- *
- * @param world The world the target chunk is located within.
- * @param x The (CHUNK) X coordinate.
- * @param z The (CHUNK) Z coordinate.
- * @return Returns the loaded chunk, or null.
- *
- * @deprecated Use {@link WorldUtils#getLoadedChunk(World, int, int)} instead.
- */
- @Deprecated
- public static Chunk getLoadedChunk(final World world, final int x, final int z) {
- if (!isChunkLoaded(world, x, z)) {
- return null;
- }
- return world.getChunkAt(x, z);
- }
-
- /**
- * Determines if a block is loaded by nature of whether the chunk it's in is loaded.
- *
- * @param location The block location.
- * @return Returns true if the block is laoded.
- *
- * @deprecated Use {@link WorldUtils#isBlockLoaded(Location)} instead.
- */
- @Deprecated
- public static boolean isBlockLoaded(Location location) {
- if (!LocationAPI.isValidLocation(location)) {
- return false;
- }
- World world = Objects.requireNonNull(location.getWorld());
- int chunkX = location.getBlockX() >> 4;
- int chunkZ = location.getBlockZ() >> 4;
- return world.isChunkLoaded(chunkX, chunkZ);
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/api/package-info.java b/src/main/java/vg/civcraft/mc/civmodcore/api/package-info.java
deleted file mode 100644
index 41b27c19..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/api/package-info.java
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * This package was created as a sort of island of tranquility for standardised, standalone functions... however it's
- * gotten a little unwieldy and so the plan is to deprecate and disperse the functionality to more sensible locations.
- *
- * The plan is to remove the "api" package completely.
- */
-package vg.civcraft.mc.civmodcore.api;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chat/ChatUtils.java b/src/main/java/vg/civcraft/mc/civmodcore/chat/ChatUtils.java
index f87f8585..ce349e21 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/chat/ChatUtils.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/chat/ChatUtils.java
@@ -1,19 +1,36 @@
package vg.civcraft.mc.civmodcore.chat;
-import com.google.common.base.Strings;
+import io.papermc.paper.adventure.PaperAdventure;
import java.awt.Color;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.experimental.UtilityClass;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.md_5.bungee.api.ChatColor;
-import net.md_5.bungee.api.chat.BaseComponent;
-import net.md_5.bungee.api.chat.TextComponent;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.bukkit.craftbukkit.v1_17_R1.util.CraftChatMessage;
+import org.jetbrains.annotations.Contract;
+@UtilityClass
public final class ChatUtils {
/**
* This is necessary as {@link ChatColor#values()} has all colours and all formats.
+ *
+ * @deprecated Use {@link NamedTextColor} instead.
*/
- @SuppressWarnings("deprecation")
+ @Deprecated
public static final List COLOURS = List.of(
ChatColor.BLACK,
ChatColor.DARK_BLUE,
@@ -38,7 +55,11 @@ public final class ChatUtils {
* @param g The green value.
* @param b The blue value.
* @return Returns a valid Bungee ChatColor.
+ *
+ * @deprecated Use {@link net.kyori.adventure.text.format.TextColor#color(int, int, int)} instead.
*/
+ @Nonnull
+ @Deprecated
public static ChatColor fromRGB(final byte r, final byte g, final byte b) {
return ChatColor.of(new Color(r, g, b));
}
@@ -49,7 +70,9 @@ public static ChatColor fromRGB(final byte r, final byte g, final byte b) {
* @param colour The given RGB colour.
* @return Returns the closest Minecraft match, or null.
*/
- public static ChatColor collapseColour(final ChatColor colour) {
+ @Contract("!null -> !null")
+ @Nullable
+ public static ChatColor collapseColour(@Nullable final ChatColor colour) {
if (colour == null) {
return null;
}
@@ -74,23 +97,47 @@ public static ChatColor collapseColour(final ChatColor colour) {
// Color parsing
// -------------------------------------------- //
- public static String parseColor(String string) {
+ /**
+ * @deprecated Please use MiniMessage instead.
+ * Read More.
+ */
+ @Nonnull
+ @Deprecated
+ public static String parseColor(@Nonnull String string) {
string = parseColorAmp(string);
string = parseColorAcc(string);
string = parseColorTags(string);
return string;
}
- public static String parseColorAmp(String string) {
+ /**
+ * @deprecated Please use MiniMessage instead.
+ * Read More.
+ */
+ @Nonnull
+ @Deprecated
+ public static String parseColorAmp(@Nonnull String string) {
string = string.replace("&&", "&");
return ChatColor.translateAlternateColorCodes('&', string);
}
- public static String parseColorAcc(String string) {
+ /**
+ * @deprecated Please use MiniMessage instead.
+ * Read More.
+ */
+ @Nonnull
+ @Deprecated
+ public static String parseColorAcc(@Nonnull String string) {
return ChatColor.translateAlternateColorCodes('`', string);
}
- public static String parseColorTags(String string) {
+ /**
+ * @deprecated Please use MiniMessage instead.
+ * Read More.
+ */
+ @Nonnull
+ @Deprecated
+ public static String parseColorTags(@Nonnull String string) {
return string
.replace("", ChatColor.BLACK.toString())
.replace("", ChatColor.DARK_BLUE.toString())
@@ -151,73 +198,185 @@ public static String parseColorTags(String string) {
*
* @param component The component to test if null or empty.
* @return Returns true if the component is null or has no visible content.
+ *
+ * @deprecated Has been deprecated due to Paper's move to Kyori's Adventure.
*/
- public static boolean isNullOrEmpty(final BaseComponent component) {
- if (component == null) {
+ public static boolean isNullOrEmpty(@Nullable final Component component) {
+ if (component == null || component == Component.empty()) {
return true;
}
- final TextComponent text = fromLegacyText(component.toPlainText());
- return Strings.isNullOrEmpty(text.toPlainText());
+ return StringUtils.isBlank(PlainTextComponentSerializer.plainText().serialize(component));
}
/**
- * Converts a string containing Minecraft's legacy colour codes into a text component.
+ * Determines whether a given base component is null or empty.
*
- * Note: This does not work on Civ's colour code equivalents, make sure to parse those before using this.
+ * This is determined by converting the component into plain text, so a non-null component filled with
+ * nothing but colour codes and hover text will likely return true.
*
- * @param text The legacy text to parse.
- * @return Returns a text component of the legacy text.
+ * @param component The component to test if null or empty.
+ * @return Returns true if the component is null or has no visible content.
*/
- public static TextComponent fromLegacyText(final String text) {
- if (Strings.isNullOrEmpty(text)) {
- return new TextComponent(text);
+ public static boolean isBaseComponent(@Nullable final Component component) {
+ if (component == null) {
+ return false;
}
- return new TextComponent(TextComponent.fromLegacyText(text, ChatColor.RESET));
- }
-
- /**
- * This is an easy way to create a text component when all you want to do is colour it.
- *
- * @param value The value of the text. (Objects will be stringified)
- * @param formats The colour formats.
- * @return Returns the created component, so you can do more stuff to it.
- */
- public static TextComponent textComponent(final Object value, final ChatColor... formats) {
- final TextComponent component = new TextComponent(value == null ? "" : value.toString());
- if (!ArrayUtils.isEmpty(formats)) {
- for (final ChatColor format : formats) {
- if (format == null) {
- //continue;
- }
- else if (format.getColor() != null) {
- component.setColor(format);
- }
- else if (format == ChatColor.RESET) {
- component.setColor(format);
- component.setBold(false);
- component.setItalic(false);
- component.setUnderlined(false);
- component.setStrikethrough(false);
- component.setObfuscated(false);
- }
- else if (format == ChatColor.BOLD) {
- component.setBold(true);
- }
- else if (format == ChatColor.ITALIC) {
- component.setItalic(true);
- }
- else if (format == ChatColor.UNDERLINE) {
- component.setUnderlined(true);
- }
- else if (format == ChatColor.STRIKETHROUGH) {
- component.setStrikethrough(true);
- }
- else if (format == ChatColor.MAGIC) {
- component.setObfuscated(true);
- }
- }
+ return (!(component instanceof TextComponent textComponent) || StringUtils.isEmpty(textComponent.content()))
+ && !component.children().isEmpty()
+ && component.clickEvent() == null
+ && component.hoverEvent() == null
+ && !component.hasStyling();
+ }
+
+ private static final Map NORMALISED_DECORATION_MAP =
+ Map.of(TextDecoration.ITALIC, TextDecoration.State.FALSE);
+
+ /**
+ * Checks whether a given component is the result of {@link #normaliseComponent(Component...)} or
+ * {@link #normaliseComponent(List)}.
+ *
+ * @param component The component to check.
+ * @return Returns true if the given component is "normalised."
+ */
+ public static boolean isNormalisedComponent(@Nullable final Component component) {
+ if (!(component instanceof final TextComponent textComponent)) {
+ return false;
+ }
+ return StringUtils.isEmpty(textComponent.content())
+ && !component.children().isEmpty()
+ && component.clickEvent() == null
+ && component.hoverEvent() == null
+ && Objects.equals(component.color(), NamedTextColor.WHITE)
+ && Objects.equals(component.decorations(), NORMALISED_DECORATION_MAP);
+ }
+
+ /**
+ * Wraps a component or series of components into a "normalised" display component, meaning that the text is
+ * white and non-italic by default.
+ *
+ * @param components The component / components to wrap.
+ * @return Returns the normalised component, or empty if no components are passed.
+ */
+ @Nonnull
+ public static Component normaliseComponent(final Component... components) {
+ if (ArrayUtils.isEmpty(components)) {
+ return Component.empty();
+ }
+ return Component.text()
+ .color(NamedTextColor.WHITE)
+ .decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE)
+ .append(components)
+ .build();
+ }
+
+ /**
+ * Wraps a series of components into a "normalised" display component, meaning that the text is white and
+ * non-italic by default.
+ *
+ * @param components The components to wrap.
+ * @return Returns the normalised component, or empty if no components are passed.
+ */
+ @Nonnull
+ public static Component normaliseComponent(@Nullable final List components) {
+ if (CollectionUtils.isEmpty(components)) {
+ return Component.empty();
+ }
+ return Component.text()
+ .color(NamedTextColor.WHITE)
+ .decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE)
+ .append(components)
+ .build();
+ }
+
+ /**
+ * This will also happily translate any translatable components.
+ *
+ * @param component The component to stringify.
+ * @return Returns a stringified version of the given component.
+ */
+ @Nonnull
+ public static String stringify(@Nullable final Component component) {
+ return component == null || component == Component.empty() ? "" :
+ CraftChatMessage.fromComponent(PaperAdventure.asVanilla(component));
+ }
+
+ /**
+ * Upgrades a legacy string (eg: §6Hello, World!) to a Kyori component.
+ *
+ * @param string The string to convert into a component.
+ * @return Returns a new component, or null if the given string was null.
+ */
+ @Contract("!null -> !null")
+ @Nullable
+ public static Component upgradeLegacyString(@Nullable final String string) {
+ return string == null ? null : string.isEmpty() ? Component.empty() :
+ LegacyComponentSerializer.legacySection().deserialize(string);
+ }
+
+ /**
+ * @return Generates a new text component that's specifically NOT italicised. Use this for item names and
+ * lore.
+ */
+ @Nonnull
+ public static TextComponent newComponent() {
+ return newComponent("");
+ }
+
+ /**
+ * Generates a new text component that's specifically NOT italicised. Use this for item names and lore.
+ *
+ * @param content The text content for the component.
+ * @return Returns the generated text component.
+ */
+ @Nonnull
+ public static TextComponent newComponent(final String content) {
+ return Component.text(Objects.requireNonNull(content))
+ .decoration(TextDecoration.ITALIC, TextDecoration.State.FALSE);
+ }
+
+ /**
+ * Clones a component.
+ *
+ * @param component The component to clone.
+ * @return Returns a clone of the given component.
+ *
+ * @deprecated Kyori components are immutable, so any methods that offer to update the component will actually
+ * instantiate a new component with the modification, thus making this utility unnecessary, if not
+ * downright inefficient.
+ */
+ @Contract("!null -> !null")
+ @Nullable
+ @Deprecated(forRemoval = true)
+ public static Component cloneComponent(@Nullable final Component component) {
+ return component == null ? null : component.style(component.style());
+ }
+
+ /**
+ * Determines whether two given components are equal to each other.
+ *
+ * @param former The left hand side component.
+ * @param latter The right hand side component.
+ * @return Returns whether the two given components are equal.
+ */
+ public static boolean areComponentsEqual(@Nullable final Component former,
+ @Nullable final Component latter) {
+ if (Objects.equals(former, latter)) {
+ return true;
+ }
+ if (former == null || latter == null) {
+ return false;
+ }
+ if (StringUtils.equals(
+ MiniMessage.get().serialize(former),
+ MiniMessage.get().serialize(latter))) {
+ return true;
+ }
+ if (StringUtils.equals(
+ LegacyComponentSerializer.legacyAmpersand().serialize(former),
+ LegacyComponentSerializer.legacyAmpersand().serialize(latter))) {
+ return true;
}
- return component;
+ return false;
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chat/Componentify.java b/src/main/java/vg/civcraft/mc/civmodcore/chat/Componentify.java
new file mode 100644
index 00000000..8a5b9855
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/chat/Componentify.java
@@ -0,0 +1,69 @@
+package vg.civcraft.mc.civmodcore.chat;
+
+import javax.annotation.Nonnull;
+import lombok.experimental.UtilityClass;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Location;
+
+@UtilityClass
+public final class Componentify {
+
+ private static Component INTERNAL_addLocationWorld(final Location location) {
+ if (location.isWorldLoaded()) {
+ return Component.text(location.getWorld().getName())
+ .hoverEvent(HoverEvent.showText(Component.text("World name")));
+ }
+ else {
+ return Component.text("")
+ .color(NamedTextColor.RED)
+ .hoverEvent(HoverEvent.showText(Component.text("World not specified / loaded")));
+ }
+ }
+
+ public static Component fullLocation(@Nonnull final Location location) {
+ final var component = Component.text();
+ component.append(INTERNAL_addLocationWorld(location));
+ component.append(Component.space());
+ component.append(Component.text(location.getX())
+ .color(NamedTextColor.RED)
+ .hoverEvent(HoverEvent.showText(Component.text("X"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getY())
+ .color(NamedTextColor.GREEN)
+ .hoverEvent(HoverEvent.showText(Component.text("Y"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getZ())
+ .color(NamedTextColor.BLUE)
+ .hoverEvent(HoverEvent.showText(Component.text("Z"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getYaw())
+ .color(NamedTextColor.GOLD)
+ .hoverEvent(HoverEvent.showText(Component.text("Yaw"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getPitch())
+ .color(NamedTextColor.AQUA)
+ .hoverEvent(HoverEvent.showText(Component.text("Pitch"))));
+ return component.build();
+ }
+
+ public static Component blockLocation(@Nonnull final Location location) {
+ final var component = Component.text();
+ component.append(INTERNAL_addLocationWorld(location));
+ component.append(Component.space());
+ component.append(Component.text(location.getBlockX())
+ .color(NamedTextColor.RED)
+ .hoverEvent(HoverEvent.showText(Component.text("Block X"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getBlockY())
+ .color(NamedTextColor.GREEN)
+ .hoverEvent(HoverEvent.showText(Component.text("Block X"))));
+ component.append(Component.space());
+ component.append(Component.text(location.getBlockZ())
+ .color(NamedTextColor.BLUE)
+ .hoverEvent(HoverEvent.showText(Component.text("Block X"))));
+ return component.build();
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/DialogManager.java b/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/DialogManager.java
index 56dab136..7ddf7daf 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/DialogManager.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/DialogManager.java
@@ -18,7 +18,7 @@ public class DialogManager implements Listener {
public static final DialogManager INSTANCE = new DialogManager();
private static final Map DIALOGS = new TreeMap<>();
-
+
private DialogManager() {}
public static Dialog getDialog(final UUID player) {
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/ChatListener.java b/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/ChatListener.java
deleted file mode 100644
index 596783bd..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/ChatListener.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package vg.civcraft.mc.civmodcore.chatDialog;
-
-import java.util.Collections;
-import java.util.List;
-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.PlayerQuitEvent;
-import org.bukkit.event.server.TabCompleteEvent;
-
-@Deprecated
-public class ChatListener implements Listener {
-
- @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
- public void tabComplete(TabCompleteEvent e) {
- if (!(e.getSender() instanceof Player)) {
- return;
- }
- Dialog dia = DialogManager.getDialog((Player) e.getSender());
- if (dia != null) {
- String[] split = e.getBuffer().split(" ");
- List complet = dia.onTabComplete(split.length > 0 ? split[split.length - 1] : "", split);
- if (complet == null) {
- complet = Collections.emptyList();
- }
- e.setCompletions(complet);
- }
- }
-
- @EventHandler
- public void logoff(PlayerQuitEvent e) {
- DialogManager.forceEndDialog(e.getPlayer());
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/Dialog.java b/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/Dialog.java
deleted file mode 100644
index 9aa61ed7..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/Dialog.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package vg.civcraft.mc.civmodcore.chatDialog;
-
-import java.util.List;
-import org.bukkit.Bukkit;
-import org.bukkit.conversations.Conversation;
-import org.bukkit.conversations.ConversationContext;
-import org.bukkit.conversations.ConversationFactory;
-import org.bukkit.conversations.Prompt;
-import org.bukkit.conversations.StringPrompt;
-import org.bukkit.entity.Player;
-import org.bukkit.plugin.java.JavaPlugin;
-
-/**
- * @deprecated Use {@link vg.civcraft.mc.civmodcore.chat.dialog.Dialog} instead.
- */
-@Deprecated
-public abstract class Dialog {
-
- protected Player player;
-
- private Conversation convo;
-
- public Dialog(Player player, JavaPlugin plugin) {
- this(player, plugin, null);
- }
-
- public Dialog(Player player, JavaPlugin plugin, final String toDisplay) {
- DialogManager.registerDialog(player, this);
- this.player = player;
-
- Bukkit.getScheduler().runTask(plugin, (Runnable) player::closeInventory);
-
- convo = new ConversationFactory(plugin).withModality(false).withLocalEcho(false)
- .withFirstPrompt(new StringPrompt() {
-
- @Override
- public String getPromptText(ConversationContext arg0) {
- if (toDisplay != null) {
- return toDisplay;
- }
- return "";
- }
-
- @Override
- public Prompt acceptInput(ConversationContext arg0, String arg1) {
- onReply(arg1.split(" "));
- return Prompt.END_OF_CONVERSATION;
- }
-
- }).buildConversation(player);
-
- convo.begin();
- }
-
- public abstract void onReply(String[] message);
-
- public abstract List onTabComplete(String wordCompleted, String[] fullMessage);
-
- public Player getPlayer() {
- return player;
- }
-
- public void end() {
- convo.abandon();
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/DialogManager.java b/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/DialogManager.java
deleted file mode 100644
index 9cbcc598..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/DialogManager.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package vg.civcraft.mc.civmodcore.chatDialog;
-
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.UUID;
-import org.bukkit.entity.Player;
-
-/**
- * @deprecated Use {@link vg.civcraft.mc.civmodcore.chat.dialog.DialogManager} instead.
- */
-@Deprecated
-public class DialogManager {
-
- private static Map dialogs = new TreeMap<>();
-
- private DialogManager() {}
-
- public static Dialog getDialog(Player p) {
- return getDialog(p.getUniqueId());
- }
-
- public static Dialog getDialog(UUID uuid) {
- return dialogs.get(uuid);
- }
-
- public static void registerDialog(Player p, Dialog dialog) {
- Dialog current = dialogs.get(p.getUniqueId());
- if (current != null) {
- current.end();
- }
- dialogs.put(p.getUniqueId(), dialog);
- }
-
- public static void forceEndDialog(Player p) {
- forceEndDialog(p.getUniqueId());
- }
-
- public static void forceEndDialog(UUID uuid) {
- Dialog dia = dialogs.remove(uuid);
- if (dia != null) {
- dia.end();
- }
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/LDialog.java b/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/LDialog.java
deleted file mode 100644
index 8dd875dd..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/LDialog.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package vg.civcraft.mc.civmodcore.chatDialog;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-import org.bukkit.entity.Player;
-import vg.civcraft.mc.civmodcore.CivModCorePlugin;
-
-/**
- * @deprecated Create an anonymous instance of {@link vg.civcraft.mc.civmodcore.chat.dialog.Dialog} instead.
- */
-@Deprecated
-public class LDialog extends Dialog {
-
- private Consumer replyFunction;
-
- public LDialog(Player player, Consumer replyFunction) {
- this(player, replyFunction, null);
- }
-
- public LDialog(Player player, Consumer replyFunction, String msgToShow) {
- super(player, CivModCorePlugin.getInstance(), msgToShow);
- this.replyFunction = replyFunction;
- }
-
- @Override
- public void onReply(String[] message) {
- replyFunction.accept(String.join(" ", message));
- }
-
- @Override
- public List onTabComplete(String wordCompleted, String[] fullMessage) {
- return Collections.emptyList();
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/package-info.java b/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/package-info.java
deleted file mode 100644
index 2ee0705f..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/chatDialog/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * The goal with this package is to relocate it to "chat.dialog"
- */
-package vg.civcraft.mc.civmodcore.chatDialog;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommand.java
deleted file mode 100644
index 7c0ec819..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommand.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import co.aikar.commands.BaseCommand;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Class that represents a compatible command for {@link AikarCommandManager}.
- */
-public abstract class AikarCommand extends BaseCommand {
-
- /**
- * This annotation, when used on a method on an {@link AikarCommand} extending class will be automatically
- * registered as a tab completion.
- *
- * The return type of the method MUST, MUST be {@code List}, otherwise it will fail.
- *
- * The method is permitted to have 0-1 parameters, but that parameter MUST, MUST be of type
- * {@code BukkitCommandCompletionContext}, otherwise it will fail.
- */
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface TabComplete {
- String value();
- boolean async() default false;
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommandManager.java b/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommandManager.java
deleted file mode 100644
index 27890ad4..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/AikarCommandManager.java
+++ /dev/null
@@ -1,267 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import static vg.civcraft.mc.civmodcore.command.AikarCommand.TabComplete;
-
-import co.aikar.commands.BukkitCommandCompletionContext;
-import co.aikar.commands.BukkitCommandExecutionContext;
-import co.aikar.commands.BukkitCommandManager;
-import co.aikar.commands.CommandCompletions;
-import co.aikar.commands.CommandCompletions.CommandCompletionHandler;
-import co.aikar.commands.CommandContexts;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.common.collect.Maps;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.reflect.FieldUtils;
-import org.bukkit.Bukkit;
-import org.bukkit.Material;
-import org.bukkit.OfflinePlayer;
-import vg.civcraft.mc.civmodcore.ACivMod;
-import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils;
-
-/**
- * Command registration class wrapper around {@link BukkitCommandManager}.
- */
-public class AikarCommandManager {
-
- private final ACivMod plugin;
-
- private CustomBukkitManager manager;
-
- /**
- * Creates a new command manager for Aikar based commands and tab completions.
- *
- * @param plugin The plugin to bind this manager to.
- */
- public AikarCommandManager(ACivMod plugin) {
- this(plugin, true);
- }
-
- /**
- * Creates a new command manager for Aikar based commands and tab completions.
- *
- * @param plugin The plugin to bind this manager to.
- * @param autoInit If true will automatically initialise and register all commands and completions as defined within
- * {@link AikarCommandManager#registerCommands()} and
- * {@link AikarCommandManager#registerCompletions(CommandCompletions)}.
- */
- public AikarCommandManager(ACivMod plugin, boolean autoInit) {
- this.plugin = plugin;
- if (autoInit) {
- init();
- }
- }
-
- /**
- * @deprecated Use {@link #init()} instead as it's more indicative of what's happening.
- */
- @Deprecated
- public final void register() {
- init();
- }
-
- /**
- * Will initialise the manager and register both commands and completions. You should only really use this if
- * you've used {@link AikarCommandManager#reset()} or both {@link AikarCommandManager#deregisterCommands()} and
- * {@link AikarCommandManager#deregisterCompletions()}, otherwise there may be issues.
- */
- public final void init() {
- this.manager = new CustomBukkitManager(plugin);
- registerCommands();
- registerCompletions(this.manager.getCommandCompletions());
- registerContexts(this.manager.getCommandContexts());
- }
-
- /**
- * This is called as part of {@link AikarCommandManager#register()} and should be overridden by an extending class
- * to register all (or as many) commands at once.
- */
- public void registerCommands() { }
-
- /**
- * This is called as part of {@link AikarCommandManager#register()} and should be overridden by an extending class
- * to register all (or as many) completions at once, though make sure to call super.
- *
- * @param completions The completion manager is given. It is the same manager that can be reached via
- * {@link CustomBukkitManager#getCommandCompletions()}.
- */
- public void registerCompletions(CommandCompletions completions) {
- completions.registerAsyncCompletion("allplayers", context ->
- Arrays.stream(Bukkit.getOfflinePlayers())
- .map(OfflinePlayer::getName)
- .filter(name -> StringUtils.startsWithIgnoreCase(name, context.getInput()))
- .collect(Collectors.toCollection(ArrayList::new)));
- completions.registerAsyncCompletion("materials", context ->
- Arrays.stream(Material.values())
- .map(Enum::name)
- .filter((name) -> StringUtils.startsWithIgnoreCase(name, context.getInput()))
- .collect(Collectors.toCollection(ArrayList::new)));
- completions.registerAsyncCompletion("itemMaterials", context ->
- Arrays.stream(Material.values())
- .filter(ItemUtils::isValidItemMaterial)
- .map(Enum::name)
- .filter((name) -> StringUtils.startsWithIgnoreCase(name, context.getInput()))
- .collect(Collectors.toCollection(ArrayList::new)));
- }
-
- /**
- * This is called as part of {@link AikarCommandManager#register()} and should be overridden by an extending class
- * to register all (or as many) contexts at once.
- *
- * @param contexts The context manager is given. It is the same manager that can be reached via
- * {@link CustomBukkitManager#getCommandContexts()}.
- */
- public void registerContexts(CommandContexts contexts) { }
-
- /**
- * Registers a new command and any attached tab completions.
- *
- * @param command The command instance to register.
- */
- public final void registerCommand(AikarCommand command) {
- Preconditions.checkArgument(command != null, "Could not register that command: the command was null.");
- this.manager.registerCommand(command);
- this.plugin.info("Command [" + command.getClass().getSimpleName() + "] registered.");
- for (Map.Entry entry : getTabCompletions(command.getClass()).entrySet()) {
- if (entry.getValue().async()) {
- this.manager.getCommandCompletions().registerAsyncCompletion(entry.getValue().value(), (context) ->
- runCommandCompletion(context, command, entry.getValue().value(), entry.getKey()));
- }
- else {
- this.manager.getCommandCompletions().registerCompletion(entry.getValue().value(), (context) ->
- runCommandCompletion(context, command, entry.getValue().value(), entry.getKey()));
- }
- this.plugin.info("Command Completer [" + entry.getValue().value() + "] registered.");
- }
- }
-
- /**
- * Deregisters a command and any attached tab completions.
- *
- * @param command The command instance to register.
- */
- @SuppressWarnings("unchecked")
- public final void deregisterCommand(AikarCommand command) {
- Preconditions.checkArgument(command != null, "Could not deregister that command: the command was null.");
- this.manager.unregisterCommand(command);
- this.plugin.info("Command [" + command.getClass().getSimpleName() + "] deregistered.");
- Map> internal;
- try {
- internal = (HashMap>)
- FieldUtils.readField(this.manager.getCommandCompletions(), "completionMap", true);
- }
- catch (Exception exception) {
- throw new UnsupportedOperationException("Could not get internal completion map.", exception);
- }
- for (TabComplete complete : getTabCompletions(command.getClass()).values()) {
- internal.remove(complete.value().toLowerCase(Locale.ENGLISH));
- this.plugin.info("Command Completer [" + complete.value() + "] deregistered.");
- }
- }
-
- /**
- * Deregisters all commands.
- */
- public final void deregisterCommands() {
- this.manager.unregisterCommands();
- }
-
- /**
- * Deregisters all command completions.
- */
- public final void deregisterCompletions() {
- this.manager.unregisterCompletions();
- }
-
- /**
- * Resets the manager, resetting all commands and completions.
- */
- public final void reset() {
- this.manager.unregisterCommands();
- this.manager.unregisterCompletions();
- this.manager = null;
- }
-
- /**
- * Retrieves the internal manager this class wraps.
- *
- * @return Returns the internal manager.
- */
- public final CustomBukkitManager getInternalManager() {
- return this.manager;
- }
-
- // ------------------------------------------------------------
- // Utilities
- // ------------------------------------------------------------
-
- @SuppressWarnings("unchecked")
- private List runCommandCompletion(BukkitCommandCompletionContext context, AikarCommand command,
- String id, Method method) {
- try {
- method.setAccessible(true);
- switch (method.getParameterCount()) {
- case 0:
- return (List) method.invoke(command);
- case 1:
- return (List) method.invoke(command, context);
- default:
- throw new UnsupportedOperationException("Unsupported number of parameters.");
- }
- }
- catch (Exception exception) {
- this.plugin.warning("Could not tab complete [@" + id + "]: an error with the handler!", exception);
- return Collections.emptyList();
- }
- }
-
- private static Map getTabCompletions(Class extends AikarCommand> clazz) {
- Map completions = Maps.newHashMap();
- if (clazz == null) {
- return completions;
- }
- for (Method method : clazz.getDeclaredMethods()) {
- if (!Modifier.isPublic(method.getModifiers())) {
- continue;
- }
- if (!List.class.isAssignableFrom(method.getReturnType())) {
- continue;
- }
- if (method.getParameterCount() > 1) {
- continue;
- }
- TabComplete tabComplete = method.getAnnotation(TabComplete.class);
- if (tabComplete == null) {
- continue;
- }
- if (Strings.isNullOrEmpty(tabComplete.value())) {
- continue;
- }
- completions.put(method, tabComplete);
- }
- return completions;
- }
-
- public static class CustomBukkitManager extends BukkitCommandManager {
-
- public CustomBukkitManager(ACivMod plugin) {
- super(plugin);
- }
-
- public void unregisterCompletions() {
- this.completions = null;
- }
-
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/CivCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/command/CivCommand.java
deleted file mode 100644
index 0074b407..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/CivCommand.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-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.TYPE)
-public @interface CivCommand {
-
- String id();
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/CivConfigAnnotationProcessor.java b/src/main/java/vg/civcraft/mc/civmodcore/command/CivConfigAnnotationProcessor.java
deleted file mode 100644
index 4edfc902..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/CivConfigAnnotationProcessor.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.Set;
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Filer;
-import javax.annotation.processing.RoundEnvironment;
-import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
-import javax.lang.model.SourceVersion;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ElementKind;
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.ExecutableType;
-import javax.lang.model.util.Elements;
-import javax.tools.Diagnostic.Kind;
-import javax.tools.FileObject;
-import javax.tools.StandardLocation;
-
-@SupportedSourceVersion(value=SourceVersion.RELEASE_8)
-@SupportedAnnotationTypes("vg.civcraft.mc.civmodcore.command.CivCommand")
-public class CivConfigAnnotationProcessor extends AbstractProcessor {
-
- public static final String fileLocation = "META-INF/civmodcore/civconfig";
-
- @Override
- public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- if (roundEnv.processingOver()) {
- return false;
- }
- Set pluginClasses = new HashSet<>();
- Elements elements = processingEnv.getElementUtils();
- for (Element element : roundEnv.getElementsAnnotatedWith(CivCommand.class)) {
- CivCommand annot = element.getAnnotation(CivCommand.class);
- if (annot == null) {
- // ????
- continue;
- }
- if (!validConstructor(element)) {
- return true;
- }
- pluginClasses.add(elements.getBinaryName((TypeElement) element).toString());
- }
- // load existing
- Filer filer = processingEnv.getFiler();
- try {
- FileObject file = filer.getResource(StandardLocation.CLASS_OUTPUT, "", fileLocation);
- BufferedReader reader = new BufferedReader(new InputStreamReader(file.openInputStream(), "UTF-8"));
- String line;
- while ((line = reader.readLine()) != null) {
- pluginClasses.add(line);
- }
- reader.close();
- } catch (java.nio.file.NoSuchFileException | FileNotFoundException x) {
- // doesn't exist yet, that's fine
- } catch (IOException x) {
- processingEnv.getMessager().printMessage(Kind.ERROR,
- "Failed to load existing service definition files: " + x);
- }
-
- // write back
- try {
- FileObject f = filer.createResource(StandardLocation.CLASS_OUTPUT, "", fileLocation);
- PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(f.openOutputStream(), "UTF-8"));
- processingEnv.getMessager().printMessage(Kind.NOTE, "Writing " + f.getName());
- for (String value : pluginClasses) {
- printWriter.println(value);
- }
- printWriter.close();
- } catch (IOException x) {
- processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write service definition files: " + x);
- }
- return true;
- }
-
- private boolean validConstructor(Element el) {
- for (Element subelement : el.getEnclosedElements()) {
- if (subelement.getKind() == ElementKind.CONSTRUCTOR) {
- if (!subelement.getModifiers().contains(Modifier.PUBLIC)) {
- processingEnv.getMessager().printMessage(Kind.ERROR,
- "Invalid constructor visibility for plugin " + subelement.toString());
- return false;
- }
- ExecutableType mirror = (ExecutableType) subelement.asType();
- if (!mirror.getParameterTypes().isEmpty()) {
- processingEnv.getMessager().printMessage(Kind.ERROR,
- "Invalid constructor for plugin, taking arguments is not allowed: " + subelement.toString());
- return false;
- }
- }
- }
- return true;
- }
-
-}
-
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/Command.java b/src/main/java/vg/civcraft/mc/civmodcore/command/Command.java
deleted file mode 100644
index 5f74e99e..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/Command.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.util.List;
-import org.bukkit.command.CommandSender;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiter;
-
-public interface Command {
-
- boolean execute(CommandSender sender, String[] args);
-
- List tabComplete(CommandSender sender, String[] args);
-
- String getName();
-
- String getDescription();
-
- String getUsage();
-
- String getIdentifier();
-
- int getMaxArguments();
-
- int getMinArguments();
-
- void postSetup();
-
- boolean getSenderMustBePlayer();
-
- boolean getErrorOnTooManyArgs();
-
- void setSender(CommandSender sender);
-
- void setArgs(String[] args);
-
- RateLimiter getRateLimiter();
-
- RateLimiter getTabCompletionRateLimiter();
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/CommandHandler.java b/src/main/java/vg/civcraft/mc/civmodcore/command/CommandHandler.java
deleted file mode 100644
index 9b1c3233..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/CommandHandler.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiter;
-import vg.civcraft.mc.civmodcore.util.TextUtil;
-
-@Deprecated
-public abstract class CommandHandler {
-
- private static final String cmdMustBePlayer = "This command can only be used by in-game players.";
- private static final String cmdRateLimited = "You have run this command too often and have to wait before running it again.";
-
- public Map commands = new HashMap<>();
-
- public abstract void registerCommands();
-
- protected void addCommands(Command command) {
- commands.put(command.getIdentifier().toLowerCase(), command);
- }
-
- public boolean execute(CommandSender sender, org.bukkit.command.Command cmd, String[] args) {
- if (commands.containsKey(cmd.getName().toLowerCase())) {
- Command command = commands.get(cmd.getName().toLowerCase());
- boolean isPlayer = sender instanceof Player;
- if (command.getSenderMustBePlayer() && !isPlayer) {
- sender.sendMessage(TextUtil.parse(cmdMustBePlayer));
- return true;
- }
- if (args.length < command.getMinArguments()
- || (command.getErrorOnTooManyArgs() && args.length > command.getMaxArguments())) {
- helpPlayer(command, sender);
- return true;
- }
- RateLimiter limiter = command.getRateLimiter();
- if (limiter != null && isPlayer) {
- if (!limiter.pullToken((Player) sender)) {
- sender.sendMessage(TextUtil.parse(cmdRateLimited));
- return true;
- }
- }
- command.setSender(sender);
- command.setArgs(args);
- command.execute(sender, args);
- }
- else {
- sender.sendMessage("Command was registered in plugin.yml, but not registered in command handler, tell a dev about this");
- }
- return true;
- }
-
- public List complete(CommandSender sender, org.bukkit.command.Command cmd, String[] args) {
- if (commands.containsKey(cmd.getName().toLowerCase())) {
- Command command = commands.get(cmd.getName().toLowerCase());
- boolean isPlayer = sender instanceof Player;
- if (command.getSenderMustBePlayer() && !isPlayer) {
- sender.sendMessage(TextUtil.parse(cmdMustBePlayer));
- return null;
- }
- RateLimiter limiter = command.getTabCompletionRateLimiter();
- if (limiter != null && isPlayer) {
- if (!limiter.pullToken((Player) sender)) {
- sender.sendMessage(TextUtil.parse(cmdRateLimited));
- return null;
- }
- }
-
- command.setSender(sender);
- command.setArgs(args);
- List completes = command.tabComplete(sender, args);
- String completeArg;
- if (args.length == 0) {
- completeArg = "";
- } else {
- completeArg = args[args.length - 1].toLowerCase();
- }
- if (completes == null) {
- completes = new ArrayList<>();
- for (Player p : Bukkit.getOnlinePlayers()) {
- if (p.getName().toLowerCase().startsWith(completeArg)) {
- completes.add(p.getName());
- }
- }
- return completes;
- } else {
- return completes;
- }
- }
- return null;
- }
-
- protected void helpPlayer(Command command, CommandSender sender) {
- sender.sendMessage(new StringBuilder().append(ChatColor.RED + "Command: ").append(command.getName()).toString());
- sender.sendMessage(new StringBuilder().append(ChatColor.RED + "Description: ").append(command.getDescription())
- .toString());
- sender.sendMessage(new StringBuilder().append(ChatColor.RED + "Usage: ").append(command.getUsage()).toString());
- }
-
- public void reset() {
- this.commands.clear();
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/MailBoxAPI.java b/src/main/java/vg/civcraft/mc/civmodcore/command/MailBoxAPI.java
deleted file mode 100644
index 41c86cb9..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/MailBoxAPI.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import com.google.common.base.Preconditions;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import vg.civcraft.mc.civmodcore.playersettings.PlayerSettingAPI;
-import vg.civcraft.mc.civmodcore.playersettings.impl.collection.ListSetting;
-
-public class MailBoxAPI {
-
- private static ListSetting mail;
-
- /**
- * Internal setup, should only be called when CivModCore is enabling
- */
- public static void setup() {
- if (mail != null) {
- throw new IllegalStateException("Was already registed");
- }
- /*mail = new ListSetting<>(CivModCorePlugin.getInstance(), "Mail box", "cmcMailBox",
- new ItemStack(Material.STICK), null, String.class, false); */
- PlayerSettingAPI.registerSetting(mail, null);
- }
-
- /**
- * Adds a new messages to the players mail box
- * @param player UUID of the player to send message to
- * @param msg Message to add
- */
- public static void addMail(UUID player, String msg) {
- Preconditions.checkNotNull(player, "Player may not be null");
- Preconditions.checkNotNull(msg, "Message to add may not be null");
- mail.addElement(player, msg);
- }
-
- /**
- * Gets all pending messages in a players mail box
- * @param player Player to mail box of
- * @return Messages in the players mail box
- */
- public static List getMail(UUID player) {
- Preconditions.checkNotNull(player, "Player may not be null");
- return mail.getValue(player);
- }
-
- /**
- * Clears all mail a player has
- * @param player
- */
- public static void clearMail(UUID player) {
- Preconditions.checkNotNull(player, "Player may not be null");
- mail.setValue(player, new ArrayList<>());
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/PlayerCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/command/PlayerCommand.java
deleted file mode 100644
index 69c71660..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/PlayerCommand.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import org.bukkit.Bukkit;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.entity.Player;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiter;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiting;
-import vg.civcraft.mc.civmodcore.util.TextUtil;
-
-@Deprecated
-public abstract class PlayerCommand implements Command {
-
- protected String name = "";
- protected String description = "";
- private String usage = "";
- protected String identifier = "";
- protected int min = 0;
- protected int max = 0;
- protected boolean senderMustBePlayer = false;
- protected boolean errorOnTooManyArgs = true;
- protected CommandSender sender;
- protected String[] args;
- protected RateLimiter rateLimiter;
- protected RateLimiter tabCompletionRateLimiter;
-
- public PlayerCommand(String name) {
- this.name = name;
- }
-
- @Override
- @Deprecated
- public String getName() {
- return name;
- }
-
- @Override
- public String getDescription() {
- return description;
- }
-
- @Override
- public String getUsage() {
- return usage;
- }
-
- @Override
- public String getIdentifier() {
- return identifier;
- }
-
- @Override
- public int getMinArguments() {
- return min;
- }
-
- @Override
- public int getMaxArguments() {
- return max;
- }
-
- @Override
- public boolean getSenderMustBePlayer() {
- return senderMustBePlayer;
- }
-
- @Override
- public boolean getErrorOnTooManyArgs() {
- return this.errorOnTooManyArgs;
- }
-
- @Override
- public void setSender(CommandSender sender) {
- this.sender = sender;
- }
-
- @Override
- @Deprecated
- public void setArgs(String[] args) {
- this.args = args;
- }
-
- @Deprecated
- public String[] getArgs() {
- return args;
- }
-
- @Override
- public void postSetup() {
- PluginCommand cmd = Bukkit.getPluginCommand(identifier);
- if (cmd != null) {
- cmd.setDescription(this.description);
- cmd.setUsage(this.usage);
- }
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public void setDescription(String description) {
- this.description = description;
- postSetup();
- }
-
- public void setUsage(String usage) {
- this.usage = usage;
- postSetup();
- }
-
- public void setIdentifier(String identifier) {
- this.identifier = identifier;
- }
-
- public void setArguments(int min, int max) {
- this.min = min;
- this.max = max;
- }
-
- public boolean sendPlayerMessage(Player p, String m, boolean flag) {
- p.sendMessage(m);
- return flag;
- }
-
- public void setSenderMustBePlayer(boolean senderMustBePlayer) {
- this.senderMustBePlayer = senderMustBePlayer;
- }
-
- public void setErrorOnTooManyArgs(boolean errorOnTooManyArgs) {
- this.errorOnTooManyArgs = errorOnTooManyArgs;
- }
-
- public void setRateLimitingBehavior(int limit, int refillAmount, int refillIntervallInSeconds) {
- this.rateLimiter = RateLimiting.createRateLimiter("COMMAND_" + identifier,
- limit, limit, refillAmount, ((long) refillIntervallInSeconds) * 1000);
- }
-
- public void setTabCompletionRateLimitingBehavior(int limit, int refillAmount, int refillIntervallInSeconds) {
- this.tabCompletionRateLimiter = RateLimiting.createRateLimiter("COMMAND_" + identifier,
- limit, limit, refillAmount, ((long) refillIntervallInSeconds) * 1000);
- }
-
- public RateLimiter getRateLimiter() {
- return rateLimiter;
- }
-
- public RateLimiter getTabCompletionRateLimiter() {
- return tabCompletionRateLimiter;
- }
-
- public Player player() {
- return (Player) sender;
- }
-
- public void msg(String msg) {
- sender.sendMessage(parse(msg));
- }
-
- public void msg(String msg, Object... args) {
- sender.sendMessage(parse(msg, args));
- }
-
- public String parse(String text) {
- return TextUtil.parse(text);
- }
-
- public String parse(String text, Object... args) {
- return TextUtil.parse(text, args);
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommand.java
deleted file mode 100644
index 25cb2e3d..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommand.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.function.Function;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiter;
-
-public abstract class StandaloneCommand {
-
- protected int minArgs = 0;
- protected int maxArgs = Integer.MAX_VALUE;
- protected boolean mustBePlayer = false;
- protected boolean mustBeConsole = false;
- protected RateLimiter rateLimiter;
- protected RateLimiter tabCompletionRateLimiter;
-
- public abstract boolean execute(CommandSender sender, String[] args);
-
- public abstract List tabComplete(CommandSender sender, String[] args);
-
- public String getIdentifier() {
- Class extends StandaloneCommand> pluginClass = this.getClass();
- CivCommand annot = pluginClass.getAnnotation(CivCommand.class);
- if (annot == null) {
- return null;
- }
- return annot.id();
- }
-
- boolean isRateLimitedToExecute(Player p) {
- if (rateLimiter == null) {
- return false;
- }
- return rateLimiter.pullToken(p);
- }
-
- boolean isRateLimitedToTabComplete(Player p) {
- if (tabCompletionRateLimiter == null) {
- return false;
- }
- return tabCompletionRateLimiter.pullToken(p);
- }
-
- boolean hasTooManyArgs(int argLength) {
- return argLength > maxArgs;
- }
-
- boolean hasTooFewArgs(int argLength) {
- return argLength < minArgs;
- }
-
- void setMaxArgs(int maxArgs) {
- this.maxArgs = maxArgs;
- }
-
- void setMinArgs(int minArgs) {
- this.minArgs = minArgs;
- }
-
- void setRateLimiter(RateLimiter limiter) {
- this.rateLimiter = limiter;
- }
-
- void setTabCompletionRateLimiter(RateLimiter limiter) {
- this.tabCompletionRateLimiter = limiter;
- }
-
- void setSenderMustBePlayer(boolean mustBePlayer) {
- this.mustBePlayer = mustBePlayer;
- }
-
- void setSenderMustBeConsole(boolean mustBeConsole) {
- this.mustBeConsole = mustBeConsole;
- }
-
- boolean canBeRunByPlayers() {
- return !mustBeConsole;
- }
-
- boolean canBeRunByConsole() {
- return !mustBePlayer;
- }
-
- protected static List doTabComplete(String arg, Collection candidates, boolean caseSensitive) {
- return doTabComplete(arg, candidates, s -> s ,caseSensitive);
- }
-
- protected static List doTabComplete(String arg, Collection suppliers, Function function, boolean caseSensitive) {
- List result = new ArrayList<>();
- if (!caseSensitive) {
- arg = arg.toLowerCase();
- }
- for(T supplier : suppliers) {
- String candidate = function.apply(supplier);
- boolean matches;
- if (caseSensitive) {
- matches = candidate.startsWith(arg);
- }
- else {
- matches = candidate.toLowerCase().startsWith(arg);
- }
- if (matches) {
- result.add(candidate);
- }
- }
- return result;
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommandHandler.java b/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommandHandler.java
deleted file mode 100644
index 391e1772..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/StandaloneCommandHandler.java
+++ /dev/null
@@ -1,294 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.SimpleCommandMap;
-import org.bukkit.entity.Player;
-import org.bukkit.plugin.InvalidDescriptionException;
-import org.bukkit.plugin.PluginDescriptionFile;
-import org.bukkit.plugin.SimplePluginManager;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.plugin.java.JavaPluginLoader;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiter;
-import vg.civcraft.mc.civmodcore.ratelimiting.RateLimiting;
-import vg.civcraft.mc.civmodcore.util.ConfigParsing;
-
-public class StandaloneCommandHandler {
-
- private JavaPlugin plugin;
- private Map commands;
-
- public StandaloneCommandHandler(JavaPlugin plugin) {
- this.plugin = plugin;
- this.commands = new HashMap<>();
- loadAll();
- }
-
- public void registerCommand(StandaloneCommand command) {
- String id = command.getIdentifier();
- if (id == null) {
- plugin.getLogger()
- .warning("Could not register command " + command.getClass().getName() + ". Identifier was null");
- return;
- }
- // we dont do alias resolving, Bukkit will do that for us
- commands.put(id.toLowerCase(), command);
- }
-
- public boolean executeCommand(CommandSender sender, org.bukkit.command.Command cmd, String[] args) {
- StandaloneCommand command = commands.get(cmd.getName().toLowerCase());
- if (command == null) {
- plugin.getLogger()
- .warning("Could not execute command " + cmd.getName() + ", no implementation was provided");
- sender.sendMessage(ChatColor.RED + "Command not available");
- return true;
- }
- if (command.hasTooManyArgs(args.length)) {
- sender.sendMessage(ChatColor.RED + "You provided too many arguments");
- helpPlayer(command, sender);
- return false;
- }
- if (command.hasTooFewArgs(args.length)) {
- sender.sendMessage(ChatColor.RED + "You provided too few arguments");
- helpPlayer(command, sender);
- return false;
- }
- if (sender instanceof Player) {
- if (!command.canBeRunByPlayers()) {
- sender.sendMessage(ChatColor.RED + "This command can only be run from console");
- return true;
- }
- if (command.isRateLimitedToExecute((Player) sender)) {
- sender.sendMessage(
- ChatColor.RED + "You are rate limited and have to wait before running this command again");
- return true;
- }
- } else {
- // console
- if (!command.canBeRunByConsole()) {
- sender.sendMessage(ChatColor.RED + "This command can only be run by players");
- return true;
- }
- }
- boolean worked = command.execute(sender, args);
- if (!worked) {
- helpPlayer(command, sender);
- }
- return worked;
- }
-
- public List tabCompleteCommand(CommandSender sender, org.bukkit.command.Command cmd, String[] args) {
- StandaloneCommand command = commands.get(cmd.getName().toLowerCase());
- if (command == null) {
- plugin.getLogger().warning(
- "Could not tab complete command " + cmd.getName() + ", no implementation was provided");
- return Collections.emptyList();
- }
- if (sender instanceof Player) {
- if (!command.canBeRunByPlayers()) {
- sender.sendMessage(ChatColor.RED + "This command can only be used from console");
- return Collections.emptyList();
- }
- if (command.isRateLimitedToTabComplete((Player) sender)) {
- sender.sendMessage(ChatColor.RED
- + "You are rate limited and have to wait before tab completing this command again");
- return Collections.emptyList();
- }
- } else {
- // console
- if (!command.canBeRunByConsole()) {
- sender.sendMessage(ChatColor.RED + "This command can only be used by players");
- return Collections.emptyList();
- }
- }
- return command.tabComplete(sender, args);
- }
-
- protected void helpPlayer(StandaloneCommand sCommand, CommandSender sender) {
- org.bukkit.command.Command bukCom = getBukkitCommand(sCommand);
- if (bukCom == null) {
- sender.sendMessage(ChatColor.RED + "No help available for command " + sCommand.getIdentifier());
- return;
- }
- sender.sendMessage(ChatColor.RED + "Command: " + sCommand.getIdentifier());
- sender.sendMessage(ChatColor.RED + "Description: " + bukCom.getDescription());
- sender.sendMessage(ChatColor.RED + "Usage: " + bukCom.getUsage());
- }
-
- public org.bukkit.command.Command getBukkitCommand(StandaloneCommand command) {
- SimpleCommandMap commandMap;
- try {
- Field mapField = SimplePluginManager.class.getDeclaredField("commandMap");
- mapField.setAccessible(true);
- commandMap = (SimpleCommandMap) mapField.get(Bukkit.getPluginManager());
- } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
- plugin.getLogger().severe("Failed to retrieve command map field " + e.toString());
- return null;
- }
- return (org.bukkit.command.Command) commandMap.getCommand(command.getIdentifier());
- }
-
- private void loadAll() {
- File file = getPluginJar();
- if (file == null) {
- return;
- }
- @SuppressWarnings("deprecation")
- JavaPluginLoader pluginLoader = new JavaPluginLoader(Bukkit.getServer());
- PluginDescriptionFile pluginYml;
- try {
- pluginYml = pluginLoader.getPluginDescription(file);
- } catch (InvalidDescriptionException e1) {
- plugin.getLogger().severe("Plugin " + plugin.getName() + " had invalid plugin.yml");
- return;
- }
- try (JarFile jar = new JarFile(file)) {
- JarEntry entry = jar.getJarEntry(CivConfigAnnotationProcessor.fileLocation);
- if (entry == null) {
- // doesn't exist, that's fine
- return;
- }
- try (InputStream stream = jar.getInputStream(entry);
- InputStreamReader reader = new InputStreamReader(stream);
- BufferedReader buffer = new BufferedReader(reader)) {
- String line;
- while ((line = buffer.readLine()) != null) {
- loadCommand(line, pluginYml);
- }
- }
- } catch (IOException e) {
- plugin.getLogger().severe("Failed to load plugin.yml: " + e.toString());
- }
- }
-
- private File getPluginJar() {
- try {
- Method method = JavaPlugin.class.getDeclaredMethod("getFile");
- method.setAccessible(true);
- return (File) method.invoke(plugin);
- } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
- | InvocationTargetException e) {
- plugin.getLogger().severe("Failed to retrieve plugin file: " + e.toString());
- return null;
- }
- }
-
- private void loadCommand(String classPath, PluginDescriptionFile pluginYml) {
- Class> commandClass;
- try {
- commandClass = Class.forName(classPath);
- } catch (ClassNotFoundException e) {
- plugin.getLogger().warning("Attempted to load command " + classPath + ", but it could not be found");
- return;
- }
- StandaloneCommand command;
- try {
- command = (StandaloneCommand) commandClass.getConstructor().newInstance();
- } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException | SecurityException e) {
- plugin.getLogger().warning("Error occured when loading command " + classPath + ": " + e.toString());
- return;
- }
- Object commandSection = pluginYml.getCommands().get(command.getIdentifier().toLowerCase());
- if (commandSection == null) {
- plugin.getLogger().warning("No command with the identifier " + command.getIdentifier()
- + " could be found in the plugin.yml. Command will be unavailable");
- return;
- }
- @SuppressWarnings("unchecked")
- Map commandMap = (Map) commandSection;
- command.setRateLimiter(
- parseRateLimiter(command.getIdentifier() + "-ratelimit", commandMap.get("rate-limiter")));
- command.setTabCompletionRateLimiter(
- parseRateLimiter(command.getIdentifier() + "-tabratelimit", commandMap.get("tab-rate-limiter")));
- Boolean playerOnly = attemptBoolean(commandMap.get("player-only"));
- if (playerOnly != null) {
- command.setSenderMustBePlayer(playerOnly);
- }
- Boolean consoleOnly = attemptBoolean(commandMap.get("console-only"));
- if (consoleOnly != null) {
- command.setSenderMustBeConsole(consoleOnly);
- if (consoleOnly && playerOnly != null && playerOnly) {
- plugin.getLogger().severe("Command " + command.getIdentifier()
- + " is simultaneously console only and player only. It can not be run");
- }
- }
- Integer minArgs = attemptInteger(commandMap.get("min-args"));
- if (minArgs != null) {
- command.setMinArgs(minArgs);
- }
- Integer maxArgs = attemptInteger(commandMap.get("max-args"));
- if (maxArgs != null) {
- command.setMaxArgs(maxArgs);
- }
- this.commands.put(command.getIdentifier().toLowerCase(), command);
- }
-
- private RateLimiter parseRateLimiter(String name, Object o) {
- if (o == null) {
- return null;
- }
- if (o instanceof Map) {
- @SuppressWarnings("unchecked")
- Map map = (Map) o;
- if (!(map.containsKey("capacity") && map.containsKey("amount") && map.containsKey("interval"))) {
- plugin.getLogger().severe("Incomplete rate limiting configuration for command " + name);
- return null;
- }
- Integer capacity = attemptInteger(map.get("capacity"));
- if (capacity == null) {
- plugin.getLogger().severe("No capacity provided for rate limiting configuration for " + name);
- return null;
- }
- Integer refillAmount = attemptInteger(map.get("amount"));
- if (refillAmount == null) {
- plugin.getLogger().severe("No amount provided for rate limiting configuration for " + name);
- return null;
- }
- long interval = ConfigParsing.parseTime(String.valueOf(map.get("interval")));
- if (interval == 0) {
- plugin.getLogger().severe("No interval provided for rate limiting configuration for " + name);
- return null;
- }
- // multiply by 50 to convert ticks into ms
- return RateLimiting.createRateLimiter("command-" + name, capacity, capacity, refillAmount, interval * 50);
- }
- return null;
- }
-
- public void reset() {
- this.commands.clear();
- }
-
- // if only java generics allowed instanceof...
-
- private Integer attemptInteger(Object o) {
- if (o instanceof Integer) {
- return (Integer) o;
- }
- return null;
- }
-
- private Boolean attemptBoolean(Object o) {
- if (o instanceof Boolean) {
- return (Boolean) o;
- }
- return null;
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/command/Trie.java b/src/main/java/vg/civcraft/mc/civmodcore/command/Trie.java
deleted file mode 100644
index 8e2770e6..00000000
--- a/src/main/java/vg/civcraft/mc/civmodcore/command/Trie.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package vg.civcraft.mc.civmodcore.command;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.commons.lang3.StringUtils;
-
-public final class Trie {
-
- private Map children;
- private String word;
- // instead of storing only the suffix and re-concatenating the original word for
- // every lookup we always store the full word and depth as a relative offset
- private int depth;
- //whether this non-leaf also represents the end of a word
- private boolean isEnd;
-
- public static Trie getNewTrie() {
- Trie trie = new Trie("", 0);
- trie.children = new HashMap<>();
- return trie;
- }
-
- private Trie(String word, int depth) {
- this.word = word;
- this.depth = depth;
- this.isEnd = false;
- }
-
- public boolean isLeaf() {
- return children == null;
- }
-
- public void insert(String wordToInsert) {
- if (isLeaf()) {
- if (word.equals(wordToInsert)) {
- return;
- }
- children = new HashMap<>();
- // insert current suffix
- if (word.length() > depth) {
- children.put(word.charAt(depth), new Trie(word, depth + 1));
- this.word = word.substring(0, depth);
- }
- else {
- isEnd = true;
- }
- }
- Trie targetNode = children.computeIfAbsent(wordToInsert.charAt(depth), c -> new Trie(wordToInsert, depth + 1));
- targetNode.insert(wordToInsert);
- }
-
- public List match(String prefix) {
- List result = new ArrayList<>();
- matchWord(prefix, result);
- return result;
- }
-
- public List complete(String [] args) {
- String full = String.join(" ", args);
- List matches = match(full);
- if (args.length < 2) {
- return matches;
- }
- int elementsToRemove = args.length - 1;
- for (int i = 0; i < args.length; i++) {
- String mod = matches.get(i);
- int startingSpot = StringUtils.ordinalIndexOf(mod, " ", elementsToRemove) + 1;
- matches.set(i, mod.substring(startingSpot));
- }
- return matches;
- }
-
- private void matchWord(String wordToMatch, List result) {
- if (isLeaf()) {
- if (wordToMatch.length() <= this.word.length()) {
- result.add(word);
- return;
- }
- if (wordToMatch.length() > this.word.length()) {
- //we can not be a prefix if we are shorter
- return;
- }
- for(int i = depth; i < this.word.length(); i++) {
- if (wordToMatch.charAt(i) != this.word.charAt(i)) {
- return;
- }
- }
- result.add(this.word);
- } else {
- if (isEnd) {
- result.add(this.word);
- }
- if (wordToMatch.length() <= depth) {
- //valid prefix from here on and deeper, so deep search and add everything below
- for(Trie subTrie : children.values()) {
- subTrie.matchWord(wordToMatch, result);
- }
- return;
- }
- Trie deeperNode = children.get(wordToMatch.charAt(depth));
- if (deeperNode != null) {
- deeperNode.matchWord(wordToMatch, result);
- }
- }
- }
-
-}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/commands/CommandManager.java b/src/main/java/vg/civcraft/mc/civmodcore/commands/CommandManager.java
new file mode 100644
index 00000000..bb204017
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/commands/CommandManager.java
@@ -0,0 +1,218 @@
+package vg.civcraft.mc.civmodcore.commands;
+
+import co.aikar.commands.BaseCommand;
+import co.aikar.commands.BukkitCommandCompletionContext;
+import co.aikar.commands.BukkitCommandExecutionContext;
+import co.aikar.commands.BukkitCommandManager;
+import co.aikar.commands.CommandCompletions;
+import co.aikar.commands.CommandCompletions.CommandCompletionHandler;
+import co.aikar.commands.CommandContexts;
+import com.google.common.base.Strings;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.logging.Level;
+import javax.annotation.Nonnull;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.plugin.Plugin;
+import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils;
+import vg.civcraft.mc.civmodcore.utilities.CivLogger;
+
+/**
+ * Command registration class wrapper around {@link BukkitCommandManager}.
+ */
+public class CommandManager extends BukkitCommandManager {
+
+ private final CivLogger logger;
+
+ /**
+ * Creates a new command manager for Aikar based commands and tab completions.
+ *
+ * @param plugin The plugin to bind this manager to.
+ */
+ public CommandManager(@Nonnull final Plugin plugin) {
+ super(Objects.requireNonNull(plugin));
+ this.logger = CivLogger.getLogger(plugin.getClass(), getClass());
+ }
+
+ /**
+ * Will initialise the manager and register both commands and completions. You should only really use this if
+ * you've used {@link CommandManager#reset()} or both {@link #unregisterCommands()} and
+ * {@link #unregisterCompletions()}, otherwise there may be issues.
+ */
+ public final void init() {
+ registerCommands();
+ registerCompletions(getCommandCompletions());
+ registerContexts(getCommandContexts());
+ }
+
+ /**
+ * This is called as part of {@link CommandManager#init()} and should be overridden by an extending class to
+ * register all (or as many) commands at once.
+ */
+ public void registerCommands() { }
+
+ /**
+ * This is called as part of {@link CommandManager#init()} and should be overridden by an extending class to
+ * register all (or as many) completions at once, though make sure to call super.
+ *
+ * @param completions The completion manager is given. It is the same manager that can be reached via
+ * {@link #getCommandCompletions()}.
+ */
+ public void registerCompletions(@Nonnull final CommandCompletions completions) {
+ completions.registerCompletion("none", (context) -> Collections.emptyList());
+ completions.registerAsyncCompletion("allplayers", (context) ->
+ Arrays.stream(Bukkit.getOfflinePlayers())
+ .map(OfflinePlayer::getName)
+ .toList());
+ completions.registerAsyncCompletion("materials", (context) ->
+ Arrays.stream(Material.values())
+ .map(Enum::name)
+ .toList());
+ completions.registerAsyncCompletion("itemMaterials", (context) ->
+ Arrays.stream(Material.values())
+ .filter(ItemUtils::isValidItemMaterial)
+ .map(Enum::name)
+ .toList());
+ }
+
+ /**
+ * This is called as part of {@link CommandManager#init()} and should be overridden by an extending class
+ * to register all (or as many) contexts at once.
+ *
+ * @param contexts The context manager is given. It is the same manager that can be reached via
+ * {@link #getCommandContexts()}.
+ */
+ public void registerContexts(@Nonnull final CommandContexts contexts) { }
+
+ /**
+ * Registers a new command and any attached tab completions.
+ *
+ * @param command The command instance to register.
+ * @param forceReplace Whether to force replace any existing command.
+ */
+ @Override
+ public final void registerCommand(@Nonnull final BaseCommand command, final boolean forceReplace) {
+ super.registerCommand(Objects.requireNonNull(command), forceReplace);
+ this.logger.info("Command [" + command.getClass().getSimpleName() + "] registered.");
+ getTabCompletions(command.getClass()).forEach((method, annotation) -> {
+ if (annotation.async()) {
+ getCommandCompletions().registerAsyncCompletion(annotation.value(), (context) ->
+ runCommandCompletion(context, command, annotation.value(), method));
+ }
+ else {
+ getCommandCompletions().registerCompletion(annotation.value(), (context) ->
+ runCommandCompletion(context, command, annotation.value(), method));
+ }
+ this.logger.info("Command Completer [" + annotation.value() + "] registered.");
+ });
+ }
+
+ /**
+ * Deregisters a command and any attached tab completions.
+ *
+ * @param command The command instance to register.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public final void unregisterCommand(@Nonnull final BaseCommand command) {
+ super.unregisterCommand(Objects.requireNonNull(command));
+ this.logger.info("Command [" + command.getClass().getSimpleName() + "] unregistered.");
+ final Map> internal;
+ try {
+ internal = (HashMap>)
+ FieldUtils.readField(getCommandCompletions(), "completionMap", true);
+ }
+ catch (final Throwable exception) {
+ throw new UnsupportedOperationException("Could not get internal completion map.", exception);
+ }
+ for (final TabComplete complete : getTabCompletions(command.getClass()).values()) {
+ internal.remove(complete.value().toLowerCase(Locale.ENGLISH));
+ this.logger.info("Command Completer [" + complete.value() + "] unregistered.");
+ }
+ }
+
+ /**
+ * Resets all command completions.
+ */
+ public final void unregisterCompletions() {
+ this.completions = null;
+ }
+
+ /**
+ * Resets the manager, resetting all commands and completions.
+ */
+ public final void reset() {
+ unregisterCommands();
+ unregisterCompletions();
+ }
+
+ // ------------------------------------------------------------
+ // Tab Completions
+ // ------------------------------------------------------------
+
+ @SuppressWarnings("unchecked")
+ private List runCommandCompletion(final BukkitCommandCompletionContext context,
+ final BaseCommand command,
+ final String id,
+ final Method method) {
+ try {
+ method.setAccessible(true);
+ return switch (method.getParameterCount()) {
+ case 0 -> (List) method.invoke(command);
+ case 1 -> (List) method.invoke(command, context);
+ default -> throw new UnsupportedOperationException("Unsupported number of parameters.");
+ };
+ }
+ catch (final Throwable exception) {
+ this.logger.log(Level.WARNING,
+ "Could not tab complete [@" + id + "]: an error with the handler!", exception);
+ return Collections.emptyList();
+ }
+ }
+
+ private static Map getTabCompletions(final Class extends BaseCommand> clazz) {
+ final var completions = new HashMap();
+ if (clazz == null) {
+ return completions;
+ }
+ for (final Method method : clazz.getDeclaredMethods()) {
+ if (!Modifier.isPublic(method.getModifiers())) {
+ continue;
+ }
+ if (!List.class.isAssignableFrom(method.getReturnType())) {
+ continue;
+ }
+ // TODO add a generic type check here when possible
+ switch (method.getParameterCount()) {
+ case 0:
+ break;
+ case 1:
+ if (BukkitCommandCompletionContext.class.isAssignableFrom(method.getParameterTypes()[0])) {
+ break;
+ }
+ default:
+ continue;
+ }
+ final TabComplete tabComplete = method.getAnnotation(TabComplete.class);
+ if (tabComplete == null) {
+ continue;
+ }
+ if (Strings.isNullOrEmpty(tabComplete.value())) {
+ continue;
+ }
+ completions.put(method, tabComplete);
+ }
+ return completions;
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/commands/NamedCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/commands/NamedCommand.java
new file mode 100644
index 00000000..4b64c1dd
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/commands/NamedCommand.java
@@ -0,0 +1,17 @@
+package vg.civcraft.mc.civmodcore.commands;
+
+import co.aikar.commands.BaseCommand;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * This class should be used when you can't use {@link co.aikar.commands.annotation.CommandAlias}.
+ */
+public abstract class NamedCommand extends BaseCommand {
+
+ @SuppressWarnings("deprecation")
+ public NamedCommand(@Nonnull final String command) {
+ super(Objects.requireNonNull(command));
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/commands/TabComplete.java b/src/main/java/vg/civcraft/mc/civmodcore/commands/TabComplete.java
new file mode 100644
index 00000000..3568f0a2
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/commands/TabComplete.java
@@ -0,0 +1,24 @@
+package vg.civcraft.mc.civmodcore.commands;
+
+import co.aikar.commands.BaseCommand;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation, when used on a method on an {@link BaseCommand} extending class will be automatically
+ * registered as a tab completion.
+ *
+ * The return type of the method MUST, MUST be {@code List}, otherwise it will fail.
+ *
+ * The method is permitted to have 0-1 parameters, but that parameter MUST, MUST be of type
+ * {@code BukkitCommandCompletionContext}, otherwise it will fail.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface TabComplete {
+ String value();
+
+ boolean async() default false;
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/util/ConfigParsing.java b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java
similarity index 56%
rename from src/main/java/vg/civcraft/mc/civmodcore/util/ConfigParsing.java
rename to src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java
index 5ee5e53b..9f26a833 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/util/ConfigParsing.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java
@@ -1,13 +1,17 @@
-package vg.civcraft.mc.civmodcore.util;
+package vg.civcraft.mc.civmodcore.config;
import com.google.common.collect.Lists;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Logger;
-import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
@@ -26,29 +30,109 @@
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
-import vg.civcraft.mc.civmodcore.areas.EllipseArea;
-import vg.civcraft.mc.civmodcore.areas.GlobalYLimitedArea;
-import vg.civcraft.mc.civmodcore.areas.IArea;
-import vg.civcraft.mc.civmodcore.areas.RectangleArea;
-import vg.civcraft.mc.civmodcore.itemHandling.ItemMap;
+import vg.civcraft.mc.civmodcore.inventory.items.EnchantUtils;
+import vg.civcraft.mc.civmodcore.inventory.items.ItemMap;
+import vg.civcraft.mc.civmodcore.inventory.items.MaterialUtils;
+import vg.civcraft.mc.civmodcore.world.model.EllipseArea;
+import vg.civcraft.mc.civmodcore.world.model.GlobalYLimitedArea;
+import vg.civcraft.mc.civmodcore.world.model.IArea;
+import vg.civcraft.mc.civmodcore.world.model.RectangleArea;
-public class ConfigParsing {
+@UtilityClass
+public final class ConfigHelper {
- private static final Logger log = Bukkit.getLogger();
+ private static final Logger LOGGER = Bukkit.getLogger();
/**
- * Creates an itemmap containing all the items listed in the given config
+ * Retrieves the configuration section at the given key on the given configuration section.
+ *
+ * @param config The config to get the keyed section from.
+ * @param key The key of the section to retrieve.
+ * @return Returns the configuration section at the given key, or returns a new, empty section.
+ */
+ @Nonnull
+ public static ConfigurationSection getSection(@NonNull final ConfigurationSection config,
+ @NonNull final String key) {
+ ConfigurationSection found = config.getConfigurationSection(key);
+ if (found == null) {
+ found = config.createSection(key);
+ }
+ return found;
+ }
+
+ /**
+ * Retrieves a string list from a given config section. If the keyed value is a standalone string instead of a
+ * list, that value will be converted to a list.
+ *
+ * @param config The config section to retrieve the list from.
+ * @param key The key to get the list of.
+ * @return Returns a list of strings, which is never null.
+ */
+ @Nonnull
+ public static List getStringList(@NonNull final ConfigurationSection config,
+ @NonNull final String key) {
+ if (config.isString(key)) {
+ final var list = new ArrayList(1);
+ list.add(config.getString(key));
+ return list;
+ }
+ return config.getStringList(key);
+ }
+
+ /**
+ * Attempts to retrieve a list from a config section.
+ *
+ * @param The type to parse the list into.
+ * @param config The config section.
+ * @param key The key of the list.
+ * @param parser The parser to convert the string value into the correct type.
+ * @return Returns a list, or null.
+ */
+ @Nonnull
+ public static List parseList(@NonNull final ConfigurationSection config,
+ @NonNull final String key,
+ @NonNull final Function parser) {
+ if (!config.isList(key)) {
+ return new ArrayList<>(0);
+ }
+ final var entries = getStringList(config, key);
+ final var result = new ArrayList(entries.size());
+ for (final String entry : entries) {
+ final T item = parser.apply(entry);
+ if (item != null) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to retrieve a list of materials from a config section.
+ *
+ * @param config The config section.
+ * @param key The key of the list.
+ * @return Returns a list of materials, or null.
+ */
+ @Nonnull
+ public static List parseMaterialList(@Nonnull final ConfigurationSection config,
+ @Nonnull final String key) {
+ return parseList(config, key, MaterialUtils::getMaterial);
+ }
+
+ /**
+ * Creates an item map containing all the items listed in the given config
* section
*
* @param config ConfigurationSection to parse the items from
* @return The item map created
*/
- public static ItemMap parseItemMap(ConfigurationSection config) {
- ItemMap result = new ItemMap();
+ @Nonnull
+ public static ItemMap parseItemMap(@Nullable final ConfigurationSection config) {
+ final var result = new ItemMap();
if (config == null) {
return result;
}
- for (String key : config.getKeys(false)) {
+ for (final String key : config.getKeys(false)) {
ConfigurationSection current = config.getConfigurationSection(key);
ItemMap partMap = parseItemMapDirectly(current);
result.merge(partMap);
@@ -56,7 +140,8 @@ public static ItemMap parseItemMap(ConfigurationSection config) {
return result;
}
- public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
+ @Nonnull
+ public static ItemMap parseItemMapDirectly(@Nullable final ConfigurationSection current) {
ItemMap im = new ItemMap();
if (current == null) {
return im;
@@ -68,17 +153,17 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
m = null;
} finally {
if (m == null) {
- log.severe("Failed to find material " + current.getString("material") + " in section " + current.getCurrentPath());
+ LOGGER.severe("Failed to find material " + current.getString("material") + " in section " + current.getCurrentPath());
return im;
}
}
ItemStack toAdd = new ItemStack(m);
if (current.isInt("durability")) {
- log.warning("Item durability as specified at " + current.getCurrentPath() + " is no longer supported");
+ LOGGER.warning("Item durability as specified at " + current.getCurrentPath() + " is no longer supported");
}
ItemMeta meta = toAdd.getItemMeta();
if (meta == null) {
- log.severe("No item meta found for" + current.getCurrentPath());
+ LOGGER.severe("No item meta found for" + current.getCurrentPath());
} else {
String name = current.getString("name");
if (name != null) {
@@ -101,7 +186,7 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
ConfigurationSection enchantConfig = current.getConfigurationSection("enchants")
.getConfigurationSection(enchantKey);
if (!enchantConfig.isString("enchant")) {
- log.warning("No enchant specified for enchantment entry at " + enchantConfig.getCurrentPath()
+ LOGGER.warning("No enchant specified for enchantment entry at " + enchantConfig.getCurrentPath()
+ ". Entry was ignored");
continue;
}
@@ -109,7 +194,7 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
enchant = Enchantment
.getByKey(NamespacedKey.minecraft((enchantConfig.getString("enchant").toLowerCase())));
if (enchant == null) {
- log.severe("Failed to parse enchantment " + enchantConfig.getString("enchant")
+ LOGGER.severe("Failed to parse enchantment " + enchantConfig.getString("enchant")
+ ", the entry was ignored");
continue;
}
@@ -147,14 +232,14 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
ConfigurationSection currentStoredEnchantSection = storedEnchantSection
.getConfigurationSection(sEKey);
if (currentStoredEnchantSection != null) {
- Enchantment enchant = parseEnchantment(currentStoredEnchantSection, "enchant");
+ Enchantment enchant = EnchantUtils.getEnchantment(currentStoredEnchantSection.getString("enchant"));
int level = currentStoredEnchantSection.getInt("level", 1);
if (enchant != null) {
enchantMeta.addStoredEnchant(enchant, level, true);
}
else {
- log.severe("Failed to parse enchantment at " + currentStoredEnchantSection.getCurrentPath()
- + ", it was not applied");
+ LOGGER.severe("Failed to parse enchantment at " + currentStoredEnchantSection.getCurrentPath()
+ + ", it was not applied");
}
}
}
@@ -168,7 +253,7 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
try {
potType = PotionType.valueOf(potion.getString("type", "AWKWARD"));
} catch (IllegalArgumentException e) {
- log.warning("Expected potion type at " + potion.getCurrentPath() + ", but "
+ LOGGER.warning("Expected potion type at " + potion.getCurrentPath() + ", but "
+ potion.getString("type") + " is not a valid potion type");
potType = PotionType.AWKWARD;
}
@@ -199,12 +284,13 @@ public static ItemMap parseItemMapDirectly(ConfigurationSection current) {
im.addItemStack(toAdd);
return im;
}
-
- public static int parseTimeAsTicks(String arg) {
+
+ public static int parseTimeAsTicks(@Nonnull final String arg) {
return (int) (parseTime(arg, TimeUnit.MILLISECONDS) / 50L);
}
- public static long parseTime(String arg, TimeUnit unit) {
+ public static long parseTime(@Nonnull final String arg,
+ @Nonnull final TimeUnit unit) {
long millis = parseTime(arg);
return unit.convert(millis, TimeUnit.MILLISECONDS);
}
@@ -224,7 +310,7 @@ public static long parseTime(String arg, TimeUnit unit) {
* @param input Parsed string containing the time format
* @return How many milliseconds the given time value is
*/
- public static long parseTime(String input) {
+ public static long parseTime(@Nonnull String input) {
input = input.replace(" ", "").replace(",", "").toLowerCase();
long result = 0;
try {
@@ -244,63 +330,65 @@ public static long parseTime(String input) {
duration = Long.parseLong(numberSuffix);
}
switch (typeSuffix) {
- case "ms":
- case "milli":
- case "millis":
- result += duration;
- break;
- case "s": // seconds
- case "sec":
- case "second":
- case "seconds":
- result += TimeUnit.SECONDS.toMillis(duration);
- break;
- case "m": // minutes
- case "min":
- case "minute":
- case "minutes":
- result += TimeUnit.MINUTES.toMillis(duration);
- break;
- case "h": // hours
- case "hour":
- case "hours":
- result += TimeUnit.HOURS.toMillis(duration);
- break;
- case "d": // days
- case "day":
- case "days":
- result += TimeUnit.DAYS.toMillis(duration);
- break;
- case "w": // weeks
- case "week":
- case "weeks":
- result += TimeUnit.DAYS.toMillis(duration * 7);
- break;
- case "month": // weeks
- case "months":
- result += TimeUnit.DAYS.toMillis(duration * 30);
- break;
- case "y":
- case "year":
- case "years":
- result += TimeUnit.DAYS.toMillis(duration * 365);
- break;
- case "never":
- case "inf":
- case "infinite":
- case "perm":
- case "perma":
- case "forever":
- // 1000 years counts as perma
- result += TimeUnit.DAYS.toMillis(365 * 1000);
- default:
- // just ignore it
+ case "ms":
+ case "milli":
+ case "millis":
+ result += duration;
+ break;
+ case "s": // seconds
+ case "sec":
+ case "second":
+ case "seconds":
+ result += TimeUnit.SECONDS.toMillis(duration);
+ break;
+ case "m": // minutes
+ case "min":
+ case "minute":
+ case "minutes":
+ result += TimeUnit.MINUTES.toMillis(duration);
+ break;
+ case "h": // hours
+ case "hour":
+ case "hours":
+ result += TimeUnit.HOURS.toMillis(duration);
+ break;
+ case "d": // days
+ case "day":
+ case "days":
+ result += TimeUnit.DAYS.toMillis(duration);
+ break;
+ case "w": // weeks
+ case "week":
+ case "weeks":
+ result += TimeUnit.DAYS.toMillis(duration * 7);
+ break;
+ case "month": // weeks
+ case "months":
+ result += TimeUnit.DAYS.toMillis(duration * 30);
+ break;
+ case "y":
+ case "year":
+ case "years":
+ result += TimeUnit.DAYS.toMillis(duration * 365);
+ break;
+ case "never":
+ case "inf":
+ case "infinite":
+ case "perm":
+ case "perma":
+ case "forever":
+ // 1000 years counts as perma
+ result += TimeUnit.DAYS.toMillis(365 * 1000);
+ default:
+ // just ignore it
}
}
return result;
}
- private static String getSuffix(String arg, Predicate selector) {
+ @Nonnull
+ private static String getSuffix(@Nonnull final String arg,
+ @Nonnull final Predicate selector) {
StringBuilder number = new StringBuilder();
for (int i = arg.length() - 1; i >= 0; i--) {
if (selector.test(arg.charAt(i))) {
@@ -318,20 +406,21 @@ private static String getSuffix(String arg, Predicate selector) {
* @param configurationSection ConfigurationSection to parse the effect from
* @return The potion effect parsed
*/
- public static List parsePotionEffects(ConfigurationSection configurationSection) {
+ @Nonnull
+ public static List parsePotionEffects(@Nullable final ConfigurationSection configurationSection) {
List potionEffects = Lists.newArrayList();
if (configurationSection != null) {
for (String name : configurationSection.getKeys(false)) {
ConfigurationSection configEffect = configurationSection.getConfigurationSection(name);
String type = configEffect.getString("type");
if (type == null) {
- log.severe("Expected potion type to be specified, but found no \"type\" option at "
+ LOGGER.severe("Expected potion type to be specified, but found no \"type\" option at "
+ configEffect.getCurrentPath());
continue;
}
PotionEffectType effect = PotionEffectType.getByName(type);
if (effect == null) {
- log.severe("Expected potion type to be specified at " + configEffect.getCurrentPath()
+ LOGGER.severe("Expected potion type to be specified at " + configEffect.getCurrentPath()
+ " but found " + type + " which is no valid type");
}
int duration = configEffect.getInt("duration", 200);
@@ -342,26 +431,27 @@ public static List parsePotionEffects(ConfigurationSection configu
return potionEffects;
}
- public static IArea parseArea(ConfigurationSection config) {
+ @Nullable
+ public static IArea parseArea(@Nullable final ConfigurationSection config) {
if (config == null) {
- log.warning("Tried to parse area on null section");
+ LOGGER.warning("Tried to parse area on null section");
return null;
}
String type = config.getString("type");
if (type == null) {
- log.warning("Found no area type at " + config.getCurrentPath());
+ LOGGER.warning("Found no area type at " + config.getCurrentPath());
return null;
}
int lowerYBound = config.getInt("lowerYBound", 0);
int upperYBound = config.getInt("upperYBound", 255);
String worldName = config.getString("world");
if (worldName == null) {
- log.warning("Found no world specified for area at " + config.getCurrentPath());
+ LOGGER.warning("Found no world specified for area at " + config.getCurrentPath());
return null;
}
World world = Bukkit.getWorld(worldName);
if (world == null) {
- log.warning("Found no world with name " + worldName + " as specified at " + config.getCurrentPath());
+ LOGGER.warning("Found no world with name " + worldName + " as specified at " + config.getCurrentPath());
return null;
}
Location center = null;
@@ -378,48 +468,48 @@ public static IArea parseArea(ConfigurationSection config) {
int zSize = config.getInt("zSize", -1);
IArea area = null;
switch (type) {
- case "GLOBAL":
- area = new GlobalYLimitedArea(lowerYBound, upperYBound, world);
- break;
- case "ELLIPSE":
- if (center == null) {
- log.warning("Found no center for area at " + config.getCurrentPath());
- return null;
- }
- if (xSize == -1) {
- log.warning("Found no xSize for area at " + config.getCurrentPath());
- return null;
- }
- if (zSize == -1) {
- log.warning("Found no zSize for area at " + config.getCurrentPath());
- return null;
- }
- area = new EllipseArea(lowerYBound, upperYBound, center, xSize, zSize);
- break;
- case "RECTANGLE":
- if (center == null) {
- log.warning("Found no center for area at " + config.getCurrentPath());
- return null;
- }
- if (xSize == -1) {
- log.warning("Found no xSize for area at " + config.getCurrentPath());
- return null;
- }
- if (zSize == -1) {
- log.warning("Found no zSize for area at " + config.getCurrentPath());
- return null;
- }
- area = new RectangleArea(lowerYBound, upperYBound, center, xSize, zSize);
- break;
- default:
- log.warning("Invalid area type " + type + " at " + config.getCurrentPath());
+ case "GLOBAL":
+ area = new GlobalYLimitedArea(lowerYBound, upperYBound, world);
+ break;
+ case "ELLIPSE":
+ if (center == null) {
+ LOGGER.warning("Found no center for area at " + config.getCurrentPath());
+ return null;
+ }
+ if (xSize == -1) {
+ LOGGER.warning("Found no xSize for area at " + config.getCurrentPath());
+ return null;
+ }
+ if (zSize == -1) {
+ LOGGER.warning("Found no zSize for area at " + config.getCurrentPath());
+ return null;
+ }
+ area = new EllipseArea(lowerYBound, upperYBound, center, xSize, zSize);
+ break;
+ case "RECTANGLE":
+ if (center == null) {
+ LOGGER.warning("Found no center for area at " + config.getCurrentPath());
+ return null;
+ }
+ if (xSize == -1) {
+ LOGGER.warning("Found no xSize for area at " + config.getCurrentPath());
+ return null;
+ }
+ if (zSize == -1) {
+ LOGGER.warning("Found no zSize for area at " + config.getCurrentPath());
+ return null;
+ }
+ area = new RectangleArea(lowerYBound, upperYBound, center, xSize, zSize);
+ break;
+ default:
+ LOGGER.warning("Invalid area type " + type + " at " + config.getCurrentPath());
}
return area;
}
/**
* Parses a section which contains key-value mappings of a type to another type
- *
+ *
* @param Key type
* @param Value type
* @param parent Configuration section containing the section with the values
@@ -429,8 +519,12 @@ public static IArea parseArea(ConfigurationSection config) {
* @param valueConverter Converts strings to type V
* @param mapToUse The map to place parsed keys and values.
*/
- public static void parseKeyValueMap(ConfigurationSection parent, String identifier, Logger logger,
- Function keyConverter, Function valueConverter, Map mapToUse) {
+ public static void parseKeyValueMap(@Nonnull final ConfigurationSection parent,
+ @Nonnull final String identifier,
+ @Nonnull final Logger logger,
+ @Nonnull final Function keyConverter,
+ @Nonnull final Function valueConverter,
+ @Nonnull final Map mapToUse) {
if (!parent.isConfigurationSection(identifier)) {
return;
}
@@ -453,20 +547,15 @@ public static void parseKeyValueMap(ConfigurationSection parent, String i
mapToUse.put(keyinstance, value);
}
}
-
- public static Enchantment parseEnchantment(ConfigurationSection config, String key) {
- if (!config.isString(key)) {
- return null;
- }
- String val = config.getString(key);
- if (!Pattern.matches("[a-z0-9/._-]+", val)) {
- return null;
- }
- NamespacedKey nsKey = NamespacedKey.minecraft(val);
- if (nsKey == null) {
- return null;
- }
- return Enchantment.getByKey(nsKey);
+
+ /**
+ * @deprecated Use {@link EnchantUtils#getEnchantment(String)} instead.
+ */
+ @Nullable
+ @Deprecated(forRemoval = true)
+ public static Enchantment parseEnchantment(@Nonnull final ConfigurationSection config,
+ @Nonnull final String key) {
+ return EnchantUtils.getEnchantment(config.getString(key));
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java
new file mode 100644
index 00000000..901f7f07
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigParser.java
@@ -0,0 +1,82 @@
+package vg.civcraft.mc.civmodcore.config;
+
+import java.util.logging.Logger;
+import org.bukkit.ChatColor;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.Plugin;
+
+/**
+ * This is a config parsing class intended to make handling configs a little easier, and automatically parse commonly
+ * seen things within civ configs.
+ */
+public abstract class ConfigParser {
+
+ protected final Plugin plugin;
+ protected final Logger logger;
+
+ private boolean debug;
+ private boolean logReplies;
+
+ public ConfigParser(final Plugin plugin) {
+ this.plugin = plugin;
+ this.logger = plugin.getLogger();
+ }
+
+ /**
+ * Parses this civ plugin's config. It will also save a default config (if one is not already present) and reload
+ * the config, so there's no need to do so yourself beforehand.
+ *
+ * @return Returns true if the config was successfully parsed.
+ */
+ public final boolean parse() {
+ this.logger.info(ChatColor.BLUE + "Parsing config.");
+ this.plugin.saveDefaultConfig();
+ FileConfiguration config = this.plugin.getConfig();
+ // Parse debug value
+ this.debug = config.getBoolean("debug", false);
+ this.logger.info("Debug mode: " + (this.debug ? "enabled" : "disabled"));
+ // Parse reply logging value
+ this.logReplies = config.getBoolean("logReplies", false);
+ this.logger.info("Logging replies: " + (this.logReplies ? "enabled" : "disabled"));
+ // Allow child class parsing
+ final boolean worked = parseInternal(config);
+ if (worked) {
+ this.logger.info(ChatColor.BLUE + "Config parsed.");
+ }
+ else {
+ this.logger.warning("Failed to parse config!");
+ }
+ return worked;
+ }
+
+ /**
+ * An internal parser method intended to be overridden by child classes.
+ *
+ * @param config The root config section.
+ * @return Return true if the
+ */
+ protected abstract boolean parseInternal(final ConfigurationSection config);
+
+ /**
+ * This should reset all config values back to their defaults. Child classes should override this if they parse
+ * additional values that should be reset.
+ */
+ public void reset() {
+ this.debug = false;
+ this.logReplies = false;
+ }
+
+ // ------------------------------------------------------------ //
+ // Getters
+ // ------------------------------------------------------------ //
+
+ public final boolean isDebugEnabled() {
+ return this.debug;
+ }
+
+ public final boolean logReplies() {
+ return this.logReplies;
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java
new file mode 100644
index 00000000..6e0053c3
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigSection.java
@@ -0,0 +1,24 @@
+package vg.civcraft.mc.civmodcore.config;
+
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.bukkit.configuration.MemoryConfiguration;
+import org.bukkit.configuration.serialization.ConfigurationSerializable;
+
+/**
+ * Class is intended to be used by implementations of {@link ConfigurationSerializable} during the deserialization
+ * process.
+ */
+public class ConfigSection extends MemoryConfiguration {
+
+ /**
+ * @param data The data to create a new ConfigSection from.
+ * @return Returns a new ConfigSection.
+ */
+ public static ConfigSection fromData(@Nonnull final Map data) {
+ final var section = new ConfigSection();
+ section.map.putAll(data);
+ return section;
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/dao/ConnectionPool.java b/src/main/java/vg/civcraft/mc/civmodcore/dao/ConnectionPool.java
index e163d494..6b9098ba 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/dao/ConnectionPool.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/dao/ConnectionPool.java
@@ -1,13 +1,15 @@
package vg.civcraft.mc.civmodcore.dao;
-import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
+import java.util.Objects;
import java.util.logging.Logger;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.bukkit.Bukkit;
/**
@@ -20,7 +22,6 @@ public class ConnectionPool {
private static final Logger LOGGER = Bukkit.getLogger();
private final DatabaseCredentials credentials;
-
private HikariDataSource datasource;
/**
@@ -29,23 +30,23 @@ public class ConnectionPool {
*
* @param credentials The credentials to connect with.
*/
- public ConnectionPool(DatabaseCredentials credentials) {
- Preconditions.checkArgument(credentials != null,
+ public ConnectionPool(@Nonnull final DatabaseCredentials credentials) {
+ this.credentials = Objects.requireNonNull(credentials,
"Cannot create a ConnectionPool with a null set of credentials.");
- this.credentials = credentials;
HikariConfig config = new HikariConfig();
- config.setJdbcUrl("jdbc:" + credentials.getDriver() + "://" + credentials.getHostname() + ":" +
- credentials.getPort() + "/" + credentials.getDatabase());
- config.setConnectionTimeout(credentials.getConnectionTimeout());
- config.setIdleTimeout(credentials.getIdleTimeout());
- config.setMaxLifetime(credentials.getMaxLifetime());
- config.setMaximumPoolSize(credentials.getPoolSize());
- config.setUsername(credentials.getUsername());
- if (!Strings.isNullOrEmpty(credentials.getPassword())) {
- config.setPassword(credentials.getPassword());
+ config.setJdbcUrl("jdbc:" + credentials.driver() + "://" + credentials.host() + ":" +
+ credentials.port() + "/" + credentials.database());
+ config.setConnectionTimeout(credentials.connectionTimeout());
+ config.setIdleTimeout(credentials.idleTimeout());
+ config.setMaxLifetime(credentials.maxLifetime());
+ config.setMaximumPoolSize(credentials.poolSize());
+ config.setUsername(credentials.username());
+ if (!Strings.isNullOrEmpty(credentials.password())) {
+ config.setPassword(credentials.password());
}
this.datasource = new HikariDataSource(config);
- try (Connection connection = getConnection(); Statement statement = connection.createStatement();) {
+ try (final Connection connection = getConnection();
+ final Statement statement = connection.createStatement()) {
statement.executeQuery("SELECT 1;");
LOGGER.info("Successfully connected to the database.");
}
@@ -72,40 +73,26 @@ public ConnectionPool(DatabaseCredentials credentials) {
* @param idleTimeout The longest a connection can sit idle before recycling (10 minutes recommended, check dbms)
* @param maxLifetime The longest a connection can exist in total. (2 hours recommended, check dbms)
*/
- public ConnectionPool(String user, String pass, String host, int port, String driver, String database,
- int poolSize, long connectionTimeout, long idleTimeout, long maxLifetime) {
+ public ConnectionPool(final String user,
+ final String pass,
+ final String host,
+ final int port,
+ final String driver,
+ final String database,
+ final int poolSize,
+ final long connectionTimeout,
+ final long idleTimeout,
+ final long maxLifetime) {
this(new DatabaseCredentials(user, pass, host, port, driver, database,
poolSize, connectionTimeout, idleTimeout, maxLifetime));
}
- /**
- * Legacy support constructor to create a connection pool.
- *
- * @param logger The logger that would be logged to. This can now be null since ConnectionPool now
- * uses its own logger.
- * @param user The SQL user to connect as.
- * @param pass The SQL user's password.
- * @param host The hostname of the database.
- * @param port The port to connect via.
- * @param database The specific database to create and modify tables in.
- * @param poolSize The maximum size of the connection pool (under 10 recommended)
- * @param connectionTimeout The longest a query can run until it times out (1-5 seconds recommended)
- * @param idleTimeout The longest a connection can sit idle before recycling (10 minutes recommended, check dbms)
- * @param maxLifetime The longest a connection can exist in total. (2 hours recommended, check dbms)
- *
- * @deprecated This is deprecated as it insists on a logger and does not allow you to specify a jdbc driver.
- */
- @Deprecated
- public ConnectionPool(Logger logger, String user, String pass, String host, int port, String database,
- int poolSize, long connectionTimeout, long idleTimeout, long maxLifetime) {
- this(user, pass, host, port, "mysql", database, poolSize, connectionTimeout, idleTimeout, maxLifetime);
- }
-
/**
* Gets the credentials used for this ConnectionPool.
*
* @return Returns the credentials being used.
*/
+ @Nonnull
public DatabaseCredentials getCredentials() {
return this.credentials;
}
@@ -116,6 +103,7 @@ public DatabaseCredentials getCredentials() {
* @return A new Connection
* @throws SQLException
*/
+ @Nonnull
public Connection getConnection() throws SQLException {
available();
return this.datasource.getConnection();
@@ -148,8 +136,9 @@ public void available() throws SQLException {
*
* @return DataSource being used
*/
+ @Nullable
HikariDataSource getHikariDataSource() {
- return datasource;
+ return this.datasource;
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseCredentials.java b/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseCredentials.java
index ffb1bdc6..85707ef2 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseCredentials.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseCredentials.java
@@ -2,90 +2,39 @@
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.apache.commons.collections4.MapUtils;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.util.NumberConversions;
-import vg.civcraft.mc.civmodcore.util.MoreMapUtils;
+import vg.civcraft.mc.civmodcore.utilities.MoreMapUtils;
/**
* This is a data class representing a set of credentials used for connecting to databases.
*
* @author Protonull
*/
-public class DatabaseCredentials implements ConfigurationSerializable {
-
- private final String user;
- private final String pass;
- private final String host;
- private final int port;
- private final String driver;
- private final String database;
- private final int poolSize;
- private final long connectionTimeout;
- private final long idleTimeout;
- private final long maxLifetime;
-
- public DatabaseCredentials(String user, String pass, String host, int port, String driver, String database,
- int poolSize, long connectionTimeout, long idleTimeout, long maxLifetime) {
- this.user = user;
- this.pass = pass;
- this.host = host;
- this.port = port;
- this.driver = driver;
- this.database = database;
- this.poolSize = poolSize;
- this.connectionTimeout = connectionTimeout;
- this.idleTimeout = idleTimeout;
- this.maxLifetime = maxLifetime;
- }
-
- public final String getUsername() {
- return this.user;
- }
-
- public final String getPassword() {
- return this.pass;
- }
-
- public final String getHostname() {
- return this.host;
- }
-
- public final int getPort() {
- return this.port;
- }
-
- public final String getDriver() {
- return this.driver;
- }
-
- public final String getDatabase() {
- return this.database;
- }
-
- public final int getPoolSize() {
- return this.poolSize;
- }
-
- public final long getConnectionTimeout() {
- return this.connectionTimeout;
- }
-
- public final long getIdleTimeout() {
- return this.idleTimeout;
- }
-
- public final long getMaxLifetime() {
- return this.maxLifetime;
- }
-
+public record DatabaseCredentials(String username,
+ String password,
+ String host,
+ int port,
+ String driver,
+ String database,
+ int poolSize,
+ long connectionTimeout,
+ long idleTimeout,
+ long maxLifetime)
+ implements ConfigurationSerializable {
+
+ @Nonnull
@Override
- public final Map serialize() {
- Map data = new HashMap<>();
- data.put("username", this.user);
- data.put("password", this.pass);
+ public Map serialize() {
+ final var data = new HashMap(10);
+ data.put("username", this.username);
+ data.put("password", this.password);
data.put("hostname", this.host);
data.put("port", this.port);
+ data.put("driver", this.driver);
data.put("database", this.database);
data.put("poolSize", this.poolSize);
data.put("connectionTimeout", this.connectionTimeout);
@@ -94,26 +43,32 @@ public final Map serialize() {
return data;
}
- // This must be kept compatible with ManagedDatasource's deserialization
- public static DatabaseCredentials deserialize(Map data) {
+ @Nullable
+ public static DatabaseCredentials deserialize(@Nonnull final Map data) {
if (MapUtils.isEmpty(data)) {
return null;
}
- String user = MoreMapUtils.attemptGet(data, "root", "username", "user");
- String pass = MoreMapUtils.attemptGet(data, "", "password", "pass");
- String host = MoreMapUtils.attemptGet(data, "localhost", "hostname", "host");
- int port = MoreMapUtils.attemptGet(data, NumberConversions::toInt, 3306, "port");
- String driver = MoreMapUtils.attemptGet(data, "mysql", "driver");
- String database = MoreMapUtils.attemptGet(data, "minecraft", "database", "dbname", "db");
- int poolSize = MoreMapUtils.attemptGet(data, NumberConversions::toInt, 10, "poolSize", "poolsize");
- long connectionTimeout = MoreMapUtils.attemptGet(data, NumberConversions::toLong, 10_000L,
- "connectionTimeout", "connection_timeout");
- long idleTimeout = MoreMapUtils.attemptGet(data, NumberConversions::toLong, 600_000L,
- "idleTimeout", "idle_timeout");
- long maxLifetime = MoreMapUtils.attemptGet(data, NumberConversions::toLong, 7_200_000L,
- "maxLifetime", "max_lifetime");
- return new DatabaseCredentials(user, pass, host, port, driver, database,
- poolSize, connectionTimeout, idleTimeout, maxLifetime);
+ return new DatabaseCredentials(
+ MoreMapUtils.attemptGet(data, "root",
+ "username", "user"), // keys
+ MoreMapUtils.attemptGet(data, "",
+ "password", "pass"), // keys
+ MoreMapUtils.attemptGet(data, "localhost",
+ "hostname", "host"), // keys
+ MoreMapUtils.attemptGet(data, NumberConversions::toInt, 3306,
+ "port"), // keys
+ MoreMapUtils.attemptGet(data, "mysql",
+ "driver"), // keys
+ MoreMapUtils.attemptGet(data, "minecraft",
+ "database", "dbname", "db"), // keys
+ MoreMapUtils.attemptGet(data, NumberConversions::toInt, 10,
+ "poolSize", "poolsize"), // keys
+ MoreMapUtils.attemptGet(data, NumberConversions::toLong, 10_000L,
+ "connectionTimeout", "connection_timeout"), // keys
+ MoreMapUtils.attemptGet(data, NumberConversions::toLong, 600_000L,
+ "idleTimeout", "idle_timeout"), // keys
+ MoreMapUtils.attemptGet(data, NumberConversions::toLong, 7_200_000L,
+ "maxLifetime", "max_lifetime")); // keys
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseMigration.java b/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseMigration.java
new file mode 100644
index 00000000..852aabcc
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/dao/DatabaseMigration.java
@@ -0,0 +1,56 @@
+package vg.civcraft.mc.civmodcore.dao;
+
+import java.sql.SQLException;
+import javax.annotation.Nonnull;
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * Interface to allow for more object-oriented based migration. I've noticed as plugins get older, their DAOs get
+ * larger and larger to accommodate more and more migrations. This interface allows migrations to be split up into
+ * individually functional classes with the convention of NEVER using plugin APIs: that anything the migration needs
+ * to do should be encapsulated within the migration class.
+ */
+public interface DatabaseMigration {
+
+ /**
+ * @return Returns this migration's id -- 0, 1, 2, etc, must be unique.
+ */
+ int getMigrationId();
+
+ /**
+ * @return Returns whether errors in this migration should be ignored.
+ */
+ default boolean shouldIgnoreErrors() {
+ return false;
+ }
+
+ /**
+ * @return Returns this migration's queries. Each query will be run in sequences. Must not be null or empty!
+ */
+ @Nonnull
+ String[] getMigrationQueries();
+
+ /**
+ * An optional callback that'll run after the migration has completed.
+ *
+ * @return Returns whether the callback completed successfully.
+ */
+ default boolean migrationCallback(@Nonnull final ManagedDatasource datasource) throws SQLException {
+ return true;
+ }
+
+ /**
+ * @param datasource The datasource to register this migration to.
+ */
+ default void registerMigration(@Nonnull final ManagedDatasource datasource) {
+ final var queries = getMigrationQueries();
+ if (ArrayUtils.isEmpty(queries)) {
+ throw new IllegalArgumentException("Migration queries cannot be null or empty!");
+ }
+ datasource.registerMigration(getMigrationId(),
+ shouldIgnoreErrors(),
+ () -> migrationCallback(datasource),
+ queries);
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/dao/ManagedDatasource.java b/src/main/java/vg/civcraft/mc/civmodcore/dao/ManagedDatasource.java
index e867fb2e..61429ec3 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/dao/ManagedDatasource.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/dao/ManagedDatasource.java
@@ -1,7 +1,5 @@
package vg.civcraft.mc.civmodcore.dao;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -9,11 +7,8 @@
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.NavigableMap;
-import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@@ -21,14 +16,12 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
-import java.util.logging.Logger;
-import org.apache.commons.collections4.MapUtils;
-import org.bukkit.Bukkit;
-import org.bukkit.configuration.serialization.ConfigurationSerializable;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.bukkit.plugin.Plugin;
import vg.civcraft.mc.civmodcore.ACivMod;
-import vg.civcraft.mc.civmodcore.util.MoreCollectionUtils;
-import vg.civcraft.mc.civmodcore.util.MoreMapUtils;
+import vg.civcraft.mc.civmodcore.utilities.CivLogger;
+import vg.civcraft.mc.civmodcore.utilities.MoreCollectionUtils;
/**
* Plugins should replace their custom Database handlers with an instance of ManagedDatasource.
@@ -71,67 +64,77 @@
*
* @author ProgrammerDan (refactored by Protonull)
*/
-public class ManagedDatasource implements ConfigurationSerializable {
+public class ManagedDatasource {
- private static final Logger LOGGER = Bukkit.getLogger();
+ private static final String CREATE_MIGRATIONS_TABLE = """
+ CREATE TABLE IF NOT EXISTS managed_plugin_data (
+ managed_id BIGINT NOT NULL AUTO_INCREMENT,
+ plugin_name VARCHAR(120) NOT NULL,
+ management_began TIMESTAMP NOT NULL DEFAULT NOW(),
+ current_migration_number INT NOT NULL,
+ last_migration TIMESTAMP,
+ CONSTRAINT pk_managed_plugin_data PRIMARY KEY (managed_id),
+ CONSTRAINT uniq_managed_plugin UNIQUE (plugin_name),
+ INDEX idx_managed_plugin USING BTREE (plugin_name)
+ );
+ """;
- private static final String CREATE_MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS managed_plugin_data ("
- + "managed_id BIGINT NOT NULL AUTO_INCREMENT, " + "plugin_name VARCHAR(120) NOT NULL, "
- + "management_began TIMESTAMP NOT NULL DEFAULT NOW(), " + "current_migration_number INT NOT NULL, "
- + "last_migration TIMESTAMP, " + "CONSTRAINT pk_managed_plugin_data PRIMARY KEY (managed_id), "
- + "CONSTRAINT uniq_managed_plugin UNIQUE (plugin_name), "
- + "INDEX idx_managed_plugin USING BTREE (plugin_name)" + ");";
+ private static final String CREATE_LOCK_TABLE = """
+ CREATE TABLE IF NOT EXISTS managed_plugin_locks (
+ plugin_name VARCHAR(120) NOT NULL,
+ lock_time TIMESTAMP NOT NULL DEFAULT NOW(),
+ CONSTRAINT pk_managed_plugin_locks PRIMARY KEY (plugin_name)
+ );
+ """;
- private static final String CREATE_LOCK_TABLE = "CREATE TABLE IF NOT EXISTS managed_plugin_locks ("
- + "plugin_name VARCHAR(120) NOT NULL, " + "lock_time TIMESTAMP NOT NULL DEFAULT NOW(), "
- + "CONSTRAINT pk_managed_plugin_locks PRIMARY KEY (plugin_name)" + ");";
+ private static final String CHECK_LAST_MIGRATION = """
+ SELECT current_migration_number FROM managed_plugin_data WHERE plugin_name = ?;
+ """;
- private static final String CHECK_LAST_MIGRATION = "SELECT current_migration_number FROM managed_plugin_data "
- + "WHERE plugin_name = ?;";
+ private static final String RECORD_MIGRATION = """
+ INSERT INTO managed_plugin_data
+ (plugin_name, current_migration_number, last_migration)
+ VALUES
+ (?, ?, NOW())
+ ON DUPLICATE KEY UPDATE
+ plugin_name = VALUES(plugin_name),
+ current_migration_number = VALUES(current_migration_number),
+ last_migration = VALUES(last_migration);
+ """;
- private static final String RECORD_MIGRATION = "INSERT INTO managed_plugin_data "
- + "(plugin_name, current_migration_number, last_migration) "
- + "VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE plugin_name = VALUES(plugin_name), "
- + "current_migration_number = VALUES(current_migration_number), "
- + "last_migration = VALUES(last_migration);";
+ private static final String CLEANUP_LOCK_TABLE = """
+ DELETE FROM managed_plugin_locks WHERE lock_time <= TIMESTAMPADD(HOUR, -8, NOW());
+ """;
- private static final String CLEANUP_LOCK_TABLE = "DELETE FROM managed_plugin_locks "
- + "WHERE lock_time <= TIMESTAMPADD(HOUR, -8, NOW());";
+ private static final String ACQUIRE_LOCK = """
+ INSERT IGNORE INTO managed_plugin_locks (plugin_name) VALUES (?);
+ """;
- private static final String ACQUIRE_LOCK = "INSERT IGNORE INTO managed_plugin_locks (plugin_name) VALUES (?);";
-
- private static final String RELEASE_LOCK = "DELETE FROM managed_plugin_locks WHERE plugin_name = ?;";
+ private static final String RELEASE_LOCK = """
+ DELETE FROM managed_plugin_locks WHERE plugin_name = ?;
+ """;
private static final long MAX_WAIT_FOR_LOCK = 600000L;
-
private static final long WAIT_PERIOD = 500L;
+ private final CivLogger logger;
private final Plugin plugin;
- private final DatabaseCredentials credentials;
private final ConnectionPool connections;
private final ExecutorService postExecutor;
private final TreeMap migrations;
private int firstMigration;
private int lastMigration;
- /**
- * See {@link #ManagedDatasource(ACivMod, String, String, String, int, String, String, int, long, long, long)} for
- * more details.
- *
- * @param plugin The civ plugin whose database is being managed.
- * @param user The SQL user to connect as.
- * @param pass The SQL user's password.
- * @param host The hostname of the database.
- * @param port The port to connect via.
- * @param database The specific database to create and modify tables in.
- * @param poolSize The maximum size of the connection pool (under 10 recommended)
- * @param connectionTimeout The longest a query can run until it times out (1-5 seconds recommended)
- * @param idleTimeout The longest a connection can sit idle before recycling (10 minutes recommended, check dbms)
- * @param maxLifetime The longest a connection can exist in total. (2 hours recommended, check dbms)
- */
- public ManagedDatasource(ACivMod plugin, String user, String pass, String host, int port, String database,
- int poolSize, long connectionTimeout, long idleTimeout, long maxLifetime) {
- this(plugin, user, pass, host, port, "mysql", database, poolSize, connectionTimeout, idleTimeout, maxLifetime);
+ private ManagedDatasource(final CivLogger logger,
+ final ACivMod plugin,
+ final ConnectionPool connections) {
+ this.logger = logger;
+ this.plugin = plugin;
+ this.connections = connections;
+ this.postExecutor = Executors.newSingleThreadExecutor();
+ this.migrations = new TreeMap<>();
+ this.firstMigration = Integer.MAX_VALUE;
+ this.lastMigration = Integer.MIN_VALUE;
}
/**
@@ -174,55 +177,36 @@ public ManagedDatasource(ACivMod plugin, String user, String pass, String host,
* docs for any further questions.
*
* @param plugin The plugin whose database is being managed.
- * @param user The SQL user to connect as.
- * @param pass The SQL user's password.
- * @param host The hostname of the database.
- * @param port The port to connect via.
- * @param driver The jdbc driver to use to connect to the database.
- * @param database The specific database to create and modify tables in.
- * @param poolSize The maximum size of the connection pool (under 10 recommended)
- * @param connectionTimeout The longest a query can run until it times out (1-5 seconds recommended)
- * @param idleTimeout The longest a connection can sit idle before recycling (10 minutes recommended, check dbms)
- * @param maxLifetime The longest a connection can exist in total. (2 hours recommended, check dbms)
- */
- public ManagedDatasource(ACivMod plugin, String user, String pass, String host, int port, String driver,
- String database, int poolSize, long connectionTimeout, long idleTimeout,
- long maxLifetime) {
- this(plugin, new DatabaseCredentials(user, pass, host, port, driver, database,
- poolSize, connectionTimeout, idleTimeout, maxLifetime));
- }
-
- /**
- * Create a new ManagedDatasource.
- *
- * @param plugin The plugin whose database is being managed.
* @param credentials The credentials to connect to the database with.
+ * @return Returns
*/
- public ManagedDatasource(ACivMod plugin, DatabaseCredentials credentials) {
- Preconditions.checkArgument(plugin != null && plugin.isEnabled());
- Preconditions.checkArgument(credentials != null);
- this.plugin = plugin;
- this.credentials = credentials;
- LOGGER.info(String.format("Connecting to %s@%s:%s using %s",credentials.getDatabase(),
- credentials.getHostname(), credentials.getPort(), credentials.getUsername()));
- this.connections = new ConnectionPool(credentials);
- this.postExecutor = Executors.newSingleThreadExecutor();
- this.migrations = new TreeMap<>();
- this.firstMigration = Integer.MAX_VALUE;
- this.lastMigration = Integer.MIN_VALUE;
- try (Connection connection = connections.getConnection();) {
- try (Statement statement = connection.createStatement();) {
+ @Nullable
+ public static ManagedDatasource construct(@Nonnull final ACivMod plugin,
+ @Nullable final DatabaseCredentials credentials) {
+ final var logger = CivLogger.getLogger(plugin.getClass(), ManagedDatasource.class);
+ if (credentials == null) {
+ logger.warning("You must pass in a set of credentials");
+ return null;
+ }
+ final var connections = new ConnectionPool(credentials);
+ logger.info(String.format("Connecting to %s@%s:%s using %s",credentials.database(),
+ credentials.host(), credentials.port(), credentials.username()));
+ try (final Connection connection = connections.getConnection()) {
+ try (final Statement statement = connection.createStatement()) {
statement.executeUpdate(ManagedDatasource.CREATE_MIGRATIONS_TABLE);
}
- try (Statement statement = connection.createStatement();) {
+ try (final Statement statement = connection.createStatement()) {
statement.executeUpdate(ManagedDatasource.CREATE_LOCK_TABLE);
}
}
- catch (SQLException ignored) {
- LOGGER.severe("Failed to prepare migrations table or register this plugin to it.");
- LOGGER.log(Level.SEVERE, "Assuming you provided proper database credentials this is most likely happening, because " +
- "your mysql install is outdated. We recommend using MariaDB or at least the latest mysql version", ignored);
+ catch (final SQLException exception) {
+ logger.severe("Failed to prepare migrations table or register this plugin to it.");
+ logger.log(Level.SEVERE, "Assuming you provided proper database credentials this is most likely " +
+ "happening, because your mysql install is outdated. We recommend using MariaDB or at least the " +
+ "latest mysql version.", exception);
+ return null;
}
+ return new ManagedDatasource(logger, plugin, connections);
}
/**
@@ -234,7 +218,9 @@ public ManagedDatasource(ACivMod plugin, DatabaseCredentials credentials) {
* @param ignoreErrors Indicates if errors in this migration should be ignored.
* @param queries The queries to run, in sequence.
*/
- public void registerMigration(int id, boolean ignoreErrors, String... queries) {
+ public void registerMigration(final int id,
+ final boolean ignoreErrors,
+ final String... queries) {
registerMigration(id, ignoreErrors, null, queries);
}
@@ -248,13 +234,16 @@ public void registerMigration(int id, boolean ignoreErrors, String... queries) {
* @param callback An optional callback that'll run after the migration has completed.
* @param queries The queries to run, in sequence.
*/
- public void registerMigration(int id, boolean ignoreErrors, Callable callback, String... queries) {
+ public void registerMigration(final int id,
+ final boolean ignoreErrors,
+ final Callable callback,
+ final String... queries) {
this.migrations.put(id, new Migration(ignoreErrors, callback, queries));
- if (id > lastMigration) {
- lastMigration = id;
+ if (id > this.lastMigration) {
+ this.lastMigration = id;
}
- if (id < firstMigration) {
- firstMigration = id;
+ if (id < this.firstMigration) {
+ this.firstMigration = id;
}
}
@@ -280,51 +269,51 @@ public boolean updateDatabase() {
try {
checkWaitLock();
}
- catch (SQLException exception) {
- LOGGER.log(Level.SEVERE, "An uncorrectable SQL error was encountered!", exception);
+ catch (final SQLException exception) {
+ this.logger.log(Level.SEVERE, "An uncorrectable SQL error was encountered!", exception);
return false;
}
- catch (TimeoutException exception) {
- LOGGER.log(Level.SEVERE, "Unable to acquire a lock!", exception);
+ catch (final TimeoutException exception) {
+ this.logger.log(Level.SEVERE, "Unable to acquire a lock!", exception);
return false;
}
// Now check update level, etc.
- int currentLevel = migrations.firstKey() - 1;
- try (Connection connection = getConnection();
- PreparedStatement statement = connection.prepareStatement(CHECK_LAST_MIGRATION);) {
+ int currentLevel = this.migrations.firstKey() - 1;
+ try (final Connection connection = getConnection();
+ final PreparedStatement statement = connection.prepareStatement(CHECK_LAST_MIGRATION)) {
statement.setString(1, plugin.getName());
- try (ResultSet set = statement.executeQuery();) {
+ try (final ResultSet set = statement.executeQuery()) {
if (set.next()) {
currentLevel = set.getInt(1);
} // else we aren't tracked yet!
}
}
- catch (SQLException e) {
- LOGGER.log(Level.SEVERE, "Unable to check last migration!", e);
+ catch (final SQLException exception) {
+ this.logger.log(Level.SEVERE, "Unable to check last migration!", exception);
releaseLock();
return false;
}
- NavigableMap newApply = migrations.tailMap(currentLevel, false);
+ final NavigableMap newApply = this.migrations.tailMap(currentLevel, false);
try {
if (newApply.size() > 0) {
- LOGGER.info(String.format("%s database is behind, %s migrations found",
- plugin.getName(), newApply.size()));
+ this.logger.info(String.format("%s database is behind, %s migrations found",
+ this.plugin.getName(), newApply.size()));
if (doMigrations(newApply)) {
- LOGGER.info(plugin.getName() + " fully migrated.");
+ this.logger.info(this.plugin.getName() + " fully migrated.");
}
else {
- LOGGER.warning(plugin.getName() + " failed to apply updates.");
+ this.logger.warning(this.plugin.getName() + " failed to apply updates.");
return false;
}
}
else {
- LOGGER.info(plugin.getName() + " database is up to date.");
+ this.logger.info(this.plugin.getName() + " database is up to date.");
}
return true;
}
- catch (Exception exception) {
- LOGGER.warning(plugin.getName() + " failed to apply updates for some reason...");
- LOGGER.log(Level.WARNING, "Full exception: ", exception);
+ catch (final Throwable exception) {
+ this.logger.warning(this.plugin.getName() + " failed to apply updates for some reason...");
+ this.logger.log(Level.WARNING, "Full exception: ", exception);
return false;
}
finally {
@@ -332,60 +321,63 @@ public boolean updateDatabase() {
}
}
- private boolean doMigrations(NavigableMap migrations) {
+ private boolean doMigrations(final NavigableMap migrations) {
try {
- for (Integer id : migrations.keySet()) {
- LOGGER.info("Migration " + id + " ] Applying");
- Migration migration = migrations.get(id);
+ for (final Integer id : migrations.keySet()) {
+ this.logger.info("Migration " + id + " ] Applying");
+ final Migration migration = migrations.get(id);
if (migration == null) {
continue; // huh?
}
if (doMigration(id, migration.migrations, migration.ignoreErrors, migration.postMigration)) {
- LOGGER.info("Migration " + id + " ] Successful");
- try (Connection connection = getConnection();
- PreparedStatement statement = connection.prepareStatement(RECORD_MIGRATION);) {
- statement.setString(1, plugin.getName());
+ this.logger.info("Migration " + id + " ] Successful");
+ try (final Connection connection = getConnection();
+ final PreparedStatement statement = connection.prepareStatement(RECORD_MIGRATION)) {
+ statement.setString(1, this.plugin.getName());
statement.setInt(2, id);
if (statement.executeUpdate() < 1) {
- LOGGER.warning("Might not have recorded migration " + id + " occurrence successfully.");
+ this.logger.warning("Might not have recorded migration " + id + " occurrence successfully.");
}
}
- catch (SQLException exception) {
- LOGGER.warning("Failed to record migration " + id + " occurrence successfully.");
- LOGGER.log(Level.SEVERE, "Full Error: ", exception);
+ catch (final SQLException exception) {
+ this.logger.warning("Failed to record migration " + id + " occurrence successfully.");
+ this.logger.log(Level.SEVERE, "Full Error: ", exception);
return false;
}
}
else {
- LOGGER.info("Migration " + id + " ] Failed");
+ this.logger.info("Migration " + id + " ] Failed");
return false;
}
}
return true;
}
- catch (Exception exception) {
- LOGGER.log(Level.SEVERE, "Unexpected failure during migrations", exception);
+ catch (final Throwable exception) {
+ this.logger.log(Level.SEVERE, "Unexpected failure during migrations", exception);
return false;
}
}
- private boolean doMigration(Integer migration, List queries, boolean ignoreErrors, Callable post) {
- try (Connection connection = getConnection();) {
- for (String query : queries) {
- try (Statement statement = connection.createStatement();) {
+ private boolean doMigration(final Integer migration,
+ final List queries,
+ final boolean ignoreErrors,
+ final Callable post) {
+ try (final Connection connection = getConnection()) {
+ for (final String query : queries) {
+ try (final Statement statement = connection.createStatement()) {
statement.executeUpdate(query);
if (!ignoreErrors) { // if we ignore errors we totally ignore warnings.
SQLWarning warning = statement.getWarnings();
while (warning != null) {
- LOGGER.warning("Migration " + migration + " ] Warning: " + warning.getMessage());
+ this.logger.warning("Migration " + migration + " ] Warning: " + warning.getMessage());
// TODO: add verbose check
warning = warning.getNextWarning();
}
}
}
- catch (SQLException exception) {
+ catch (final SQLException exception) {
if (ignoreErrors) {
- LOGGER.warning("Migration " + migration + " ] Ignoring error: " + exception.getMessage());
+ this.logger.warning("Migration " + migration + " ] Ignoring error: " + exception.getMessage());
}
else {
throw exception;
@@ -393,40 +385,40 @@ private boolean doMigration(Integer migration, List queries, boolean ign
}
}
}
- catch (SQLException exception) {
+ catch (final SQLException exception) {
if (ignoreErrors) {
- LOGGER.warning("Migration " + migration + " ] Ignoring error: " + exception.getMessage());
+ this.logger.warning("Migration " + migration + " ] Ignoring error: " + exception.getMessage());
}
else {
- LOGGER.warning("Migration " + migration + " ] Failed migration: " + exception.getMessage());
- LOGGER.log(Level.SEVERE, "Full Error: ", exception);
+ this.logger.warning("Migration " + migration + " ] Failed migration: " + exception.getMessage());
+ this.logger.log(Level.SEVERE, "Full Error: ", exception);
return false;
}
}
if (post != null) {
- Future doing = postExecutor.submit(post);
+ final Future doing = postExecutor.submit(post);
try {
if (doing.get()) {
- LOGGER.info("Migration " + migration + " ] Post Call Complete");
+ this.logger.info("Migration " + migration + " ] Post Call Complete");
}
else {
if (ignoreErrors) {
- LOGGER.warning("Migration " + migration + " ] Post Call indicated failure; ignored.");
+ this.logger.warning("Migration " + migration + " ] Post Call indicated failure; ignored.");
}
else {
- LOGGER.severe("Migration " + migration + " ] Post Call failed!");
+ this.logger.severe("Migration " + migration + " ] Post Call failed!");
return false;
}
}
}
- catch (Exception exception) {
+ catch (final Throwable exception) {
if (ignoreErrors) {
- LOGGER.warning("Migration " + migration + " ] Post Call indicated failure; ignored: " +
+ this.logger.warning("Migration " + migration + " ] Post Call indicated failure; ignored: " +
exception.getMessage());
}
else {
- LOGGER.severe("Migration " + migration + " ] Post Call failed!");
- LOGGER.log(Level.SEVERE, "Full Error: ", exception);
+ this.logger.severe("Migration " + migration + " ] Post Call failed!");
+ this.logger.log(Level.SEVERE, "Full Error: ", exception);
return false;
}
}
@@ -440,14 +432,14 @@ private boolean doMigration(Integer migration, List queries, boolean ign
* @return Returns true if the plugin has an entry in the migrations table; false for any other outcome.
*/
public boolean isManaged() {
- try (Connection connection = getConnection();
- PreparedStatement statement = connection.prepareStatement(CHECK_LAST_MIGRATION);) {
+ try (final Connection connection = getConnection();
+ final PreparedStatement statement = connection.prepareStatement(CHECK_LAST_MIGRATION)) {
statement.setString(1, plugin.getName());
- try (ResultSet set = statement.executeQuery();) {
+ try (final ResultSet set = statement.executeQuery()) {
return set.next();
}
}
- catch (SQLException e) {
+ catch (final SQLException ignored) {
return false;
}
}
@@ -469,27 +461,28 @@ public boolean isManaged() {
*/
private boolean checkWaitLock() throws TimeoutException, SQLException {
/* First, cleanup old locks if any */
- try (Connection connection = getConnection(); Statement cleanup = connection.createStatement();) {
+ try (final Connection connection = getConnection();
+ final Statement cleanup = connection.createStatement()) {
cleanup.executeUpdate(CLEANUP_LOCK_TABLE);
}
- catch (SQLException exception) {
- LOGGER.severe("Unable to cleanup old locks, error encountered!");
+ catch (final SQLException exception) {
+ this.logger.severe("Unable to cleanup old locks, error encountered!");
throw exception;
}
/* Now get our own lock */
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < MAX_WAIT_FOR_LOCK) {
- try (Connection connection = getConnection();
- PreparedStatement tryAcquire = connection.prepareStatement(ACQUIRE_LOCK);) {
- tryAcquire.setString(1, plugin.getName());
+ try (final Connection connection = getConnection();
+ final PreparedStatement tryAcquire = connection.prepareStatement(ACQUIRE_LOCK)) {
+ tryAcquire.setString(1, this.plugin.getName());
int hasLock = tryAcquire.executeUpdate();
if (hasLock > 0) {
- LOGGER.info("Lock acquired, proceeding.");
+ this.logger.info("Lock acquired, proceeding.");
return true;
}
}
- catch (SQLException failToAcquire) {
- LOGGER.severe("Unable to acquire a lock, error encountered!");
+ catch (final SQLException failToAcquire) {
+ this.logger.severe("Unable to acquire a lock, error encountered!");
// let the exception continue so we return right away; only errors we'd encounter here are terminal.
throw failToAcquire;
}
@@ -499,7 +492,7 @@ private boolean checkWaitLock() throws TimeoutException, SQLException {
try {
Thread.sleep(WAIT_PERIOD);
}
- catch (InterruptedException ignored) {
+ catch (final InterruptedException ignored) {
// Someone wants us to check right away.
}
}
@@ -507,19 +500,19 @@ private boolean checkWaitLock() throws TimeoutException, SQLException {
}
private void releaseLock() {
- try (Connection connection = getConnection();
- PreparedStatement release = connection.prepareStatement(RELEASE_LOCK);) {
+ try (final Connection connection = getConnection();
+ final PreparedStatement release = connection.prepareStatement(RELEASE_LOCK)) {
release.setString(1, plugin.getName());
int releaseLock = release.executeUpdate();
if (releaseLock < 1) {
- LOGGER.warning("Attempted to release a lock, already released.");
+ this.logger.warning("Attempted to release a lock, already released.");
}
else {
- LOGGER.info("Lock released.");
+ this.logger.info("Lock released.");
}
}
- catch (SQLException exception) {
- LOGGER.log(Level.WARNING, "Attempted to release lock; failed. This may interrupt startup for other " +
+ catch (final SQLException exception) {
+ this.logger.log(Level.WARNING, "Attempted to release lock; failed. This may interrupt startup for other " +
"servers working against this database.", exception);
}
}
@@ -533,7 +526,7 @@ private void releaseLock() {
* @throws SQLException If the pool has gone away, database is not connected, or other error has occurred.
*/
public Connection getConnection() throws SQLException {
- return connections.getConnection();
+ return this.connections.getConnection();
}
/**
@@ -542,48 +535,13 @@ public Connection getConnection() throws SQLException {
* @throws SQLException Something went horribly wrong.
*/
public void close() throws SQLException {
- connections.close();
+ this.connections.close();
}
- private static class Migration {
- public List migrations;
- public boolean ignoreErrors;
- public Callable postMigration;
+ private static record Migration(boolean ignoreErrors, Callable postMigration, List migrations) {
public Migration(boolean ignoreErrors, Callable postMigration, String... migrations) {
- this.migrations = MoreCollectionUtils.collect(ArrayList::new, migrations);
- this.ignoreErrors = ignoreErrors;
- this.postMigration = postMigration;
- }
- }
-
- @Override
- public Map serialize() {
- Map data = new HashMap<>();
- data.put("plugin", this.plugin.getName());
- data.putAll(this.credentials.serialize());
- return data;
- }
-
- public static ManagedDatasource deserialize(Map data) {
- if (MapUtils.isEmpty(data)) {
- LOGGER.info("Database not defined.");
- return null;
- }
- String pluginName = MoreMapUtils.attemptGet(data, "", "plugin");
- if (Strings.isNullOrEmpty(pluginName)) {
- LOGGER.warning("Config defined ManagedDatasource did not specify a plugin, which is required.");
- return null;
- }
- Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName);
- if (plugin == null || !plugin.isEnabled()) {
- LOGGER.warning("Config defined ManagedDatasource did not specify a loaded plugin, is it correct?");
- return null;
- }
- if (!ACivMod.class.isAssignableFrom(plugin.getClass())) {
- LOGGER.warning("ManagedDatasource only supports ACivMod plugins.");
- return null;
+ this(ignoreErrors, postMigration, MoreCollectionUtils.collect(ArrayList::new, migrations));
}
- return new ManagedDatasource((ACivMod) plugin, Objects.requireNonNull(DatabaseCredentials.deserialize(data)));
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/entities/EntityUtils.java b/src/main/java/vg/civcraft/mc/civmodcore/entities/EntityUtils.java
index 0621b2d2..129b5d96 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/entities/EntityUtils.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/entities/EntityUtils.java
@@ -1,13 +1,16 @@
package vg.civcraft.mc.civmodcore.entities;
import com.google.common.base.Strings;
-import org.bukkit.entity.Entity;
+import javax.annotation.Nonnull;
+import lombok.experimental.UtilityClass;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TranslatableComponent;
import org.bukkit.entity.EntityType;
-import org.bukkit.entity.Player;
/**
* Class of static APIs for Entities.
*/
+@UtilityClass
public final class EntityUtils {
/**
@@ -24,34 +27,23 @@ public static EntityType getEntityType(final String value) {
try {
return EntityType.valueOf(value.toUpperCase());
}
- catch (Exception ignored) { }
+ catch (final Throwable ignored) { }
try {
final EntityType type = EntityType.fromId(Short.parseShort(value));
if (type != null) {
return type;
}
}
- catch (final Exception ignored) { }
+ catch (final Throwable ignored) { }
return null;
}
/**
- * Checks whether an entity is a player.
- *
- * @param entity The entity to test.
- * @return Returns true if the entity is a player.
+ * @param entityType The entity type to translate.
+ * @return Returns a translatable component based on the given entity type.
*/
- public static boolean isPlayer(final Entity entity) {
- if (entity == null) {
- return false;
- }
- if (entity.getType() != EntityType.PLAYER) {
- return false;
- }
- if (!(entity instanceof Player)) {
- return false;
- }
- return true;
+ public static TranslatableComponent asTranslatable(@Nonnull final EntityType entityType) {
+ return Component.translatable(entityType.translationKey());
}
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/ClonedInventory.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/ClonedInventory.java
index 8354095f..79c37d80 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventory/ClonedInventory.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/ClonedInventory.java
@@ -5,6 +5,8 @@
import java.util.ListIterator;
import java.util.Spliterator;
import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -13,9 +15,7 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import vg.civcraft.mc.civmodcore.util.MoreArrayUtils;
+import vg.civcraft.mc.civmodcore.utilities.MoreArrayUtils;
/**
* Wrapper for cloned inventories intended to ensure that ClonedInventories aren't themselves cloned.
@@ -58,15 +58,15 @@ public HashMap addItem(final ItemStack... items) throws Ille
return this.inventory.addItem(items);
}
- @NotNull
+ @Nonnull
@Override
public HashMap removeItem(final ItemStack... items) throws IllegalArgumentException {
return this.inventory.removeItem(items);
}
- @NotNull
+ @Nonnull
@Override
- public HashMap removeItemAnySlot(@NotNull ItemStack... items) throws IllegalArgumentException {
+ public HashMap removeItemAnySlot(@Nonnull ItemStack... items) throws IllegalArgumentException {
return this.inventory.removeItemAnySlot(items);
}
@@ -91,7 +91,7 @@ public void setStorageContents(final ItemStack[] items) throws IllegalArgumentEx
}
@Override
- public boolean contains(@NotNull final Material material) throws IllegalArgumentException {
+ public boolean contains(@Nonnull final Material material) throws IllegalArgumentException {
return this.inventory.contains(material);
}
@@ -101,7 +101,7 @@ public boolean contains(final ItemStack item) {
}
@Override
- public boolean contains(@NotNull final Material material, final int amount) throws IllegalArgumentException {
+ public boolean contains(@Nonnull final Material material, final int amount) throws IllegalArgumentException {
return this.inventory.contains(material, amount);
}
@@ -116,7 +116,7 @@ public boolean containsAtLeast(final ItemStack item, final int amount) {
}
@Override
- public HashMap all(@NotNull final Material material) throws IllegalArgumentException {
+ public HashMap all(@Nonnull final Material material) throws IllegalArgumentException {
return this.inventory.all(material);
}
@@ -126,12 +126,12 @@ public boolean containsAtLeast(final ItemStack item, final int amount) {
}
@Override
- public int first(@NotNull final Material material) throws IllegalArgumentException {
+ public int first(@Nonnull final Material material) throws IllegalArgumentException {
return this.inventory.first(material);
}
@Override
- public int first(@NotNull final ItemStack item) {
+ public int first(@Nonnull final ItemStack item) {
return this.inventory.first(item);
}
@@ -146,12 +146,12 @@ public boolean isEmpty() {
}
@Override
- public void remove(@NotNull final Material material) throws IllegalArgumentException {
+ public void remove(@Nonnull final Material material) throws IllegalArgumentException {
this.inventory.remove(material);
}
@Override
- public void remove(@NotNull final ItemStack item) {
+ public void remove(@Nonnull final ItemStack item) {
this.inventory.remove(item);
}
@@ -165,6 +165,11 @@ public void clear() {
this.inventory.clear();
}
+ @Override
+ public int close() {
+ return this.inventory.close();
+ }
+
@Override
public List getViewers() {
return this.inventory.getViewers();
@@ -186,7 +191,7 @@ public InventoryHolder getHolder(boolean useSnapshot) {
return this.inventory.getHolder(useSnapshot);
}
- @NotNull
+ @Nonnull
@Override
public ListIterator iterator() {
return this.inventory.iterator();
@@ -231,10 +236,24 @@ public Inventory getInventory() {
* @return Returns a clone of the given inventory.
*/
public static ClonedInventory cloneInventory(final Inventory inventory) {
+ return cloneInventory(inventory, false);
+ }
+
+ /**
+ * Clones the given inventory for the purpose of test manipulating its contents.
+ *
+ * Note: Do not type check the inventory, it's JUST a contents copy within an inventory wrapper to provide the
+ * relevant and useful methods.
+ *
+ * @param inventory The inventory to clone.
+ * @param forceClone Determines whether the inventory should be cloned even if it's an already cloned inventory.
+ * @return Returns a clone of the given inventory.
+ */
+ public static ClonedInventory cloneInventory(final Inventory inventory, final boolean forceClone) {
if (inventory == null) {
return null;
}
- if (inventory instanceof ClonedInventory) {
+ if (!forceClone && inventory instanceof ClonedInventory) {
return (ClonedInventory) inventory;
}
Inventory clone;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/InventoryUtils.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/InventoryUtils.java
index 094adfe7..2f1749f1 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventory/InventoryUtils.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/InventoryUtils.java
@@ -2,32 +2,39 @@
import com.google.common.base.Preconditions;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.ArrayUtils;
+import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils;
-import vg.civcraft.mc.civmodcore.util.MoreArrayUtils;
+@UtilityClass
public final class InventoryUtils {
+ public static final int CHEST_1_ROW = 9;
+ public static final int CHEST_2_ROWS = 9 * 2;
+ public static final int CHEST_3_ROWS = 9 * 3;
+ public static final int CHEST_4_ROWS = 9 * 4;
+ public static final int CHEST_5_ROWS = 9 * 5;
+ public static final int CHEST_6_ROWS = 9 * 6;
+
/**
* Tests an inventory to see if it's valid.
*
* @param inventory The inventory to test.
* @return Returns true if it's more likely than not valid.
*/
- public static boolean isValidInventory(final Inventory inventory) {
- if (inventory == null) {
- return false;
- }
- if (inventory.getSize() <= 0) {
- return false;
- }
- return true;
+ public static boolean isValidInventory(@Nullable final Inventory inventory) {
+ return inventory != null
+ && inventory.getSize() > 0;
}
/**
@@ -35,10 +42,13 @@ public static boolean isValidInventory(final Inventory inventory) {
*
* @param inventory The inventory to get the viewers of.
* @return Returns a list of players. Returns an empty list if the inventory is null.
+ *
+ * @deprecated Use Java 16's instanceof pattern matching instead.
*/
- public static List getViewingPlayers(final Inventory inventory) {
+ @Deprecated
+ public static List getViewingPlayers(@Nullable final Inventory inventory) {
if (!isValidInventory(inventory)) {
- return new ArrayList<>();
+ return new ArrayList<>(0);
}
return inventory.getViewers().stream()
.filter(entity -> entity instanceof Player)
@@ -52,15 +62,15 @@ public static List getViewingPlayers(final Inventory inventory) {
* @param inventory The inventory to attempt to find a slot in.
* @param item The item to find a place for.
* @return Returns an index of a slot that it's safe to add to. A return value of -1 means no safe place. Even if
- * the return value is -1 it may still be possible to add the item stack to the inventory, as this
- * function attempts to find the first slot that the given item stack can fit into wholly; that if it can
- * technically fit but has to be distributed then there's no "first empty".
+ * the return value is -1 it may still be possible to add the item stack to the inventory, as this
+ * function attempts to find the first slot that the given item stack can fit into wholly; that if it can
+ * technically fit but has to be distributed then there's no "first empty".
*/
- public static int firstEmpty(final Inventory inventory, final ItemStack item) {
+ public static int firstEmpty(@Nullable final Inventory inventory, final ItemStack item) {
if (inventory == null) {
return -1;
}
- // If there's a slot free, then just return that. Otherwise if
+ // If there's a slot free, then just return that. Otherwise, if
// the item is invalid, just return whatever slot was returned.
final int index = inventory.firstEmpty();
if (index >= 0 || !ItemUtils.isValidItem(item)) {
@@ -88,9 +98,10 @@ public static int firstEmpty(final Inventory inventory, final ItemStack item) {
*
* @param inventory The inventory to clear of items.
*/
- public static void clearInventory(final Inventory inventory) {
- Preconditions.checkArgument(isValidInventory(inventory));
- inventory.setContents(MoreArrayUtils.fill(inventory.getContents(), null));
+ public static void clearInventory(@Nonnull final Inventory inventory) {
+ final ItemStack[] contents = inventory.getContents();
+ Arrays.fill(contents, new ItemStack(Material.AIR));
+ inventory.setContents(contents);
}
/**
@@ -99,11 +110,8 @@ public static void clearInventory(final Inventory inventory) {
* @param inventory The inventory to check.
* @return Returns true if an inventory has multiple viewers.
*/
- public static boolean hasOtherViewers(final Inventory inventory) {
- if (!isValidInventory(inventory)) {
- return false;
- }
- return inventory.getViewers().size() > 1;
+ public static boolean hasOtherViewers(@Nullable final Inventory inventory) {
+ return inventory != null && inventory.getViewers().size() > 1;
}
/**
@@ -113,13 +121,9 @@ public static boolean hasOtherViewers(final Inventory inventory) {
* @return Returns true if the slot count is or between 1-6 multiples of 9.
*/
public static boolean isValidChestSize(final int slots) {
- if (slots <= 0 || slots > 54) {
- return false;
- }
- if ((slots % 9) > 0) {
- return false;
- }
- return true;
+ return slots > 0
+ && slots <= 54
+ && (slots % 9) == 0;
}
/**
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/RecipeManager.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/RecipeManager.java
index 3fb24f73..37b18290 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventory/RecipeManager.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/RecipeManager.java
@@ -1,14 +1,15 @@
package vg.civcraft.mc.civmodcore.inventory;
-import java.util.Iterator;
+import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.Keyed;
import org.bukkit.inventory.Recipe;
-import vg.civcraft.mc.civmodcore.util.NullUtils;
+import vg.civcraft.mc.civmodcore.utilities.NullUtils;
/**
* Class of utility functions for Recipes.
*/
+@UtilityClass
public final class RecipeManager {
/**
@@ -22,13 +23,13 @@ public final class RecipeManager {
* @return Returns true if the other recipe matches the base.
*/
public static boolean matchRecipe(final Recipe base, final Recipe other) {
- if (base == null) {
- return false;
+ if (base == other) {
+ return true;
}
- if (base instanceof Keyed && other instanceof Keyed) {
+ if (base instanceof Keyed baseKeyed && other instanceof Keyed otherKeyed) {
return NullUtils.equalsNotNull(
- ((Keyed) base).getKey(),
- ((Keyed) other).getKey());
+ baseKeyed.getKey(),
+ otherKeyed.getKey());
}
return false;
}
@@ -51,7 +52,7 @@ public static boolean registerRecipe(final Recipe recipe) {
try {
return Bukkit.getServer().addRecipe(recipe);
}
- catch (Exception exception) {
+ catch (final Throwable exception) {
return false;
}
}
@@ -66,13 +67,11 @@ public static boolean removeRecipe(final Recipe recipe) {
if (recipe == null) {
return false;
}
- final Iterator iterator = Bukkit.getServer().recipeIterator();
- while (iterator.hasNext()) {
- if (!matchRecipe(recipe, iterator.next())) {
- continue;
+ for (final var iterator = Bukkit.getServer().recipeIterator(); iterator.hasNext();) {
+ if (matchRecipe(recipe, iterator.next())) {
+ iterator.remove();
+ break;
}
- iterator.remove();
- return true;
}
return true;
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/AnimatedClickable.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java
similarity index 96%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/AnimatedClickable.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java
index 94d7c529..cccc7c32 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/AnimatedClickable.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import java.util.List;
import org.bukkit.entity.Player;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/Clickable.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/Clickable.java
similarity index 92%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/Clickable.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/Clickable.java
index 4b353a98..f1ad4fcb 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/Clickable.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/Clickable.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import org.bukkit.inventory.ItemStack;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventory.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java
similarity index 92%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventory.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java
index 3b484327..89ebf0cd 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventory.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java
@@ -1,9 +1,8 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
@@ -14,6 +13,7 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
+import vg.civcraft.mc.civmodcore.CivModCorePlugin;
/**
* Represents an inventory filled with Clickables. Whenever one of those is
@@ -172,13 +172,32 @@ public Inventory getInventory() {
/**
* Shows a player the inventory of this instance with all of its clickables
+ * Same as calling {@code showInventory(p, true)}
*
* @param p Player to show the inventory to
*/
public void showInventory(Player p) {
+ showInventory(p, true);
+ }
+
+ /**
+ * Shows a player the inventory of this instance with all of its clickables
+ * Same as calling {@code showInventory(p, true)}
+ *
+ * @param p Player to show the inventory to
+ * @param eventSafe should be true if called in any code triggered by an InventoryEvent
+ */
+ public void showInventory(Player p, boolean eventSafe) {
if (p != null) {
- p.openInventory(inventory);
- openInventories.put(p.getUniqueId(), this);
+ if (eventSafe) {
+ Bukkit.getScheduler().runTask(CivModCorePlugin.getInstance(), () -> {
+ p.openInventory(inventory);
+ openInventories.put(p.getUniqueId(), this);
+ });
+ } else {
+ p.openInventory(inventory);
+ openInventories.put(p.getUniqueId(), this);
+ }
applyCooldown(p);
}
}
@@ -188,7 +207,7 @@ public void showInventory(Player p) {
* currently open and syncs it with the internal representation.
*/
public void updateInventory() {
- for (Map.Entry c : openInventories.entrySet()) {
+ for (var c : new ArrayList<>(openInventories.entrySet())) {
if (c.getValue() == this) {
Player p = Bukkit.getPlayer(c.getKey());
p.updateInventory();
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventoryListener.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventoryListener.java
similarity index 74%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventoryListener.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventoryListener.java
index f0b915c8..34896a75 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/ClickableInventoryListener.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventoryListener.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -6,7 +6,6 @@
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerQuitEvent;
-import vg.civcraft.mc.civmodcore.entities.EntityUtils;
/**
* The listener which makes ClickableInventories work. To use this either register it as a listener in your plugin or
@@ -18,10 +17,9 @@ public class ClickableInventoryListener implements Listener {
@EventHandler
public void inventoryClick(InventoryClickEvent event) {
- if (!EntityUtils.isPlayer(event.getWhoClicked())) {
+ if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
- Player player = (Player) event.getWhoClicked();
ClickableInventory inventory = ClickableInventory.getOpenInventory(player);
if (inventory != null) {
event.setCancelled(true); // always cancel first to prevent dupes
@@ -31,10 +29,10 @@ public void inventoryClick(InventoryClickEvent event) {
@EventHandler
public void inventoryClose(InventoryCloseEvent event) {
- if (!EntityUtils.isPlayer(event.getPlayer())) { // Despite the name, it's not necessarily a player
+ if (!(event.getPlayer() instanceof Player player)) { // Despite the name, it's not necessarily a player
return;
}
- ClickableInventory.inventoryWasClosed((Player) event.getPlayer());
+ ClickableInventory.inventoryWasClosed(player);
}
@EventHandler
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/DecorationStack.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/DecorationStack.java
similarity index 93%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/DecorationStack.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/DecorationStack.java
index c7426ff3..e8ef3cf9 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/DecorationStack.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/DecorationStack.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import org.bukkit.Material;
import org.bukkit.entity.Player;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/IClickable.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/IClickable.java
new file mode 100644
index 00000000..90fc82ad
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/IClickable.java
@@ -0,0 +1,130 @@
+package vg.civcraft.mc.civmodcore.inventory.gui;
+
+import javax.annotation.Nonnull;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public abstract class IClickable {
+
+ /**
+ * @return Which item stack represents this clickable when it is initially loaded into the inventory.
+ */
+ @Nonnull
+ public abstract ItemStack getItemStack();
+
+ /**
+ * Called when this instance is added to an inventory so it can do something if
+ * desired
+ *
+ * @param inventory Inventory it was added to
+ * @param slot Slot in which it was added
+ */
+ public abstract void addedToInventory(@Nonnull final ClickableInventory inventory, final int slot);
+
+ /**
+ * General method called whenever this clickable is clicked with a type that did
+ * not a have a special implementation provided
+ *
+ * @param clicker Player who clicked
+ */
+ protected abstract void clicked(@Nonnull final Player clicker);
+
+ /**
+ * Called when a player double clicks this clickable, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onDoubleClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player drops one of this clickable (in default keybinds
+ * pressing Q while hovering the slot), overwrite to define special behavior for
+ * this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onDrop(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player drops this clickable stack (in default keybinds pressing
+ * Q while holding CTRL and hovering the slot), overwrite to define special
+ * behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onControlDrop(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player left clicks this clickable, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onLeftClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player right clicks this clickable, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onRightClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player middle (mouse wheell) clicks this clickable, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onMiddleClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player left clicks this clickable while holding shift, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onShiftLeftClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ /**
+ * Called when a player right clicks this clickable while holding shift, overwrite to define
+ * special behavior for this
+ *
+ * @param clicker Player who clicked
+ */
+ protected void onShiftRightClick(@Nonnull final Player clicker) {
+ clicked(clicker);
+ }
+
+ public void handleClick(@Nonnull final Player clicker,
+ @Nonnull final ClickType type) {
+ switch (type) {
+ case CONTROL_DROP -> onControlDrop(clicker);
+ case DOUBLE_CLICK -> onDoubleClick(clicker);
+ case DROP -> onDrop(clicker);
+ case LEFT -> onLeftClick(clicker);
+ case MIDDLE -> onMiddleClick(clicker);
+ case RIGHT -> onRightClick(clicker);
+ case SHIFT_LEFT -> onShiftLeftClick(clicker);
+ case SHIFT_RIGHT -> onShiftRightClick(clicker);
+ default -> clicked(clicker);
+ }
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/LClickable.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/LClickable.java
similarity index 96%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/LClickable.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/LClickable.java
index 317ee467..a2bfa5af 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/LClickable.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/LClickable.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import java.util.function.Consumer;
import org.bukkit.Material;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/MultiPageView.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/MultiPageView.java
similarity index 98%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/MultiPageView.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/MultiPageView.java
index 957acf04..1bc31196 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/MultiPageView.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/MultiPageView.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui;
+package vg.civcraft.mc.civmodcore.inventory.gui;
import java.util.List;
import org.bukkit.ChatColor;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuPagination.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuPagination.java
new file mode 100644
index 00000000..78e04292
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuPagination.java
@@ -0,0 +1,241 @@
+package vg.civcraft.mc.civmodcore.inventory.gui.canvas;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import org.apache.commons.collections4.CollectionUtils;
+import org.bukkit.entity.Player;
+import org.ipvp.canvas.slot.SlotSettings;
+import org.ipvp.canvas.type.AbstractCivMenu;
+
+/**
+ * Better version of {@link org.ipvp.canvas.paginate.PaginatedMenuBuilder} as it allows you to apply pagination to an
+ * already existing menu, rather than using a builder to spawn a bunch of new menu instances that'll just be used by a
+ * single player before promptly discarded.
+ */
+public final class MenuPagination {
+
+ private final AbstractCivMenu menu;
+ private final Map playerPages;
+ private final List menuBar;
+ private final ElementSupplier supplier;
+ private final int buttonSlots;
+
+ private MenuPagination(final AbstractCivMenu menu,
+ final ElementSupplier supplier,
+ final List menuBar) {
+ this.menu = Objects.requireNonNull(menu, "Menu cannot be null");
+ final int rows = menu.getDimensions().getRows();
+ if (rows < 2 || rows > 5) {
+ throw new IllegalArgumentException("Row count must be between 2 and 5 inclusively");
+ }
+ this.playerPages = new Object2ObjectAVLTreeMap<>();
+ this.menuBar = List.copyOf(menuBar);
+ if (menuBar.size() > 9) {
+ throw new IllegalArgumentException("Bar cannot contain more than 9 buttons");
+ }
+ this.supplier = Objects.requireNonNull(supplier, "Element supplier cannot be null");
+ this.buttonSlots = (rows * 9) - 9;
+
+ MenuUtils.overrideOpenHandler(menu, (viewer, opened) -> {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ pager.pageNumber = 0;
+ pager.elements = this.supplier.generate(viewer, opened);
+ INTERNAL_applyPlayerPage(viewer, opened, INTERNAL_getPlayerPager(viewer));
+ }, true);
+
+ MenuUtils.overrideUpdateHandler(menu, (viewer, updated) -> {
+ INTERNAL_applyPlayerPage(viewer, updated, INTERNAL_getPlayerPager(viewer));
+ }, true);
+
+ menu.setCallCloseHandlerRegardless(true);
+ MenuUtils.overrideCloseHandler(menu, (viewer, ignored) -> {
+ this.playerPages.remove(viewer.getUniqueId());
+ }, true);
+
+ for (int i = 0; i < this.buttonSlots; i++) {
+ menu.getSlot(i).setClickHandler((viewer, click) -> {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ final int buttonIndex = INTERNAL_calculateElementIndex(pager, click.getClickedSlot().getIndex());
+ if (buttonIndex >= pager.elements.size()) {
+ return;
+ }
+ final var handler = pager.elements.get(buttonIndex).getClickHandler();
+ if (handler == null) {
+ return;
+ }
+ handler.click(viewer, click);
+ });
+ }
+
+ for (int i = 0; i < 9; i++) {
+ final var slotIndex = INTERNAL_calculateBarIndex(i);
+ if (i >= this.menuBar.size()) {
+ menu.getSlot(slotIndex).setSettings(MenuUtils.createEmptySlot());
+ break;
+ }
+ final var barElement = this.menuBar.get(i);
+ if (barElement == null) {
+ menu.getSlot(slotIndex).setSettings(MenuUtils.createEmptySlot());
+ continue;
+ }
+ menu.getSlot(slotIndex).setSettings(barElement);
+ }
+ }
+
+ /**
+ * @param viewer The viewer to send to the next page.
+ */
+ public void nextPage(final Player viewer) {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ pager.pageNumber++;
+ this.menu.update(viewer);
+ }
+
+ /**
+ * @param viewer The viewer to send to the previous page.
+ */
+ public void previousPage(final Player viewer) {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ pager.pageNumber--;
+ this.menu.update(viewer);
+ }
+
+ /**
+ * @param viewer The viewer to check if they have a next page.
+ * @return Returns whether the viewer has a next page.
+ */
+ public boolean hasNextPage(final Player viewer) {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ final int elementsSize = CollectionUtils.size(pager.elements);
+ final int totalPages = elementsSize / this.buttonSlots;
+ if (totalPages == 0) {
+ return false;
+ }
+ return pager.pageNumber >= totalPages;
+ }
+
+ /**
+ * @param viewer The viewer to check if they have a previous page.
+ * @return Returns whether the viewer has a previous page.
+ */
+ public boolean hasPreviousPage(final Player viewer) {
+ final var pager = INTERNAL_getPlayerPager(viewer);
+ final int elementsSize = CollectionUtils.size(pager.elements);
+ final int totalPages = elementsSize / this.buttonSlots;
+ if (totalPages == 0) {
+ return false;
+ }
+ return pager.pageNumber < totalPages;
+ }
+
+ protected void INTERNAL_applyPlayerPage(final Player viewer,
+ final AbstractCivMenu menu,
+ final INTERNAL_PlayerPager pager) {
+ pager.pageNumber = Math.max(pager.pageNumber, 0);
+ pager.pageNumber = Math.min(pager.pageNumber, pager.elements.size() / this.buttonSlots);
+ for (int i = 0; i < this.buttonSlots; i++) {
+ final int buttonIndex = INTERNAL_calculateElementIndex(pager, i);
+ if (buttonIndex >= pager.elements.size()) {
+ menu.getSlot(i).setRawItem(viewer, null);
+ continue;
+ }
+ final var template = pager.elements.get(buttonIndex).getItemTemplate();
+ if (template == null) {
+ menu.getSlot(i).setRawItem(viewer, null);
+ continue;
+ }
+ menu.getSlot(i).setRawItem(viewer, template.getItem(viewer));
+ }
+ }
+
+ protected INTERNAL_PlayerPager INTERNAL_getPlayerPager(final Player viewer) {
+ return this.playerPages.computeIfAbsent(viewer.getUniqueId(), (uuid) -> new INTERNAL_PlayerPager());
+ }
+
+ protected int INTERNAL_calculateElementIndex(final INTERNAL_PlayerPager pager, final int slot) {
+ return (pager.pageNumber * this.buttonSlots) + slot;
+ }
+
+ protected int INTERNAL_calculateBarIndex(final int slot) {
+ return this.buttonSlots + slot;
+ }
+
+ protected static class INTERNAL_PlayerPager {
+ private int pageNumber;
+ private List elements;
+ }
+
+ @FunctionalInterface
+ public interface ElementSupplier {
+ List generate(Player viewer, AbstractCivMenu menu);
+ }
+
+ /**
+ * @return Returns a new pagination builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private AbstractCivMenu menu;
+ private ElementSupplier supplier;
+ private final SlotSettings[] menuBar;
+
+ Builder() {
+ this.menuBar = new SlotSettings[] {
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot(),
+ MenuUtils.createEmptySlot()
+ };
+ }
+
+ /**
+ * @param menu The menu to paginate.
+ * @return Returns this builder.
+ */
+ public Builder setMenu(final AbstractCivMenu menu) {
+ this.menu = menu;
+ return this;
+ }
+
+ /**
+ * @param supplier Supplier method to return all the elements to be paginated.
+ * @return Returns this builder.
+ */
+ public Builder setElementSupplier(final ElementSupplier supplier) {
+ this.supplier = supplier;
+ return this;
+ }
+
+ /**
+ * @param index The index of the menu bar element from 0-8 inclusively.
+ * @param element The menu bar element to add.
+ * @return Returns this builder.
+ */
+ public Builder setMenuBarElement(final int index, final SlotSettings element) {
+ this.menuBar[index] = element == null ? MenuUtils.createEmptySlot() : element;
+ return this;
+ }
+
+ /**
+ * @return Returns a constructed menu pagination based on this builder.
+ */
+ public MenuPagination build() {
+ return new MenuPagination(this.menu, this.supplier, Arrays.asList(this.menuBar));
+ }
+
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuUtils.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuUtils.java
new file mode 100644
index 00000000..8fc3eb71
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MenuUtils.java
@@ -0,0 +1,104 @@
+package vg.civcraft.mc.civmodcore.inventory.gui.canvas;
+
+import lombok.experimental.UtilityClass;
+import org.ipvp.canvas.Menu;
+import org.ipvp.canvas.slot.SlotSettings;
+import org.ipvp.canvas.type.AbstractCivMenu;
+import org.ipvp.canvas.type.OpenHandler;
+import org.ipvp.canvas.type.UpdateHandler;
+
+@UtilityClass
+public final class MenuUtils {
+
+ /**
+ * @return Returns an empty, unmodifiable slot.
+ */
+ public static SlotSettings createEmptySlot() {
+ return SlotSettings.builder().build();
+ }
+
+ /**
+ * Overrides a given menu's open event handler, in lieu of just replacing it, meaning that you can add handlers to
+ * a menu without fear of removing any previously added functionality.
+ *
+ * @param menu The menu to add the handler for.
+ * @param handler The handler to add.
+ * @param callSuperBefore Whether or not to call the existing handler before or after the given handler.
+ */
+ public static void overrideOpenHandler(final AbstractCivMenu menu,
+ final OpenHandler handler,
+ final boolean callSuperBefore) {
+ final var findExisting = menu.getOpenHandler();
+ if (findExisting.isEmpty()) {
+ menu.setOpenHandler(handler);
+ return;
+ }
+ final var existing = findExisting.get();
+ menu.setOpenHandler((viewer, openedMenu) -> {
+ if (callSuperBefore) {
+ existing.handle(viewer, openedMenu);
+ }
+ handler.handle(viewer, openedMenu);
+ if (!callSuperBefore) {
+ existing.handle(viewer, openedMenu);
+ }
+ });
+ }
+
+ /**
+ * Overrides a given menu's update event handler, in lieu of just replacing it, meaning that you can add handlers
+ * to a menu without fear of removing any previously added functionality.
+ *
+ * @param menu The menu to add the handler for.
+ * @param handler The handler to add.
+ * @param callSuperBefore Whether or not to call the existing handler before or after the given handler.
+ */
+ public static void overrideUpdateHandler(final AbstractCivMenu menu,
+ final UpdateHandler handler,
+ final boolean callSuperBefore) {
+ final var findExisting = menu.getUpdateHandler();
+ if (findExisting.isEmpty()) {
+ menu.setUpdateHandler(handler);
+ return;
+ }
+ final var existing = findExisting.get();
+ menu.setUpdateHandler((viewer, openedMenu) -> {
+ if (callSuperBefore) {
+ existing.handle(viewer, openedMenu);
+ }
+ handler.handle(viewer, openedMenu);
+ if (!callSuperBefore) {
+ existing.handle(viewer, openedMenu);
+ }
+ });
+ }
+
+ /**
+ * Overrides a given menu's close event handler, in lieu of just replacing it, meaning that you can add handlers to
+ * a menu without fear of removing any previously added functionality.
+ *
+ * @param menu The menu to add the handler for.
+ * @param handler The handler to add.
+ * @param callSuperBefore Whether or not to call the existing handler before or after the given handler.
+ */
+ public static void overrideCloseHandler(final AbstractCivMenu menu,
+ final Menu.CloseHandler handler,
+ final boolean callSuperBefore) {
+ final var findExisting = menu.getCloseHandler();
+ if (findExisting.isEmpty()) {
+ menu.setCloseHandler(handler);
+ return;
+ }
+ final var existing = findExisting.get();
+ menu.setCloseHandler((viewer, openedMenu) -> {
+ if (callSuperBefore) {
+ existing.close(viewer, openedMenu);
+ }
+ handler.close(viewer, openedMenu);
+ if (!callSuperBefore) {
+ existing.close(viewer, openedMenu);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MultiPageCanvas.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MultiPageCanvas.java
new file mode 100644
index 00000000..0b3375db
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/canvas/MultiPageCanvas.java
@@ -0,0 +1,180 @@
+package vg.civcraft.mc.civmodcore.inventory.gui.canvas;
+
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.ipvp.canvas.slot.SlotSettings;
+import org.ipvp.canvas.type.AbstractCivMenu;
+import org.ipvp.canvas.type.CivChestMenu;
+import vg.civcraft.mc.civmodcore.inventory.InventoryUtils;
+import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils;
+
+/**
+ * Utility to automate creating views, which have multiple pages and automatically adjust their size
+ *
+ */
+public class MultiPageCanvas {
+
+ private final Player viewer;
+ private final Component title;
+ private final List slots;
+ private final boolean adjustSize;
+ private final SlotSettings[] extraMenuItems;
+
+ private AbstractCivMenu window;
+ private int currentPage;
+
+ public MultiPageCanvas(@Nonnull final Player viewer,
+ @Nullable final Component title,
+ @Nonnull final List slots,
+ final boolean adjustSize) {
+ this.viewer = Objects.requireNonNull(viewer);
+ this.title = title;
+ this.slots = List.copyOf(slots);
+ this.adjustSize = adjustSize;
+ this.extraMenuItems = new SlotSettings[7];
+ this.currentPage = 0;
+ }
+
+ private void INTERNAL_constructInventory() {
+ // Clamp the total page within the valid range
+ if (this.currentPage < 0) {
+ this.currentPage = 0;
+ }
+ final int totalPages = INTERNAL_calculateTotalPages();
+ if (this.currentPage >= totalPages) {
+ this.currentPage = totalPages - 1;
+ }
+ // Update the window instance if necessary
+ final int totalRows = INTERNAL_calculateTotalRows();
+ if (this.window == null) {
+ this.window = new CivChestMenu(this.title, totalRows);
+ }
+ else {
+ this.window.clear();
+ if (this.adjustSize && totalRows != this.window.getDimensions().getRows()) {
+ this.window = new CivChestMenu(this.title, totalRows);
+ }
+ }
+ // Add all content slots
+ final int contentOffset = this.currentPage * InventoryUtils.CHEST_5_ROWS;
+ final int contentLength = Math.min(this.slots.size() - contentOffset, InventoryUtils.CHEST_5_ROWS);
+ for (int i = 0; i < contentLength; i++) {
+ this.window.getSlot(i).setSettings(this.slots.get(contentOffset + i));
+ }
+ final int totalSlots = this.window.getDimensions().getArea();
+ // Back button
+ if (this.currentPage > 0) {
+ this.window.getSlot(totalSlots - 10).setSettings(SlotSettings.builder()
+ .itemTemplate((ignored) -> {
+ final var item = new ItemStack(Material.ARROW);
+ ItemUtils.handleItemMeta(item, (ItemMeta meta) -> {
+ meta.displayName(Component.text()
+ .color(NamedTextColor.GOLD)
+ .content("Go to previous page")
+ .build());
+ return true;
+ });
+ return item;
+ })
+ .clickHandler((clicker, click) -> {
+ if (MultiPageCanvas.this.currentPage > 0) {
+ MultiPageCanvas.this.currentPage--;
+ }
+ showScreen();
+ })
+ .build());
+ }
+ // Next button
+ if (this.currentPage < totalPages) {
+ this.window.getSlot(totalSlots - 1).setSettings(SlotSettings.builder()
+ .itemTemplate((ignored) -> {
+ final var item = new ItemStack(Material.ARROW);
+ ItemUtils.handleItemMeta(item, (ItemMeta meta) -> {
+ meta.displayName(Component.text()
+ .color(NamedTextColor.GOLD)
+ .content("Go to next page")
+ .build());
+ return true;
+ });
+ return item;
+ })
+ .clickHandler((clicker, click) -> {
+ if (MultiPageCanvas.this.currentPage < totalPages) {
+ MultiPageCanvas.this.currentPage++;
+ }
+ showScreen();
+ })
+ .build());
+ }
+ // Other menu items
+ for (int i = 0; i < this.extraMenuItems.length; i++) {
+ final SlotSettings slot = this.extraMenuItems[i];
+ if (slot == null) {
+ continue;
+ }
+ this.window.getSlot(totalSlots - 9 + i).setSettings(slot);
+ }
+ }
+
+ protected int INTERNAL_calculateTotalPages() {
+ final int size = this.slots.size();
+ int pages = size / InventoryUtils.CHEST_5_ROWS;
+ if ((size % InventoryUtils.CHEST_5_ROWS) > 0) {
+ pages++;
+ }
+ return Math.max(pages, 0);
+ }
+
+ private int INTERNAL_calculateTotalRows() {
+ if (!this.adjustSize) {
+ return 6;
+ }
+ final int clicks = this.slots.size();
+ if (clicks >= InventoryUtils.CHEST_5_ROWS) {
+ return 6;
+ }
+ final int rows = (clicks / 9) + ((clicks % 9) > 0 ? 1 : 0);
+ return Math.max(rows, 2); // Always have at least 2 rows
+ }
+
+ /**
+ * Shows the current page
+ */
+ public void showScreen() {
+ INTERNAL_constructInventory();
+ this.window.open(this.viewer);
+ }
+
+ /**
+ * Allows setting a menu slot at the bottom of the GUI. Will not update the inventory, you'll need to do that
+ * manually with {@link #showScreen()}.
+ *
+ * @param slot The slot to set.
+ * @param index The index of the slot. Must be between 0 and 6 inclusively.
+ */
+ public void setMenuSlot(@Nullable final SlotSettings slot, final int index) {
+ if (index < 0 || index > 6) {
+ throw new IllegalArgumentException("Index for MultiPageView menu must be between 0 and 6");
+ }
+ this.extraMenuItems[index] = slot;
+ }
+
+ /**
+ * Forcibly changes the page of this view. Will not update the inventory, you'll need to do that manually with
+ * {@link #showScreen()}.
+ *
+ * @param page The page to set.
+ */
+ public void setPage(final int page) {
+ this.currentPage = page;
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableInventory.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableInventory.java
similarity index 85%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableInventory.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableInventory.java
index 40ba6269..ec66d6a7 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableInventory.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableInventory.java
@@ -1,9 +1,9 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
import java.util.List;
import org.bukkit.entity.Player;
-import vg.civcraft.mc.civmodcore.inventorygui.ClickableInventory;
-import vg.civcraft.mc.civmodcore.inventorygui.IClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.ClickableInventory;
+import vg.civcraft.mc.civmodcore.inventory.gui.IClickable;
public class ComponableInventory extends ComponableSection {
@@ -43,7 +43,7 @@ public void setName(String name) {
}
@Override
- void updateComponent(InventoryComponent component) {
+ protected void updateComponent(InventoryComponent component) {
// copy of the implementation from ComponableSection, except that we also mirror
// changes through to the ClickableInventory
int offSet = 0;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableSection.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableSection.java
similarity index 93%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableSection.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableSection.java
index 7b9b5d38..3c9d557e 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ComponableSection.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ComponableSection.java
@@ -1,9 +1,9 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntPredicate;
-import vg.civcraft.mc.civmodcore.inventorygui.IClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.IClickable;
public class ComponableSection extends InventoryComponent {
@@ -58,7 +58,7 @@ public void removeComponent(InventoryComponent component) {
* Updates the displayed clickables for one specific contained component
* @param component Component to update
*/
- void updateComponent(InventoryComponent component) {
+ protected void updateComponent(InventoryComponent component) {
int offSet = 0;
component.rebuild();
List componentContent = component.getContent();
@@ -84,7 +84,7 @@ public void clear() {
}
@Override
- void rebuild() {
+ protected void rebuild() {
// we use lists instead of maps here, because we expect the amount of components
// to be very low, rarely to never above 5
List> builds = new ArrayList<>();
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligner.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligner.java
similarity index 54%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligner.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligner.java
index cec3f530..f31d315c 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligner.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligner.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
public interface ContentAligner {
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligners.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligners.java
similarity index 90%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligners.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligners.java
index fbadfffd..1ec06b41 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/ContentAligners.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/ContentAligners.java
@@ -1,7 +1,9 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
import java.util.function.Function;
+import lombok.experimental.UtilityClass;
+@UtilityClass
public class ContentAligners {
public static ContentAligner getLeftAligned() {
@@ -48,7 +50,7 @@ public static ContentAligner getCenteredInOrder(int contentAmount, int windowSiz
if (contentAmount % rowLength == 0) {
offset = 1;
}
- int lastElementLastCompleteRow = ((contentAmount / rowLength) + offset) * rowLength - 1;
+ int lastElementLastCompleteRow = (((contentAmount / rowLength) + offset) * rowLength - 1) + rowLength;
return new Counter(i -> {
// just increment until we reach the last element in the last full row
@@ -56,10 +58,10 @@ public static ContentAligner getCenteredInOrder(int contentAmount, int windowSiz
return i + 1;
} else {
// jump to offset start of last row
- int lengthLastRow = contentAmount - lastElementLastCompleteRow - 1;
+ int lengthLastRow = (contentAmount + rowLength) - lastElementLastCompleteRow - 1;
int emptySlots = rowLength - lengthLastRow;
int leftOffset = Math.max(1, emptySlots / 2);
- return i + leftOffset;
+ return (i + 1) + leftOffset;
}
}, defaultNum);
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/InventoryComponent.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/InventoryComponent.java
similarity index 86%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/InventoryComponent.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/InventoryComponent.java
index deaa50d9..9c4e38e5 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/InventoryComponent.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/InventoryComponent.java
@@ -1,9 +1,9 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import vg.civcraft.mc.civmodcore.inventorygui.IClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.IClickable;
public abstract class InventoryComponent {
@@ -29,8 +29,8 @@ public InventoryComponent(int size) {
public int getSize() {
return size;
}
-
- abstract void rebuild();
+
+ protected abstract void rebuild();
List getContent() {
return Collections.unmodifiableList(content);
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/Scrollbar.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/Scrollbar.java
similarity index 94%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/Scrollbar.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/Scrollbar.java
index 8183e488..8f5a6cc4 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/Scrollbar.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/Scrollbar.java
@@ -1,11 +1,11 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.ChatColor;
import org.bukkit.Material;
-import vg.civcraft.mc.civmodcore.inventorygui.IClickable;
-import vg.civcraft.mc.civmodcore.inventorygui.LClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.IClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.LClickable;
public class Scrollbar extends InventoryComponent {
@@ -91,7 +91,7 @@ public void setPage(int page) {
}
@Override
- void rebuild() {
+ protected void rebuild() {
int size = getSize();
int contentIndex = scrollOffset * page;
// subtract offset created through the next/previous button
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/SlotPredicates.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/SlotPredicates.java
new file mode 100644
index 00000000..3a8faba7
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/SlotPredicates.java
@@ -0,0 +1,79 @@
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
+
+import java.util.function.IntPredicate;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public final class SlotPredicates {
+
+ private static final int ROW_LENGTH = 9;
+
+ public static IntPredicate slots(int count) {
+ Counter counter = new Counter(count);
+ return counter::isValid;
+ }
+
+ public static IntPredicate rows(int height) {
+ return rows(height, 9);
+ }
+
+ public static IntPredicate rows(int height, int rowLength) {
+ return slots(height * rowLength);
+ }
+
+ public static IntPredicate offsetRectangle(int height, int width, int y, int x,
+ int rowLength) {
+ OffsetSlotter slotter = new OffsetSlotter(y, x, height, width, rowLength);
+ return slotter::isValid;
+ }
+
+ public static IntPredicate offsetRectangle(int height, int width, int y, int x) {
+ return offsetRectangle(height, width, y, x, ROW_LENGTH);
+ }
+
+ public static IntPredicate rectangle(int height, int width, int rowLength) {
+ return offsetRectangle(height, width, 0, 0, rowLength);
+ }
+
+ public static IntPredicate rectangle(int height, int width) {
+ return rectangle(height, width, ROW_LENGTH);
+ }
+
+ private static class OffsetSlotter {
+
+ private int lowerHorizontalBound;
+ private int lowerVerticalBound;
+ private int upperHorizontalBound;
+ private int upperVerticalBound;
+ private int rowLength;
+
+ OffsetSlotter(int y, int x, int height, int width, int rowLength) {
+ this.lowerHorizontalBound = x;
+ this.upperHorizontalBound = x + width - 1;
+ this.lowerVerticalBound = y;
+ this.upperVerticalBound = y + height - 1;
+ this.rowLength = rowLength;
+ }
+
+ boolean isValid(int index) {
+ int row = index / rowLength;
+ int column = index % rowLength;
+ return column >= lowerHorizontalBound && column <= upperHorizontalBound && row >= lowerVerticalBound
+ && row <= upperVerticalBound;
+ }
+
+ }
+
+ private static class Counter {
+ private int count;
+
+ Counter(int count) {
+ this.count = count;
+ }
+
+ public boolean isValid(int index) {
+ return count-- > 0;
+ }
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/StaticDisplaySection.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/StaticDisplaySection.java
similarity index 83%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/StaticDisplaySection.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/StaticDisplaySection.java
index 6ec66d49..a5e87883 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/StaticDisplaySection.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/StaticDisplaySection.java
@@ -1,6 +1,6 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components;
+package vg.civcraft.mc.civmodcore.inventory.gui.components;
-import vg.civcraft.mc.civmodcore.inventorygui.IClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.IClickable;
public class StaticDisplaySection extends InventoryComponent {
private int additionCounter;
@@ -34,7 +34,7 @@ public void set(IClickable click, int slot) {
}
@Override
- void rebuild() {
+ protected void rebuild() {
//NO OP
}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/impl/CommonGUIs.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/impl/CommonGUIs.java
similarity index 59%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/impl/CommonGUIs.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/impl/CommonGUIs.java
index 4b64c24d..6fd0b4b4 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/components/impl/CommonGUIs.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/components/impl/CommonGUIs.java
@@ -1,11 +1,13 @@
-package vg.civcraft.mc.civmodcore.inventorygui.components.impl;
+package vg.civcraft.mc.civmodcore.inventory.gui.components.impl;
+import lombok.experimental.UtilityClass;
import org.bukkit.Material;
-import vg.civcraft.mc.civmodcore.inventorygui.LClickable;
-import vg.civcraft.mc.civmodcore.inventorygui.components.ComponableSection;
-import vg.civcraft.mc.civmodcore.inventorygui.components.SlotPredicates;
-import vg.civcraft.mc.civmodcore.inventorygui.components.StaticDisplaySection;
+import vg.civcraft.mc.civmodcore.inventory.gui.LClickable;
+import vg.civcraft.mc.civmodcore.inventory.gui.components.ComponableSection;
+import vg.civcraft.mc.civmodcore.inventory.gui.components.SlotPredicates;
+import vg.civcraft.mc.civmodcore.inventory.gui.components.StaticDisplaySection;
+@UtilityClass
public class CommonGUIs {
public static ComponableSection genConfirmationGUI(int rows, int columns, Runnable yesFunc, String yesText, Runnable noFunc,
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryItem.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryItem.java
new file mode 100644
index 00000000..37680fbe
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryItem.java
@@ -0,0 +1,7 @@
+package vg.civcraft.mc.civmodcore.inventory.gui.history;
+
+public interface HistoryItem {
+
+ void setStateTo();
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/history/HistoryTracker.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryTracker.java
similarity index 96%
rename from src/main/java/vg/civcraft/mc/civmodcore/inventorygui/history/HistoryTracker.java
rename to src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryTracker.java
index 919ad7ec..d065b334 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventorygui/history/HistoryTracker.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/history/HistoryTracker.java
@@ -1,4 +1,4 @@
-package vg.civcraft.mc.civmodcore.inventorygui.history;
+package vg.civcraft.mc.civmodcore.inventory.gui.history;
import java.util.LinkedList;
import java.util.ListIterator;
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/EnchantUtils.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/EnchantUtils.java
index 1f82abab..3264efa2 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/EnchantUtils.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/EnchantUtils.java
@@ -1,129 +1,71 @@
package vg.civcraft.mc.civmodcore.inventory.items;
-import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
-import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableMap;
import java.io.File;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.experimental.UtilityClass;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TranslatableComponent;
import org.apache.commons.collections4.CollectionUtils;
-import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import vg.civcraft.mc.civmodcore.CivModCorePlugin;
-import vg.civcraft.mc.civmodcore.util.Chainer;
-import vg.civcraft.mc.civmodcore.util.KeyedUtils;
+import vg.civcraft.mc.civmodcore.chat.ChatUtils;
+import vg.civcraft.mc.civmodcore.utilities.CivLogger;
+import vg.civcraft.mc.civmodcore.utilities.KeyedUtils;
/**
* Class of static utilities for Enchantments.
*/
+@UtilityClass
public final class EnchantUtils {
- private static final BiMap ENCHANT_NAMES = ImmutableBiMap.builder()
- // Beta 1.9
- .put(Enchantment.DAMAGE_ALL, "Sharpness")
- .put(Enchantment.DAMAGE_ARTHROPODS, "Bane of Arthropods")
- .put(Enchantment.DAMAGE_UNDEAD, "Smite")
- .put(Enchantment.DIG_SPEED, "Efficiency")
- .put(Enchantment.DURABILITY, "Unbreaking")
- .put(Enchantment.FIRE_ASPECT, "Fire Aspect")
- .put(Enchantment.KNOCKBACK, "Knockback")
- .put(Enchantment.LOOT_BONUS_BLOCKS, "Fortune")
- .put(Enchantment.LOOT_BONUS_MOBS, "Looting")
- .put(Enchantment.OXYGEN, "Respiration")
- .put(Enchantment.PROTECTION_ENVIRONMENTAL, "Protection")
- .put(Enchantment.PROTECTION_EXPLOSIONS, "Blast Protection")
- .put(Enchantment.PROTECTION_FALL, "Feather Falling")
- .put(Enchantment.PROTECTION_FIRE, "Fire Protection")
- .put(Enchantment.PROTECTION_PROJECTILE, "Projectile Protection")
- .put(Enchantment.SILK_TOUCH, "Silk Touch")
- .put(Enchantment.WATER_WORKER, "Aqua Affinity")
- // 1.1
- .put(Enchantment.ARROW_DAMAGE, "Power")
- .put(Enchantment.ARROW_FIRE, "Flame")
- .put(Enchantment.ARROW_INFINITE, "Infinity")
- .put(Enchantment.ARROW_KNOCKBACK, "Punch")
- // 1.4.6
- .put(Enchantment.THORNS, "Thorns")
- // 1.7.2
- .put(Enchantment.LUCK, "Luck of the Sea")
- .put(Enchantment.LURE, "Lure")
- // 1.8
- .put(Enchantment.DEPTH_STRIDER, "Depth Strider")
- // 1.9
- .put(Enchantment.FROST_WALKER, "Frost Walker")
- .put(Enchantment.MENDING, "Mending")
- // 1.11
- .put(Enchantment.BINDING_CURSE, "Curse of Binding")
- .put(Enchantment.VANISHING_CURSE, "Curse of Vanishing")
- // 1.11.1
- .put(Enchantment.SWEEPING_EDGE, "Sweeping Edge")
- // 1.13
- .put(Enchantment.CHANNELING, "Channeling")
- .put(Enchantment.IMPALING, "Impaling")
- .put(Enchantment.LOYALTY, "Loyalty")
- .put(Enchantment.RIPTIDE, "Riptide")
- // 1.14
- .put(Enchantment.MULTISHOT, "Multishot")
- .put(Enchantment.PIERCING, "Piercing")
- .put(Enchantment.QUICK_CHARGE, "Quick Charge")
- // 1.16
- .put(Enchantment.SOUL_SPEED, "Soul Speed")
- .build();
-
- private static final BiMap ENCHANT_ABBR = HashBiMap.create(ENCHANT_NAMES.size());
-
- static {
- // Determine if there's any enchants missing names
- final Set missing = new HashSet<>();
- CollectionUtils.addAll(missing, Enchantment.values());
- missing.removeIf(ENCHANT_NAMES::containsKey);
- if (!missing.isEmpty()) {
- //noinspection deprecation
- Bukkit.getLogger().warning("[EnchantUtils] The following enchants are lacking names: " +
- missing.stream().map(Enchantment::getName).collect(Collectors.joining(",")) + ".");
- }
- }
+ private static final BiMap ENCHANT_ABBR = HashBiMap.create(Enchantment.values().length);
/**
* Loads enchantment names and initials from the config.
*/
- public static void loadEnchantAbbreviations(final CivModCorePlugin plugin) {
+ public static void loadEnchantAbbreviations(@Nonnull final CivModCorePlugin plugin) {
+ final var logger = CivLogger.getLogger(EnchantUtils.class);
ENCHANT_ABBR.clear();
- final File enchantsFile = plugin.getResourceFile("enchants.yml");
+ final File enchantsFile = plugin.getDataFile("enchants.yml");
final YamlConfiguration enchantsConfig = YamlConfiguration.loadConfiguration(enchantsFile);
for (final String key : enchantsConfig.getKeys(false)) {
if (Strings.isNullOrEmpty(key)) {
- plugin.warning("[EnchantUtils] Enchantment key was empty.");
+ logger.warning("Enchantment key was empty.");
continue;
}
final Enchantment enchant = EnchantUtils.getEnchantment(key);
if (enchant == null) {
- plugin.warning("[EnchantUtils] Could not find enchantment: " + key);
+ logger.warning("Could not find enchantment: " + key);
return;
}
final String abbreviation = enchantsConfig.getString(key);
if (Strings.isNullOrEmpty(abbreviation)) {
- plugin.warning("[EnchantUtils] Abbreviation for [" + key + "] was empty.");
+ logger.warning("Abbreviation for [" + key + "] was empty.");
continue;
}
ENCHANT_ABBR.put(enchant, abbreviation);
}
- plugin.info("[EnchantUtils] Loaded a total of " + ENCHANT_ABBR.size() + " abbreviations from enchants.yml");
+ logger.info("Loaded a total of " + ENCHANT_ABBR.size() + " abbreviations from enchants.yml");
// Determine if there's any enchants missing abbreviations
final Set missing = new HashSet<>();
CollectionUtils.addAll(missing, Enchantment.values());
missing.removeIf(ENCHANT_ABBR::containsKey);
if (!missing.isEmpty()) {
//noinspection deprecation
- plugin.warning("[EnchantUtils] The following enchants are missing from enchants.yml: " +
+ logger.warning("The following enchants are missing from enchants.yml: " +
missing.stream().map(Enchantment::getName).collect(Collectors.joining(",")) + ".");
}
}
@@ -134,23 +76,27 @@ public static void loadEnchantAbbreviations(final CivModCorePlugin plugin) {
* @param value The value to search for a matching enchantment by.
* @return Returns a matched enchantment or null.
*/
- public static Enchantment getEnchantment(final String value) {
+ @Nullable
+ @SuppressWarnings("deprecation")
+ public static Enchantment getEnchantment(@Nullable final String value) {
if (Strings.isNullOrEmpty(value)) {
return null;
}
- Enchantment enchantment = Enchantment.getByKey(KeyedUtils.fromString(value));
- if (enchantment != null) {
- return enchantment;
- }
- //noinspection deprecation
- enchantment = Enchantment.getByName(value.toUpperCase());
- if (enchantment != null) {
- return enchantment;
+ Enchantment enchantment;
+ // From NamespacedKey
+ final var enchantmentKey = KeyedUtils.fromString(value);
+ if (enchantmentKey != null) {
+ enchantment = Enchantment.getByKey(enchantmentKey);
+ if (enchantment != null) {
+ return enchantment;
+ }
}
- enchantment = ENCHANT_NAMES.inverse().get(value);
+ // From Name
+ enchantment = Enchantment.getByName(value.toUpperCase()); // deprecated
if (enchantment != null) {
return enchantment;
}
+ // From Abbreviation
enchantment = ENCHANT_ABBR.inverse().get(value);
if (enchantment != null) {
return enchantment;
@@ -158,11 +104,33 @@ public static Enchantment getEnchantment(final String value) {
return null;
}
- public static String getEnchantNiceName(final Enchantment enchant) {
- return ENCHANT_NAMES.get(enchant);
+ /**
+ * @param enchant The enchantment to get a translatable component for.
+ * @return Returns a translatable component for the given enchantment.
+ */
+ @Nonnull
+ public static TranslatableComponent asTranslatable(@Nonnull final Enchantment enchant) {
+ return Component.translatable(enchant.translationKey());
}
- public static String getEnchantAbbreviation(final Enchantment enchant) {
+ /**
+ * @param enchant The enchantment to get the name of.
+ * @return Returns the name of the enchantment, or null.
+ *
+ * @deprecated Use {@link #asTranslatable(Enchantment)} instead.
+ */
+ @Nullable
+ @Deprecated
+ public static String getEnchantNiceName(@Nullable final Enchantment enchant) {
+ return enchant == null ? null : ChatUtils.stringify(asTranslatable(enchant));
+ }
+
+ /**
+ * @param enchant The enchantment to get the abbreviation of.
+ * @return Returns the abbreviation of the enchantment, or null.
+ */
+ @Nullable
+ public static String getEnchantAbbreviation(@Nullable final Enchantment enchant) {
return ENCHANT_ABBR.get(enchant);
}
@@ -176,8 +144,10 @@ public static String getEnchantAbbreviation(final Enchantment enchant) {
* @see Enchantment#getStartLevel() The starting level. A valid level cannot be below this.
* @see Enchantment#getMaxLevel() The maximum level. A valid level cannot be above this.
*/
- public static boolean isSafeEnchantment(final Enchantment enchantment, final int level) {
- return enchantment != null && level >= enchantment.getStartLevel() && level <= enchantment.getMaxLevel();
+ public static boolean isSafeEnchantment(@Nullable final Enchantment enchantment, final int level) {
+ return enchantment != null
+ && level >= enchantment.getStartLevel()
+ && level <= enchantment.getMaxLevel();
}
/**
@@ -186,8 +156,9 @@ public static boolean isSafeEnchantment(final Enchantment enchantment, final int
* @param item The item to retrieve the enchantments from.
* @return Returns the item's enchantments, which are never null.
*/
- public static Map getEnchantments(final ItemStack item) {
- return Chainer.from(item).then(ItemStack::getEnchantments).getOrGenerate(HashMap::new);
+ @Nonnull
+ public static Map getEnchantments(@Nullable final ItemStack item) {
+ return item == null ? ImmutableMap.of() : item.getEnchantments();
}
/**
@@ -200,7 +171,9 @@ public static Map getEnchantments(final ItemStack item) {
*
* @see EnchantUtils#isSafeEnchantment(Enchantment, int)
*/
- public static boolean addEnchantment(final ItemStack item, final Enchantment enchantment, final int level) {
+ public static boolean addEnchantment(@Nonnull final ItemStack item,
+ @Nonnull final Enchantment enchantment,
+ final int level) {
return addEnchantment(item, enchantment, level, true);
}
@@ -215,12 +188,11 @@ public static boolean addEnchantment(final ItemStack item, final Enchantment enc
*
* @see EnchantUtils#isSafeEnchantment(Enchantment, int)
*/
- public static boolean addEnchantment(final ItemStack item,
- final Enchantment enchantment,
+ public static boolean addEnchantment(@Nonnull final ItemStack item,
+ @Nonnull final Enchantment enchantment,
final int level,
final boolean onlyAllowSafeEnchantments) {
- Preconditions.checkArgument(ItemUtils.isValidItem(item));
- return ItemUtils.handleItemMeta(item, (ItemMeta meta) ->
+ return ItemUtils.handleItemMeta(Objects.requireNonNull(item), (ItemMeta meta) ->
meta.addEnchant(enchantment, level, !onlyAllowSafeEnchantments));
}
@@ -231,12 +203,11 @@ public static boolean addEnchantment(final ItemStack item,
* @param enchant The enchantment to remove from the item.
* @return Returns true if the enchantment was successfully removed.
*/
- public static boolean removeEnchantment(final ItemStack item, final Enchantment enchant) {
- Preconditions.checkArgument(ItemUtils.isValidItem(item));
- if (enchant == null) {
- return true;
- }
- return ItemUtils.handleItemMeta(item, (ItemMeta meta) -> meta.removeEnchant(enchant));
+ public static boolean removeEnchantment(@Nonnull final ItemStack item,
+ @Nullable final Enchantment enchant) {
+ return enchant == null
+ || ItemUtils.handleItemMeta(Objects.requireNonNull(item),
+ (ItemMeta meta) -> meta.removeEnchant(enchant));
}
/**
@@ -244,9 +215,8 @@ public static boolean removeEnchantment(final ItemStack item, final Enchantment
*
* @param item The item to clear enchantment from.
*/
- public static void clearEnchantments(final ItemStack item) {
- Preconditions.checkArgument(ItemUtils.isValidItem(item));
- ItemUtils.handleItemMeta(item, (ItemMeta meta) -> {
+ public static void clearEnchantments(@Nonnull final ItemStack item) {
+ ItemUtils.handleItemMeta(Objects.requireNonNull(item), (ItemMeta meta) -> {
meta.getEnchants().forEach((key, value) -> meta.removeEnchant(key));
return true;
});
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemBuilder.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemBuilder.java
new file mode 100644
index 00000000..e1de2451
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemBuilder.java
@@ -0,0 +1,88 @@
+package vg.civcraft.mc.civmodcore.inventory.items;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+public class ItemBuilder {
+
+ private Material material;
+ private ItemMeta meta;
+ private int amount;
+
+ private ItemBuilder() {
+ this.amount = 1;
+ }
+
+ /**
+ * @param material The material to set for this item.
+ * @return Returns this builder.
+ *
+ * @throws IllegalArgumentException Throws an IAE if the given material fails am
+ * {@link ItemUtils#isValidItemMaterial(Material)} test.
+ */
+ public ItemBuilder material(@Nonnull final Material material) {
+ if (!ItemUtils.isValidItemMaterial(material)) {
+ throw new IllegalArgumentException("That is not a valid item material!");
+ }
+ this.material = material;
+ return this;
+ }
+
+ /**
+ * @param The type to cast the item meta to.
+ * @param handler The item meta handler.
+ * @return Returns this builder.
+ *
+ * @throws NullPointerException Throws an NPE if a new item meta needs to be created, but the new meta is null.
+ * @throws ClassCastException Throws an CCE if the item meta cannot be cast to the inferred type.
+ */
+ @SuppressWarnings("unchecked")
+ public ItemBuilder meta(@Nonnull final Consumer handler) {
+ if (this.meta == null || !Bukkit.getItemFactory().isApplicable(this.meta, this.material)) {
+ this.meta = Objects.requireNonNull(
+ Bukkit.getItemFactory().getItemMeta(this.material),
+ "Tried to create an item meta for [" + this.material + "] but it returned null!");
+ }
+ handler.accept((T) this.meta);
+ return this;
+ }
+
+ /**
+ * @param amount The amount to set for this item.
+ * @return Returns this builder.
+ *
+ * @throws IllegalArgumentException Throws an IAE if the given amount is less than or equal to zero.
+ */
+ public ItemBuilder amount(final int amount) {
+ if (amount <= 0) {
+ throw new IllegalArgumentException("Item amount cannot be less than or equal to zero!");
+ }
+ this.amount = amount;
+ return this;
+ }
+
+ /**
+ * @return Returns a new ItemStack based on the builder.
+ */
+ public ItemStack build() {
+ final var item = new ItemStack(this.material, this.amount);
+ item.setItemMeta(this.meta);
+ return item;
+ }
+
+ /**
+ * Creates a new builder with the given material.
+ *
+ * @param material The material to set for the builder.
+ * @return Returns a new builder.
+ */
+ public static ItemBuilder builder(@Nonnull final Material material) {
+ return new ItemBuilder().material(material);
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemFactory.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemFactory.java
new file mode 100644
index 00000000..aaf11332
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemFactory.java
@@ -0,0 +1,70 @@
+package vg.civcraft.mc.civmodcore.inventory.items;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import javax.annotation.Nonnull;
+import lombok.experimental.UtilityClass;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftItemStack;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+/**
+ * Utility class that provides a means to construct {@link CraftItemStack CraftItemStacks} in lieu of
+ * {@link ItemStack#ItemStack(Material, int) new ItemStack(Material, Int)}. The reason being that
+ * {@link CraftItemStack CraftItemStacks} interact more efficiently with NMS than {@link ItemStack} does as the latter
+ * is entirely abstract in nature.
+ */
+@UtilityClass
+public class ItemFactory {
+
+ private static final Constructor CRAFT_ITEM_STACK_CONSTRUCTOR;
+
+ static {
+ final Constructor constructor;
+ try {
+ constructor = CraftItemStack.class.getDeclaredConstructor(
+ Material.class, Integer.TYPE, Short.TYPE, ItemMeta.class);
+ constructor.setAccessible(true);
+ }
+ catch (final NoSuchMethodException exception) {
+ throw new IllegalStateException(exception);
+ }
+ CRAFT_ITEM_STACK_CONSTRUCTOR = constructor;
+ }
+
+ /**
+ * Creates a new {@link CraftItemStack} based on a given material.
+ *
+ * @param material The material of the item.
+ * @return Returns a new instance of {@link CraftItemStack}.
+ */
+ public static ItemStack createItemStack(@Nonnull final Material material) {
+ return createItemStack(material, 1);
+ }
+
+ /**
+ * Creates a new {@link CraftItemStack} based on a given material.
+ *
+ * @param material The material of the item.
+ * @param amount The item amount.
+ * @return Returns a new instance of {@link CraftItemStack}.
+ */
+ public static ItemStack createItemStack(@Nonnull final Material material, final int amount) {
+ if (material == null || !material.isItem()) { // Ignore highlighter
+ throw new IllegalArgumentException("Material must be a valid item material!");
+ }
+ if (amount <= 0 || amount > material.getMaxStackSize()) {
+ throw new IllegalArgumentException("Must have a valid item amount");
+ }
+ try {
+ return CRAFT_ITEM_STACK_CONSTRUCTOR.newInstance(material, amount,
+ (short) 0, Bukkit.getItemFactory().getItemMeta(material));
+ }
+ catch (final InvocationTargetException | InstantiationException | IllegalAccessException exception) {
+ throw new RuntimeException("Could not construct item stack", exception);
+ }
+ }
+
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java
new file mode 100644
index 00000000..f596aacf
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java
@@ -0,0 +1,753 @@
+package vg.civcraft.mc.civmodcore.inventory.items;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagByte;
+import net.minecraft.nbt.NBTTagByteArray;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagDouble;
+import net.minecraft.nbt.NBTTagFloat;
+import net.minecraft.nbt.NBTTagInt;
+import net.minecraft.nbt.NBTTagIntArray;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTTagLong;
+import net.minecraft.nbt.NBTTagShort;
+import net.minecraft.nbt.NBTTagString;
+import org.apache.commons.collections4.CollectionUtils;
+import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.configuration.MemorySection;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.bukkit.inventory.meta.Repairable;
+import vg.civcraft.mc.civmodcore.nbt.NBTSerialization;
+import vg.civcraft.mc.civmodcore.utilities.CivLogger;
+import vg.civcraft.mc.civmodcore.utilities.MoreMath;
+
+/**
+ * Allows the storage and comparison of item stacks while ignoring their maximum possible stack sizes. This offers
+ * various tools to compare inventories, to store recipe costs or to specify setup costs. Take great care when dealing
+ * with item stacks with negative amounts, while this implementation should be consistent even with negative values,
+ * they create possibly unexpected results. For example an empty inventory/item map will seem to contain items when
+ * compared to a map containing negative values. Additionally this implementation allows durability "wild cards", if
+ * you specify -1 as durability it will count as any given durability. When working with multiple ItemMaps this will
+ * only work if all methods are executed on the instance containing items with a durability of -1.
+ *
+ * TODO: ItemMap is troubling because it manipulates and searches for items in ways that are not friendly to modern
+ * ways of cataloging items. For example, Bastion materials are all custom items, but they're only custom in the
+ * sense that they are named and lored.. but an Energizer will match with any other emerald unless you use any of
+ * the weirdly specific methods to weed out custom items. I think it would be better to refactor ItemMap to
+ * specifically support custom items, but I definitely need help in that regard. Or we can just keep it around
+ * for a little longer since they work fine enough and aren't in critical updates.
+ */
+public class ItemMap {
+
+ private static final CivLogger LOGGER = CivLogger.getLogger(ItemMap.class);
+
+ private final Object2IntMap items;
+ private int totalItems;
+
+ /**
+ * Empty constructor to create empty item map
+ */
+ public ItemMap() {
+ this.items = new Object2IntOpenHashMap<>(0);
+ this.items.defaultReturnValue(0);
+ this.totalItems = 0;
+ }
+
+ /**
+ * Constructor to create an ItemMap based on a single ItemStack
+ *
+ * @param item ItemStack to start with
+ */
+ public ItemMap(final ItemStack item) {
+ this.items = new Object2IntOpenHashMap<>(1);
+ this.items.defaultReturnValue(0);
+ this.totalItems = 0;
+ addItemStack(item);
+ }
+
+ /**
+ * Constructor to create an item map based on a collection of ItemStacks
+ *
+ * @param stacks Stacks to add to the map
+ */
+ public ItemMap(final Collection stacks) {
+ this.items = new Object2IntOpenHashMap<>(stacks.size());
+ this.items.defaultReturnValue(0);
+ addAll(stacks);
+ }
+
+ /**
+ * Constructor to create an item map based on the content of an inventory. The ItemMap will not be in sync with the
+ * inventory, it will only update if it's explicitly told to do so.
+ *
+ * @param inventory Inventory to base the item map on
+ */
+ public ItemMap(final Inventory inventory) {
+ this.items = new Object2IntOpenHashMap<>(inventory.getSize());
+ this.items.defaultReturnValue(0);
+ this.totalItems = 0;
+ update(inventory);
+ }
+
+ private static ItemStack INTERNAL_createKey(ItemStack item) {
+ item = item.asOne(); // this also clones the stack
+ ItemUtils.handleItemMeta(item, (Repairable meta) -> {
+ meta.setRepairCost(0);
+ return true;
+ });
+ return item;
+ }
+
+
+
+
+ //getAmount
+ //getTotalItemAmount
+ //getTotalUniqueItemAmount
+ //getStacksByMaterial
+ //getStacksByMaterialEnchant
+ //getStacksByLore
+ //getEntrySet
+ //getItemStackRepresentation
+ //getLoredItemCountRepresentation
+
+ //addAll
+ //addItemStack
+ //addItemAmount
+ //addToInventory
+ //addToEntrySet
+
+ //removeItemStack
+ //removeItemStackCompletely
+ //removeSafelyFrom
+
+ //fitsIn
+ //isContainedIn
+ //containedExactlyIn
+ //getMultiplesContainedIn
+ //multiplyContent
+
+ //createMapConformCopy
+ //enrichWithNBT
+ //clone
+ //merge
+ //update
+
+
+
+
+ /**
+ * Clones the given item stack, sets its amount to one and checks whether a stack equaling the created one exists
+ * in the item map. If yes the amount of the given stack (before the amount was set to 1) will be added to the
+ * current amount in the item map, if not a new entry in the map with the correct amount will be created.
+ *
+ * @param input ItemStack to insert
+ */
+ public void addItemStack(final ItemStack input) {
+ if (!ItemUtils.isValidItemIgnoringAmount(input)) {
+ return;
+ }
+ this.items.computeInt(INTERNAL_createKey(input), (key, amount) ->
+ amount == null ? input.getAmount() : amount + input.getAmount());
+ this.totalItems += input.getAmount();
+ }
+
+ /**
+ * Adds all the items contained in this instance to the given inventory
+ *
+ * @param inventory Inventory to add items to
+ */
+ public void addToInventory(Inventory inventory) {
+ for (ItemStack is : getItemStackRepresentation()) {
+ inventory.addItem(is);
+ }
+ }
+
+ /**
+ * Removes the given ItemStack from this map. Only the amount of the given ItemStack will be removed, not all of
+ * them. If the amount of the given item stack is bigger than the existing ones in this map, not more than the
+ * amount in this map will be removed
+ *
+ * @param input ItemStack to remove
+ */
+ public void removeItemStack(final ItemStack input) {
+ if (input.getAmount() <= 0) {
+ return;
+ }
+ final ItemStack key = INTERNAL_createKey(input);
+ if (key == null) {
+ return;
+ }
+ this.items.computeIntIfPresent(key, (_key, amount) -> (amount -= input.getAmount()) <= 0 ? null : amount);
+ }
+
+ /**
+ * Completely removes the given item stack of this item map, completely independent of its amount.
+ *
+ * @param input ItemStack to remove
+ */
+ public void removeItemStackCompletely(final ItemStack input) {
+ final ItemStack key = INTERNAL_createKey(input);
+ if (key != null) {
+ this.items.removeInt(key);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.items.hashCode();
+ }
+
+ /**
+ * Adds all the stacks given in the collection to this map
+ *
+ * @param stacks Stacks to add
+ */
+ public void addAll(Collection stacks) {
+ for (ItemStack is : stacks) {
+ if (is != null) {
+ addItemStack(is);
+ }
+ }
+ }
+
+ /**
+ * Merges the given item map into this instance
+ *
+ * @param im ItemMap to merge
+ */
+ public void merge(ItemMap im) {
+ for (Entry entry : im.getEntrySet()) {
+ addItemAmount(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public void update(final Inventory inventory) {
+ this.items.clear();
+ this.totalItems = 0;
+ for (int i = 0; i < inventory.getSize(); i++) {
+ final ItemStack item = inventory.getItem(i);
+ if (item != null) {
+ addItemStack(item);
+ }
+ }
+ }
+
+ public void addEntrySet(Set> entries) {
+ for (Entry entry : entries) {
+ addItemAmount(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Utility method, which has the amount of items to add as parameter.
+ *
+ * @param input ItemStack to sort into the map
+ * @param amount Amount associated with the given ItemStack
+ */
+ public void addItemAmount(ItemStack input, int amount) {
+ ItemStack copy = INTERNAL_createKey(input);
+ if (copy == null) {
+ return;
+ }
+ copy.setAmount(amount);
+ addItemStack(copy);
+ }
+
+ /**
+ * Gets a submap of this instance which contains all stacks with the same material as the given one and their
+ * respective amounts.
+ *
+ * @param m Material to search for
+ * @return New ItemMap with all ItemStack and their amount whose material
+ * matches the given one
+ */
+ public ItemMap getStacksByMaterial(Material m) {
+ ItemMap result = new ItemMap();
+ for (ItemStack is : items.keySet()) {
+ if (is.getType() == m) {
+ result.addItemAmount(is.clone(), items.get(is));
+ }
+ }
+ return result;
+ }
+
+ public ItemMap getStacksByMaterial(ItemStack is) {
+ return getStacksByMaterial(is.getType());
+ }
+
+ /**
+ * Gets a submap of this instance which contains all stacks with the same material and enchants as the given one
+ * and their respective amounts.
+ *
+ * @param m Material to search for
+ * @param enchants Enchants to search for
+ * @return New ItemMap with all ItemStack and their amount whose material and enchants matches the given one
+ */
+ public ItemMap getStacksByMaterialEnchants(Material m, Map enchants) {
+ ItemMap result = new ItemMap();
+ for (ItemStack is : items.keySet()) {
+ if (is.getType() == m && is.getItemMeta() != null && is.getItemMeta().getEnchants().equals(enchants)) {
+ result.addItemAmount(is.clone(), items.get(is));
+ }
+ }
+ return result;
+ }
+
+ public ItemMap getStacksByMaterialEnchants(ItemStack is) {
+ if (is.getItemMeta() != null) {
+ return getStacksByMaterialEnchants(is.getType(), is.getItemMeta().getEnchants());
+ } else {
+ return getStacksByMaterialEnchants(is.getType(), new HashMap<>());
+ }
+ }
+
+ /**
+ * Gets a submap of this instance which contains all stacks with the same lore as the given and their respective
+ * amount.
+ *
+ * @param lore Lore to search for
+ * @return New ItemMap with all ItemStacks and their amount whose lore matches the given one
+ */
+ public ItemMap getStacksByLore(final List lore) {
+ final boolean gaveLore = CollectionUtils.isNotEmpty(lore);
+ final ItemMap result = new ItemMap();
+ for (final ItemStack key : this.items.keySet()) {
+ if (!key.hasItemMeta()) {
+ continue;
+ }
+ final var keyMeta = key.getItemMeta();
+ if (gaveLore != keyMeta.hasLore()) {
+ continue;
+ }
+ final var keyLore = keyMeta.getLore();
+ if (!Objects.equals(lore, keyLore)) {
+ continue;
+ }
+ result.addItemAmount(key.clone(), this.items.getInt(key));
+ }
+ return result;
+ }
+
+ /**
+ * Gets how many items of the given stack are in this map. Be aware that if a stack doesnt equal with the given one,
+ * for example because of mismatched NBT tags, it wont be included in the result
+ *
+ * @param is Exact ItemStack to search for
+ * @return amount of items like the given stack in this map
+ */
+ public int getAmount(ItemStack is) {
+ ItemMap matSubMap = getStacksByMaterial(is);
+ int amount = 0;
+ for (Entry entry : matSubMap.getEntrySet()) {
+ ItemStack current = entry.getKey();
+ if (MetaUtils.areMetasEqual(is.getItemMeta(), current.getItemMeta())) {
+ amount += entry.getValue();
+ }
+ }
+ return amount;
+ }
+
+ /**
+ * @return How many items are stored in this map total
+ */
+ public int getTotalItemAmount() {
+ return totalItems;
+ }
+
+ /**
+ * @return How many unique items are stored in this map
+ */
+ public int getTotalUniqueItemAmount() {
+ return items.keySet().size();
+ }
+
+ @SuppressWarnings("deprecation")
+ public Set> getEntrySet() {
+ return this.items.entrySet();
+ }
+
+ /**
+ * Checks whether an inventory contains exactly what's described in this ItemMap
+ *
+ * @param i Inventory to compare
+ * @return True if the inventory is identical with this instance, false if not
+ */
+ public boolean containedExactlyIn(Inventory i) {
+ ItemMap invMap = new ItemMap(i);
+ for (Entry entry : getEntrySet()) {
+ if (!entry.getValue().equals(invMap.getAmount(entry.getKey()))) {
+ return false;
+ }
+ }
+ for (ItemStack is : i.getContents()) {
+ if (is == null) {
+ continue;
+ }
+ if (getStacksByMaterial(is).getTotalUniqueItemAmount() == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this instance is completely contained in the given inventory, which means every stack in this
+ * instance is also in the given inventory and the amount in the given inventory is either the same or bigger as in
+ * this instance
+ *
+ * @param i inventory to check
+ * @return true if this instance is completely contained in the given inventory, false if not
+ */
+ public boolean isContainedIn(Inventory i) {
+ ItemMap invMap = new ItemMap(i);
+ for (Entry entry : getEntrySet()) {
+ if (entry.getValue() > invMap.getAmount(entry.getKey())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder res = new StringBuilder();
+ for (ItemStack is : getItemStackRepresentation()) {
+ res.append(is.toString()).append(";");
+ }
+ return res.toString();
+ }
+
+ /**
+ * Checks how often this ItemMap is contained in the given ItemMap or how often this ItemMap could be removed from
+ * the given one before creating negative stacks
+ *
+ * @param i ItemMap to check
+ * @return How often this map is contained in the given one or Integer.MAX_VALUE if this instance is empty
+ */
+ public int getMultiplesContainedIn(Inventory i) {
+ ItemMap invMap = new ItemMap(i);
+ int res = Integer.MAX_VALUE;
+ for (Entry entry : getEntrySet()) {
+ int pulledAmount = invMap.getAmount(entry.getKey());
+ int multiples = pulledAmount / entry.getValue();
+ res = Math.min(res, multiples);
+ }
+ return res;
+ }
+
+ /**
+ * Multiplies the whole content of this instance by the given multiplier
+ *
+ * @param multiplier Multiplier to scale the amount of the contained items with
+ */
+ public void multiplyContent(double multiplier) {
+ totalItems = 0;
+ for (Entry entry : getEntrySet()) {
+ items.put(entry.getKey(), (int) (entry.getValue() * multiplier));
+ totalItems += (int) (entry.getValue() * multiplier);
+ }
+ }
+
+ /**
+ * Turns this item map into a list of ItemStacks, with amounts that do not surpass the maximum allowed stack size
+ * for each ItemStack
+ *
+ * @return List of stacksize conform ItemStacks
+ */
+ public List getItemStackRepresentation() {
+ List result = new ArrayList<>();
+ for (Entry entry : getEntrySet()) {
+ ItemStack is = entry.getKey();
+ Integer amount = entry.getValue();
+ while (amount != 0) {
+ ItemStack toAdd = is.clone();
+ int addAmount = Math.min(amount, is.getMaxStackSize());
+ toAdd.setAmount(addAmount);
+ // log.info("Adding {0} as ItemStack", toAdd.toString());
+ result.add(toAdd);
+ amount -= addAmount;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Clones this map
+ */
+ @Override
+ public ItemMap clone() {
+ ItemMap clone = new ItemMap();
+ for (Entry entry : getEntrySet()) {
+ clone.addItemAmount(entry.getKey(), entry.getValue());
+ }
+ return clone;
+ }
+
+ /**
+ * Checks whether this instance would completly fit into the given inventory
+ *
+ * @param i Inventory to check
+ * @return True if this ItemMap's item representation would completly fit in the inventory, false if not
+ */
+ public boolean fitsIn(Inventory i) {
+ int size;
+ if (i instanceof PlayerInventory) {
+ size = 36;
+ } else {
+ size = i.getSize();
+ }
+ ItemMap invCopy = new ItemMap();
+ for (ItemStack is : i.getStorageContents()) {
+ invCopy.addItemStack(is);
+ }
+ ItemMap instanceCopy = this.clone();
+ instanceCopy.merge(invCopy);
+ return instanceCopy.getItemStackRepresentation().size() <= size;
+ }
+
+ /**
+ * Instead of converting into many stacks of maximum size, this creates a stack with an amount of one for each
+ * entry and adds the total item amount and stack count as lore, which is needed to display larger ItemMaps in
+ * inventories
+ *
+ * @return UI representation of large ItemMap
+ */
+ public List