diff --git a/plugins/citadel-paper/src/main/java/vg/civcraft/mc/citadel/ReinforcementMultiplier.java b/plugins/citadel-paper/src/main/java/vg/civcraft/mc/citadel/ReinforcementMultiplier.java index ded4cfdfaa..a690974d33 100644 --- a/plugins/citadel-paper/src/main/java/vg/civcraft/mc/citadel/ReinforcementMultiplier.java +++ b/plugins/citadel-paper/src/main/java/vg/civcraft/mc/citadel/ReinforcementMultiplier.java @@ -5,7 +5,7 @@ import org.bukkit.NamespacedKey; import org.bukkit.Tag; import org.bukkit.inventory.ItemStack; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java index 686d17b7d0..c8e95f3cb8 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java @@ -5,6 +5,7 @@ import org.bukkit.entity.HumanEntity; import vg.civcraft.mc.civmodcore.chat.dialog.DialogManager; import vg.civcraft.mc.civmodcore.commands.ChunkMetaCommand; +import vg.civcraft.mc.civmodcore.commands.CommandHelpers; import vg.civcraft.mc.civmodcore.commands.CommandManager; import vg.civcraft.mc.civmodcore.commands.StatCommand; import vg.civcraft.mc.civmodcore.dao.DatabaseCredentials; @@ -73,6 +74,7 @@ public void onEnable() { // Register commands this.commands = new CommandManager(this); this.commands.init(); + CommandHelpers.enableCommandHelp(this.commands); this.commands.registerCommand(new ConfigCommand()); this.commands.registerCommand(new StatCommand()); this.commands.registerCommand(new ChunkMetaCommand()); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java index 98a907e1c8..07131f8d48 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/config/ConfigHelper.java @@ -15,7 +15,7 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem; import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; import vg.civcraft.mc.civmodcore.inventory.items.MaterialUtils; import vg.civcraft.mc.civmodcore.world.model.EllipseArea; diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/CustomItem.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/CustomItem.java deleted file mode 100644 index fe0be53b11..0000000000 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/CustomItem.java +++ /dev/null @@ -1,63 +0,0 @@ -package vg.civcraft.mc.civmodcore.inventory; - -import org.bukkit.NamespacedKey; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.persistence.PersistentDataType; -import org.bukkit.plugin.java.JavaPlugin; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; - -public class CustomItem { - - public static NamespacedKey CUSTOM_ITEM = new NamespacedKey(JavaPlugin.getPlugin(CivModCorePlugin.class), "custom_item"); - - private static final Map customItems = new HashMap<>(); - - public static void registerCustomItem(String key, ItemStack item) { - if (item == null || item.isEmpty()) { - return; - } - ItemMeta meta = item.getItemMeta(); - meta.getPersistentDataContainer().set(CUSTOM_ITEM, PersistentDataType.STRING, key); - item.setItemMeta(meta); - if (!customItems.containsKey(key)) { - customItems.put(key, item.clone()); - } - } - - public static ItemStack getCustomItem(String key) { - ItemStack itemStack = customItems.get(key); - return itemStack == null ? null : itemStack.clone(); - } - - public static boolean isCustomItem(ItemStack item) { - return item != null && !item.isEmpty() && item.getPersistentDataContainer().has(CUSTOM_ITEM); - } - - public static boolean isCustomItem(ItemStack item, String key) { - return item != null && !item.isEmpty() && key.equals(item.getPersistentDataContainer().get(CUSTOM_ITEM, PersistentDataType.STRING)); - } - - public static String getCustomItemKey(ItemStack item) { - if (isCustomItem(item)) { - String key = item.getPersistentDataContainer().get(CUSTOM_ITEM, PersistentDataType.STRING); - if (!customItems.containsKey(key)) { - return null; - } - return key; - } else { - return null; - } - } - - public static Set getKeys() { - return Collections.unmodifiableSet(customItems.keySet()); - } -} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java index 44cf23fd94..57b47f3e9c 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemMap.java @@ -22,7 +22,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem; /** * Allows the storage and comparison of item stacks while ignoring their maximum possible stack sizes. This offers diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemUtils.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemUtils.java index 0099ec61d1..76f2c2e65a 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemUtils.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/ItemUtils.java @@ -54,6 +54,7 @@ public static String getItemName(@Nullable final ItemStack item) { * @param item The item to check. * @return Returns true if the item can be interpreted as an empty slot. */ + @Contract("null -> true") public static boolean isEmptyItem(final ItemStack item) { return item == null || item.getType() == Material.AIR; } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItem.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItem.java new file mode 100644 index 0000000000..137141f0cb --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItem.java @@ -0,0 +1,94 @@ +package vg.civcraft.mc.civmodcore.inventory.items.custom; + +import java.util.Objects; +import java.util.function.Supplier; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils; + +/** + * Since Minecraft doesn't [yet] offer a means of registering custom item materials, this is the intended means of + * defining custom items in the meantime. Keep in mind that each custom item must correlate 1:1 with its key, ie, that + * custom-item keys should be treated like item materials. Do NOT use custom-item keys as custom-item categories, such + * as compacted items. You must always be able to receive the same item from the same key. + */ +public final class CustomItem { + public static NamespacedKey CUSTOM_ITEM_KEY = new NamespacedKey(JavaPlugin.getPlugin(CivModCorePlugin.class), "custom_item"); + + private static final Map> customItems = new HashMap<>(); + + public static void registerCustomItem( + final @NotNull String key, + final @NotNull Supplier<@NotNull ItemStack> factory + ) { + Objects.requireNonNull(key); + Objects.requireNonNull(factory); + customItems.putIfAbsent(key, factory); + } + + public static void registerCustomItem( + final @NotNull String key, + final @NotNull ItemStack template + ) { + registerCustomItem(key, template::clone); + } + + public static @Nullable ItemStack getCustomItem( + final @NotNull String key + ) { + if (customItems.get(Objects.requireNonNull(key)) instanceof final Supplier factory) { + final ItemStack item = factory.get(); + setCustomItemKey(item, key); + return item; + } + return null; + } + + /** + * Just remember that has-then-get is an anti-pattern: use {@link #isCustomItem(org.bukkit.inventory.ItemStack, String)} + * or {@link #getCustomItemKey(org.bukkit.inventory.ItemStack)} instead. + */ + public static boolean isCustomItem( + final ItemStack item + ) { + return !ItemUtils.isEmptyItem(item) && item.getPersistentDataContainer().has(CUSTOM_ITEM_KEY); + } + + public static boolean isCustomItem( + final ItemStack item, + final @NotNull String key + ) { + return key.equals(getCustomItemKey(item)); + } + + public static @Nullable String getCustomItemKey( + final ItemStack item + ) { + if (!ItemUtils.isEmptyItem(item)) { + return item.getPersistentDataContainer().get(CUSTOM_ITEM_KEY, PersistentDataType.STRING); + } + return null; + } + + @ApiStatus.Internal + public static void setCustomItemKey( + final @NotNull ItemStack item, + final @NotNull String key + ) { + item.editPersistentDataContainer((pdc) -> pdc.set(CUSTOM_ITEM_KEY, PersistentDataType.STRING, key)); + } + + public static @NotNull Set<@NotNull String> getRegisteredKeys() { + return Collections.unmodifiableSet(customItems.keySet()); + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItemsUpdater.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItemsUpdater.java new file mode 100644 index 0000000000..9be7eb38ce --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/custom/CustomItemsUpdater.java @@ -0,0 +1,161 @@ +package vg.civcraft.mc.civmodcore.inventory.items.custom; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater; +import vg.civcraft.mc.civmodcore.inventory.items.updater.listeners.DefaultItemUpdaterListeners; +import vg.civcraft.mc.civmodcore.inventory.items.updater.migrations.ItemMigration; +import vg.civcraft.mc.civmodcore.inventory.items.updater.migrations.ItemMigrations; + +public abstract class CustomItemsUpdater implements ItemUpdater { + protected final Logger logger = LoggerFactory.getLogger(getClass()); + protected final Map targets = new HashMap<>(); + + /** + * Retrieves a migration list for the given custom key, creating one if it didn't already exist. + */ + public @NotNull ItemMigrations getMigrationsFor( + final @NotNull String customKey + ) { + return getMigrationsFor(Objects.requireNonNull(customKey), true); + } + + /** + * Retrieves a migration list for the given custom key. + * + * @param createIfAbsent Whether to create the list if one doesn't already exist. + */ + @Contract("_, true -> !null") + public @Nullable ItemMigrations getMigrationsFor( + final @NotNull String customKey, + final boolean createIfAbsent + ) { + Objects.requireNonNull(customKey); + if (createIfAbsent) { + return this.targets.computeIfAbsent(customKey, CustomItemMigrations::new); + } + return this.targets.get(customKey); + } + + public void removeMigrationsFor( + final @NotNull String customKey + ) { + final ItemMigrations migrations = this.targets.remove(Objects.requireNonNull(customKey)); + if (migrations != null) { + migrations.clearMigrations(); + } + } + + public void clearMigrations() { + final List migrations = List.copyOf(this.targets.values()); + this.targets.clear(); + for (final ItemMigrations migration : migrations) { + migration.clearMigrations(); + } + } + + /** + *

This is how this class determines whether a given item is a custom item. You can just override this with a simple + * call to {@link vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem#getCustomItemKey(org.bukkit.inventory.ItemStack)}, + * but you may want to add additional logic to support legacy custom items.

+ * + *
{@code
+     * // Example
+     * @Override
+     * public @Nullable String getCustomKeyFrom(@NotNull ItemStack item) {
+     *     if (CustomItem.getCustomItem(item) instanceof final String customKey) {
+     *         return customKey;
+     *     }
+     *     if (item.getType() == Material.ENDER_EYE
+     *         && ExampleUtils.hasPlainDisplayName(item, "Player Essence")
+     *         && ExampleUtils.hasPlainLoreLine(item, "Activity reward used to fuel pearls")
+     *     ) {
+     *         return "player_essence";
+     *     }
+     *     if (item.getType() == Material.BONE_BLOCK
+     *         && ExampleUtils.hasPlainDisplayName(item, "City Bastion")
+     *         && ExampleUtils.hasPlainLoreLine(item, "City bastions block reinforcements and elytra")
+     *     ) {
+     *         return "city_bastion";
+     *     }
+     *     // etc
+     *     return null;
+     * }
+     * }
+ */ + protected abstract @Nullable String getCustomKeyFrom( + @NotNull ItemStack item + ); + + @Override + public boolean updateItem( + final @NotNull ItemStack item + ) { + final String customKey = getCustomKeyFrom(item); + if (customKey == null) { + return false; + } + final ItemMigrations migrations = this.targets.get(customKey); + if (migrations == null) { + return false; + } + return migrations.attemptMigration(item); + } + + // ============================================================ + // Defaults + // ============================================================ + + @Contract("_, _ -> param2") + public static @NotNull T init( + final @NotNull JavaPlugin plugin, + final @NotNull T updater + ) { + Bukkit.getPluginManager().registerEvents(DefaultItemUpdaterListeners.wrap(updater), plugin); + return updater; + } +} + +final class CustomItemMigrations extends ItemMigrations { + private static final NamespacedKey VERSION_KEY = new NamespacedKey(JavaPlugin.getPlugin(CivModCorePlugin.class), "item_version"); + + public CustomItemMigrations( + final @NotNull String customKey + ) { + // This is a deliberate 0th migration that ensures that any item + // being migrated has a custom-item key and item version. + this.migrations.put(0, (ItemMigration.OfItem) (item) -> { + CustomItem.setCustomItemKey(item, customKey); + setMigrationVersion(item, 0); + }); + } + + @Override + public @Nullable Integer getMigrationVersion( + final @NotNull ItemStack item + ) { + // This is a readonly PDC + return item.getPersistentDataContainer().get(VERSION_KEY, PersistentDataType.INTEGER); + } + + @Override + public void setMigrationVersion( + final @NotNull ItemStack item, + final int version + ) { + item.editPersistentDataContainer((pdc) -> pdc.set(VERSION_KEY, PersistentDataType.INTEGER, version)); + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/ItemUpdater.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/ItemUpdater.java new file mode 100644 index 0000000000..380f7babb2 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/ItemUpdater.java @@ -0,0 +1,36 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils; + +@FunctionalInterface +public interface ItemUpdater { + /** + * Updates the item however the implementer sees fit. + * + * @param item The item to update, which MUST NOT be an empty item, as determined by a {@link ItemUtils#isEmptyItem(ItemStack)} check. + * @return Whether the item was updated. + */ + boolean updateItem( + @NotNull ItemStack item + ); + + /** + * Updates all non-empty items (as determined by {@link ItemUtils#isEmptyItem(ItemStack)}) within a given inventory. + * @return Whether any of the items in the inventory were updated. + */ + static boolean updateInventory( + final @NotNull ItemUpdater updater, + final @NotNull Inventory inventory + ) { + boolean updated = false; + for (final ItemStack item : inventory) { + if (!ItemUtils.isEmptyItem(item)) { + updated |= updater.updateItem(item); + } + } + return updated; + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/DefaultItemUpdaterListeners.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/DefaultItemUpdaterListeners.java new file mode 100644 index 0000000000..f917ac36a9 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/DefaultItemUpdaterListeners.java @@ -0,0 +1,18 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.listeners; + +import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater; + +public interface DefaultItemUpdaterListeners extends UpdatePlayerItemsOnJoin, UpdateInventoryItemsOnOpen { + /** + * Pass your {@link vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater} implementation into this method + * and pass the result into {@link vg.civcraft.mc.civmodcore.ACivMod#registerListener(org.bukkit.event.Listener)} + * (or any other event-listener registration method), and your item updater will be called when players login, when + * inventories are opened, and when commanded. + */ + static @NotNull DefaultItemUpdaterListeners wrap( + final @NotNull ItemUpdater updater + ) { + return updater::updateItem; + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateContainerItemsOnLoad.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateContainerItemsOnLoad.java new file mode 100644 index 0000000000..406328352e --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateContainerItemsOnLoad.java @@ -0,0 +1,30 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.listeners; + +import io.papermc.paper.block.TileStateInventoryHolder; +import org.bukkit.Chunk; +import org.bukkit.block.BlockState; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; +import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater; + +public interface UpdateContainerItemsOnLoad extends ItemUpdater, Listener { + @EventHandler( + priority = EventPriority.LOWEST, + ignoreCancelled = true + ) + default void updateContainerItemsOnLoad( + final @NotNull ChunkLoadEvent event + ) { + final Chunk chunk = event.getChunk(); + for (final BlockState state : chunk.getTileEntities()) { + if (state instanceof final TileStateInventoryHolder holder) { + if (ItemUpdater.updateInventory(this, holder.getInventory())) { + holder.update(); + } + } + } + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateInventoryItemsOnOpen.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateInventoryItemsOnOpen.java new file mode 100644 index 0000000000..43c3141c34 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdateInventoryItemsOnOpen.java @@ -0,0 +1,25 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater; + +public interface UpdateInventoryItemsOnOpen extends ItemUpdater, Listener { + @EventHandler( + priority = EventPriority.LOWEST, + ignoreCancelled = true + ) + default void updateInventoryItemsOnOpen( + final @NotNull InventoryOpenEvent event + ) { + final Inventory inventory = event.getInventory(); + if (inventory.getHolder() == null) { + return; // GUI + } + ItemUpdater.updateInventory(this, inventory); + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdatePlayerItemsOnJoin.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdatePlayerItemsOnJoin.java new file mode 100644 index 0000000000..5e34c7532c --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/listeners/UpdatePlayerItemsOnJoin.java @@ -0,0 +1,28 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.listeners; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.inventory.items.updater.ItemUpdater; + +public interface UpdatePlayerItemsOnJoin extends ItemUpdater, Listener { + @EventHandler( + priority = EventPriority.LOWEST, + ignoreCancelled = true + ) + default void updatePlayerItemsOnJoin( + final @NotNull PlayerJoinEvent event + ) { + final Player player = event.getPlayer(); + if (player.hasPermission("cmc.debug")) { + // Do not auto-upgrade items if they have this debug permission as there may be reasons why they are + // carrying legacy items. + return; + } + ItemUpdater.updateInventory(this, player.getInventory()); + ItemUpdater.updateInventory(this, player.getEnderChest()); + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigration.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigration.java new file mode 100644 index 0000000000..4cf1ff8754 --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigration.java @@ -0,0 +1,58 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.migrations; + +import io.papermc.paper.datacomponent.DataComponentHolder; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +public sealed interface ItemMigration { + void doMigration( + @NotNull T item + ); + + non-sealed interface OfData extends ItemMigration { + + } + + non-sealed interface OfMeta extends ItemMigration { + + } + + /** + * This is a comparatively dangerous migration type. In the context of migrations, this is akin to using sudo, so + * only use it when you really NEED to: if you're just changing some meta or some data components, use a + * {@link OfData} or a {@link OfMeta} instead. + * + * @apiNote This is tagged as experimental and not given a convenience shortcut to drive the above point home. + */ + @ApiStatus.Experimental + non-sealed interface OfItem extends ItemMigration { + + } + + /** + * This is a comparatively dangerous migration type. In the context of migrations, this is akin to using sudo, so + * only use it when you really NEED to: if you're just changing some meta or some data components, use a + * {@link OfData} or a {@link OfMeta} instead. + * + * @apiNote This is tagged as experimental and not given a convenience shortcut to drive the above point home. + */ + @ApiStatus.Experimental + non-sealed interface OfNms extends ItemMigration { + + } + + static void migrate( + final @NotNull ItemStack item, + final @NotNull ItemMigration migration + ) { + switch (migration) { + case final ItemMigration.OfData dataMigration -> dataMigration.doMigration(item); + case final ItemMigration.OfMeta metaMigration -> item.editMeta(metaMigration::doMigration); + case final ItemMigration.OfItem itemMigration -> itemMigration.doMigration(item); + case final ItemMigration.OfNms nmsMigration -> nmsMigration.doMigration(CraftItemStack.unwrap(item)); + } + } +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigrations.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigrations.java new file mode 100644 index 0000000000..336324d7ec --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/items/updater/migrations/ItemMigrations.java @@ -0,0 +1,81 @@ +package vg.civcraft.mc.civmodcore.inventory.items.updater.migrations; + +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +public abstract class ItemMigrations { + protected final TreeMap> migrations = new TreeMap<>(Integer::compareTo); + + public void registerMigration( + final @Range(from = 1, to = Integer.MAX_VALUE) int migrationId, + final @NotNull ItemMigration updater + ) { + this.migrations.putIfAbsent(migrationId, Objects.requireNonNull(updater)); + } + + /** Convenience shortcut */ + public void registerDataMigration( + final @Range(from = 1, to = Integer.MAX_VALUE) int migrationId, + final @NotNull ItemMigration.OfData updater + ) { + registerMigration(migrationId, updater); + } + + /** Convenience shortcut */ + public void registerMetaMigration( + final @Range(from = 1, to = Integer.MAX_VALUE) int migrationId, + final @NotNull ItemMigration.OfMeta updater + ) { + registerMigration(migrationId, updater); + } + + public void clearMigrations() { + this.migrations.clear(); + } + + /** + * Retrieves the current migration version of a given item. + * + * @return Returns the current migration version, or null if it isn't set. + */ + public abstract @Nullable Integer getMigrationVersion( + @NotNull ItemStack item + ); + + /** + * Permanently stores the given migration version onto the given item in a way that + * {@link #getMigrationVersion(org.bukkit.inventory.ItemStack)} will detect. + */ + public abstract void setMigrationVersion( + @NotNull ItemStack item, + int version + ); + + /** + * Attempts to update the given item according to a series of pre-registered migrations. + * + * @param item The item to update, which MUST NOT be an empty item, as determined by a {@link vg.civcraft.mc.civmodcore.inventory.items.ItemUtils#isEmptyItem(ItemStack)} check. + * @return Whether the item was updated. + */ + public boolean attemptMigration( + final @NotNull ItemStack item + ) { + final Integer currentVersion = getMigrationVersion(item); + final Map> pendingMigrations = this.migrations.tailMap( + Objects.requireNonNullElse(currentVersion, 0), + currentVersion == null // Include the 0th migration if the version was missing + ); + boolean updated = false; + for (final Map.Entry> entry : pendingMigrations.entrySet()) { + ItemMigration.migrate(item, entry.getValue()); + setMigrationVersion(item, entry.getKey()); + updated = true; + } + return updated; + } +} diff --git a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java index db647f3c8d..3d38e7f1c6 100644 --- a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java +++ b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java @@ -48,7 +48,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitRunnable; import vg.civcraft.mc.civmodcore.config.ConfigHelper; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem; import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; import java.io.File; diff --git a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/commands/FMCommandManager.java b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/commands/FMCommandManager.java index dab586f6fd..9f01835f99 100644 --- a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/commands/FMCommandManager.java +++ b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/commands/FMCommandManager.java @@ -8,7 +8,7 @@ import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import vg.civcraft.mc.civmodcore.commands.CommandManager; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.custom.CustomItem; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,7 +41,7 @@ public void registerCompletions(@NotNull CommandCompletions