diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cc5b096..277d3c76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,8 +17,10 @@ jobs: uses: GroupeZ-dev/actions/.github/workflows/build.yml@main with: project-name: "zMenu" + java-version: '25' publish: true project-to-publish: "API:publish" + build-command: './gradlew build' discord-avatar-url: "https://minecraft-inventory-builder.com/storage/images/9UgcfGZyrmbVrXw5lbj5kXq6fW8F4nhwj6Cx4nVG.png" secrets: WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore index 3506388e..9efa5ac7 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ opencode.json CLAUDE.md .claude/settings.local.json build-and-deploy.sh +GEMINI.md + +TODO.md \ No newline at end of file diff --git a/API/build.gradle.kts b/API/build.gradle.kts index c4ee9f05..261654a0 100644 --- a/API/build.gradle.kts +++ b/API/build.gradle.kts @@ -1,9 +1,9 @@ plugins { - id("re.alwyn974.groupez.publish") version "1.0.0" + alias(libs.plugins.groupez.publish) } dependencies{ - implementation("net.kyori:adventure-api:4.25.0") + implementation(libs.adventure.api) } rootProject.extra.properties["sha"]?.let { sha -> diff --git a/API/src/main/java/fr/maxlego08/menu/api/BedrockManager.java b/API/src/main/java/fr/maxlego08/menu/api/BedrockManager.java index 4acb65d4..e679652d 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/BedrockManager.java +++ b/API/src/main/java/fr/maxlego08/menu/api/BedrockManager.java @@ -2,6 +2,7 @@ import fr.maxlego08.menu.api.exceptions.DialogException; import fr.maxlego08.menu.api.exceptions.InventoryException; +import fr.maxlego08.menu.api.inventory.bedrock.BedrockInventory; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; diff --git a/API/src/main/java/fr/maxlego08/menu/api/DialogManager.java b/API/src/main/java/fr/maxlego08/menu/api/DialogManager.java index 5c93dced..eec681c3 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/DialogManager.java +++ b/API/src/main/java/fr/maxlego08/menu/api/DialogManager.java @@ -3,6 +3,7 @@ import fr.maxlego08.menu.api.configuration.ConfigManagerInt; import fr.maxlego08.menu.api.exceptions.DialogException; import fr.maxlego08.menu.api.exceptions.InventoryException; +import fr.maxlego08.menu.api.inventory.dialog.DialogInventory; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; diff --git a/API/src/main/java/fr/maxlego08/menu/api/Inventory.java b/API/src/main/java/fr/maxlego08/menu/api/Inventory.java index 83f8db16..936f1f72 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/Inventory.java +++ b/API/src/main/java/fr/maxlego08/menu/api/Inventory.java @@ -15,6 +15,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -34,7 +35,14 @@ public interface Inventory { * Returns the size of the inventory. * * @return The size of the inventory. This value is the number of slots in the inventory. + * @deprecated Use: + *
+     * if (inventory instanceof ChestInventory chestInventory) {
+     *     int size = chestInventory.size();
+     * }
+     * 
*/ + @Deprecated(since = "1.1.1.5") int size(); /** @@ -49,19 +57,33 @@ public interface Inventory { * * @return The name of the inventory, translated to the player's language. */ - String getName(Player player, InventoryEngine InventoryEngine, Placeholders placeholders); + String getName(@NotNull Player player, InventoryEngine InventoryEngine, Placeholders placeholders); /** * Returns the type of the inventory. * * @return The type of the inventory. + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     InventoryType type = containerInventory.getType();
+     * }
+     * 
*/ + @Deprecated(since = "1.1.1.5") InventoryType getType(); /** * Indicates whether the inventory prevents the player from picking up items from the ground. * @return true if item pickup is blocked while this inventory is open, false otherwise. + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     boolean shouldCancel = containerInventory.shouldCancelItemPickup();
+     * }
+     * 
*/ + @Deprecated(since = "1.1.1.5") boolean shouldCancelItemPickup(); /** @@ -159,7 +181,14 @@ public interface Inventory { * Returns the item stack used to fill empty slots in the inventory. * * @return The fill item stack. + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     MenuItemStack fillItemStack = containerInventory.getFillItemStack();
+     * }
+     * 
*/ + @Deprecated(since = "1.1.1.5") MenuItemStack getFillItemStack(); /** @@ -180,7 +209,14 @@ public interface Inventory { * Determines whether the player's inventory should be cleared upon closing the inventory. * * @return True if the player's inventory should be cleaned, false otherwise. + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     boolean shouldClear = containerInventory.clearInventory();
+     * }
+     * 
*/ + @Deprecated(since = "1.1.1.5") boolean cleanInventory(); /** @@ -222,16 +258,55 @@ public interface Inventory { */ String getTargetPlayerNamePlaceholder(); + + /** + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     containerInventory.setTitleAnimation(animation);
+     * }
+     * 
+ */ + @Deprecated(since = "1.1.1.5") void setTitleAnimation(TitleAnimation load); + /** + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     TitleAnimation animation = containerInventory.getTitleAnimation();
+     * }
+     * 
+ */ + @Deprecated(since = "1.1.1.5") + @Nullable TitleAnimation getTitleAnimation(); List getOpenActions(); List getCloseActions(); + /** + * + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     ClearInvType clearInvType = containerInventory.getClearInvType();
+     * }
+     * 
+ */ ClearInvType getClearInvType(); + /** + * + * @deprecated Use: + *
+     * if (inventory instanceof ContainerInventory containerInventory) {
+     *     boolean isEnabled = containerInventory.isClickLimiterEnabled();
+     * }
+     * 
+ */ + @Deprecated(since = "1.1.1.5") boolean isClickLimiterEnabled(); @Nullable diff --git a/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java b/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java index 6c27702e..c1effa37 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java +++ b/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java @@ -1,6 +1,7 @@ package fr.maxlego08.menu.api; import fr.maxlego08.menu.api.engine.BaseInventory; +import fr.maxlego08.menu.api.engine.InventoryEngine; import fr.maxlego08.menu.api.engine.ItemButton; import org.bukkit.entity.Player; @@ -25,4 +26,8 @@ default void onInventoryClose(Player player, BaseInventory baseInventory){ default void onButtonClick(Player player, ItemButton button){ } + + default void onInventorySwitch(Player player, InventoryEngine oldInventoryEngine, InventoryEngine newInventoryEngine) { + + } } diff --git a/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java b/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java index 91b3ea88..ab546772 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java +++ b/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java @@ -598,6 +598,8 @@ public interface InventoryManager extends Listener { */ MenuItemStack loadItemStack(File file, String path, Map map); + MenuItemStack loadItemStack(File file, Map map); + /** * Provides access to the pagination manager for handling paginated content in inventories. * diff --git a/API/src/main/java/fr/maxlego08/menu/api/MenuItemStack.java b/API/src/main/java/fr/maxlego08/menu/api/MenuItemStack.java index e4fd7eeb..7dbbabeb 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/MenuItemStack.java +++ b/API/src/main/java/fr/maxlego08/menu/api/MenuItemStack.java @@ -4,6 +4,7 @@ import fr.maxlego08.menu.api.attribute.AttributeWrapper; import fr.maxlego08.menu.api.context.BuildContext; import fr.maxlego08.menu.api.context.MenuItemStackContext; +import fr.maxlego08.menu.api.enums.AmountType; import fr.maxlego08.menu.api.enums.MenuItemRarity; import fr.maxlego08.menu.api.itemstack.*; import fr.maxlego08.menu.api.utils.LoreType; @@ -82,6 +83,13 @@ public interface MenuItemStack extends MenuItemStackContext { */ void setAmount(String amount); + /** + * Updates the configured amount type for the item stack. + * + * @param amountType the amount type to set. + */ + void setAmountType(AmountType amountType); + /** * Defines the skull texture URL for this item stack. * @@ -401,6 +409,12 @@ public interface MenuItemStack extends MenuItemStackContext { */ void setItemModel(NamespacedKey itemModel); + /** + * Sets the item model identifier using a string value. + * @param itemModel the item model identifier to set, as a string. + */ + void setItemModel(String itemModel); + /** * Sets the equipped model identifier used for wearable items. * diff --git a/API/src/main/java/fr/maxlego08/menu/api/animation/TitleAnimation.java b/API/src/main/java/fr/maxlego08/menu/api/animation/TitleAnimation.java index 42795c30..79e5c42d 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/animation/TitleAnimation.java +++ b/API/src/main/java/fr/maxlego08/menu/api/animation/TitleAnimation.java @@ -22,5 +22,5 @@ public interface TitleAnimation { * @return A PlayerTitleAnimation if started, otherwise null. */ @Nullable - PlayerTitleAnimation playTitleAnimation(@NotNull MenuPlugin plugin, int containerId, @NotNull InventoryType type, int size, Object... args); + PlayerTitleAnimation playTitleAnimation(@NotNull MenuPlugin plugin, int containerId, @NotNull InventoryType type, int size, @NotNull Object... args); } diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoListener.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoListener.java new file mode 100644 index 00000000..e3a0c549 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoListener.java @@ -0,0 +1,11 @@ +package fr.maxlego08.menu.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AutoListener { +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoRuleLoader.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoRuleLoader.java new file mode 100644 index 00000000..6978b461 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/AutoRuleLoader.java @@ -0,0 +1,11 @@ +package fr.maxlego08.menu.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AutoRuleLoader { +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/ComponentLoader.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/ComponentLoader.java new file mode 100644 index 00000000..7f3ba552 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/ComponentLoader.java @@ -0,0 +1,11 @@ +package fr.maxlego08.menu.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ComponentLoader { +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/PaperOnly.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/PaperOnly.java new file mode 100644 index 00000000..51913f3d --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/PaperOnly.java @@ -0,0 +1,11 @@ +package fr.maxlego08.menu.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PaperOnly { +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/SinceVersion.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/SinceVersion.java new file mode 100644 index 00000000..2d2a3c3c --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/SinceVersion.java @@ -0,0 +1,15 @@ +package fr.maxlego08.menu.api.annotations; + +import org.jetbrains.annotations.NotNull; + +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 SinceVersion { + @NotNull + String value(); +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/SpigotOnly.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/SpigotOnly.java new file mode 100644 index 00000000..986a8f68 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/SpigotOnly.java @@ -0,0 +1,12 @@ +package fr.maxlego08.menu.api.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SpigotOnly { +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/annotations/UntilVersion.java b/API/src/main/java/fr/maxlego08/menu/api/annotations/UntilVersion.java new file mode 100644 index 00000000..b5739a7b --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/annotations/UntilVersion.java @@ -0,0 +1,16 @@ +package fr.maxlego08.menu.api.annotations; + +import org.jetbrains.annotations.NotNull; + +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 UntilVersion { + + @NotNull + String value(); +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/attribute/AttributeEntry.java b/API/src/main/java/fr/maxlego08/menu/api/attribute/AttributeEntry.java index fc41dc6b..c2929731 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/attribute/AttributeEntry.java +++ b/API/src/main/java/fr/maxlego08/menu/api/attribute/AttributeEntry.java @@ -2,5 +2,6 @@ import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; +import org.jetbrains.annotations.NotNull; -public record AttributeEntry(Attribute attribute, AttributeModifier modifier) {} \ No newline at end of file +public record AttributeEntry(@NotNull Attribute attribute,@NotNull AttributeModifier modifier) {} \ No newline at end of file diff --git a/API/src/main/java/fr/maxlego08/menu/api/button/Button.java b/API/src/main/java/fr/maxlego08/menu/api/button/Button.java index 302e0bae..0f174dda 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/button/Button.java +++ b/API/src/main/java/fr/maxlego08/menu/api/button/Button.java @@ -3,6 +3,7 @@ import fr.maxlego08.menu.api.Inventory; import fr.maxlego08.menu.api.MenuItemStack; import fr.maxlego08.menu.api.MenuPlugin; +import fr.maxlego08.menu.api.engine.AnvilInventoryEngine; import fr.maxlego08.menu.api.engine.InventoryEngine; import fr.maxlego08.menu.api.engine.Pagination; import fr.maxlego08.menu.api.players.DataManager; @@ -13,6 +14,7 @@ import fr.maxlego08.menu.api.sound.SoundOption; import fr.maxlego08.menu.api.utils.OpenLink; import fr.maxlego08.menu.api.utils.Placeholders; +import fr.maxlego08.menu.api.utils.TextChange; import fr.maxlego08.menu.zcore.utils.PerformanceDebug; import org.bukkit.Material; import org.bukkit.OfflinePlayer; @@ -212,6 +214,8 @@ public void onLeftClick(@NotNull Player player, @NotNull InventoryClickEvent eve public void onRightClick(@NotNull Player player, @NotNull InventoryClickEvent event, @NotNull InventoryEngine inventory, int slot) { } + public void onAnvilTextChange(@NotNull Player player, AnvilInventoryEngine inventoryEngine, TextChange textChange, Placeholders placeholders) {} + /** * Called when the middle mouse button is clicked * diff --git a/API/src/main/java/fr/maxlego08/menu/api/command/Command.java b/API/src/main/java/fr/maxlego08/menu/api/command/Command.java index a5c081ad..7322d983 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/command/Command.java +++ b/API/src/main/java/fr/maxlego08/menu/api/command/Command.java @@ -1,6 +1,7 @@ package fr.maxlego08.menu.api.command; import fr.maxlego08.menu.api.requirement.Action; +import fr.maxlego08.menu.api.requirement.Requirement; import org.bukkit.plugin.Plugin; import java.io.File; @@ -102,6 +103,12 @@ public interface Command { */ List subCommands(); + /** + * Gets the list of requirements that must be met for the command's actions to be executed. + * @return The list of requirements that must be met for the command's actions to be executed. + */ + List actions_requirements(); + /** * Gets the message to display when the player does not have the required permission. * diff --git a/API/src/main/java/fr/maxlego08/menu/api/context/MenuItemStackContext.java b/API/src/main/java/fr/maxlego08/menu/api/context/MenuItemStackContext.java index e4b94c6e..1f21da1d 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/context/MenuItemStackContext.java +++ b/API/src/main/java/fr/maxlego08/menu/api/context/MenuItemStackContext.java @@ -3,6 +3,7 @@ import fr.maxlego08.menu.api.InventoryManager; import fr.maxlego08.menu.api.attribute.AttributeMergeStrategy; import fr.maxlego08.menu.api.attribute.AttributeWrapper; +import fr.maxlego08.menu.api.enums.AmountType; import fr.maxlego08.menu.api.enums.MenuItemRarity; import fr.maxlego08.menu.api.itemstack.*; import fr.maxlego08.menu.api.utils.LoreType; @@ -62,6 +63,13 @@ public interface MenuItemStackContext { */ String getAmount(); + /** + * Retrieves the amount type for the item stack. + * + * @return the amount type. + */ + AmountType getAmountType(); + /** * Returns the skull texture URL associated with this item stack, when applicable. * @@ -329,6 +337,12 @@ public interface MenuItemStackContext { */ NamespacedKey getItemModel(); + /** + * Retrieves the item model identifier as a string, if applicable. + * @return the item model identifier as a string, or {@code null} if not set or not applicable. + */ + String getItemModelString(); + /** * Retrieves the equipped model identifier used for wearable items. * diff --git a/API/src/main/java/fr/maxlego08/menu/api/engine/AnvilInventoryEngine.java b/API/src/main/java/fr/maxlego08/menu/api/engine/AnvilInventoryEngine.java new file mode 100644 index 00000000..cf82769a --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/engine/AnvilInventoryEngine.java @@ -0,0 +1,7 @@ +package fr.maxlego08.menu.api.engine; + +import org.jetbrains.annotations.NotNull; + +public interface AnvilInventoryEngine extends InventoryEngine { + @NotNull String getCurrentText(); +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java b/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java index b0fdeef9..04ddd2ef 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java +++ b/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java @@ -13,7 +13,7 @@ public class ItemButton { private final int slot; - private final ItemStack displayItem; + private ItemStack displayItem; private final Map> onClickType = new HashMap<>(); private final boolean inPlayerInventory; private final BaseInventory baseInventory; @@ -60,6 +60,15 @@ public ItemButton setLeftClick(@NotNull Consumer onLeftClic return this; } + public void updateDisplayItem(@NotNull ItemStack displayItem) { + this.displayItem = displayItem; + if (this.inPlayerInventory) { + this.baseInventory.getPlayerInventoryItems().put(this.slot, this); + } else { + this.baseInventory.getSpigotInventory().setItem(this.slot, displayItem); + } + } + @Contract("_-> this") @NotNull public ItemButton setShiftLeftClick(@NotNull Consumer onLeftClick) { diff --git a/API/src/main/java/fr/maxlego08/menu/api/enums/AmountType.java b/API/src/main/java/fr/maxlego08/menu/api/enums/AmountType.java new file mode 100644 index 00000000..a6aac6bf --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/enums/AmountType.java @@ -0,0 +1,9 @@ +package fr.maxlego08.menu.api.enums; + +public enum AmountType { + + SET, + ADD, + REMOVE, + +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/enums/ItemVerification.java b/API/src/main/java/fr/maxlego08/menu/api/enums/ItemVerification.java index b616d5a5..e952f076 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/enums/ItemVerification.java +++ b/API/src/main/java/fr/maxlego08/menu/api/enums/ItemVerification.java @@ -3,6 +3,6 @@ public enum ItemVerification { MODELID, - SIMILAR; + SIMILAR } diff --git a/API/src/main/java/fr/maxlego08/menu/api/inventory/AnvilInventory.java b/API/src/main/java/fr/maxlego08/menu/api/inventory/AnvilInventory.java new file mode 100644 index 00000000..32a0b4b0 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/inventory/AnvilInventory.java @@ -0,0 +1,13 @@ +package fr.maxlego08.menu.api.inventory; + +import fr.maxlego08.menu.api.requirement.Requirement; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface AnvilInventory extends ContainerInventory { + + @NotNull + List getRenameRequirements(); + +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/inventory/ChestInventory.java b/API/src/main/java/fr/maxlego08/menu/api/inventory/ChestInventory.java new file mode 100644 index 00000000..23bf00a6 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/inventory/ChestInventory.java @@ -0,0 +1,8 @@ +package fr.maxlego08.menu.api.inventory; + +public interface ChestInventory extends ContainerInventory { + + + int size(); + +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/inventory/ContainerInventory.java b/API/src/main/java/fr/maxlego08/menu/api/inventory/ContainerInventory.java new file mode 100644 index 00000000..9d6236e7 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/inventory/ContainerInventory.java @@ -0,0 +1,38 @@ +package fr.maxlego08.menu.api.inventory; + +import fr.maxlego08.menu.api.Inventory; +import fr.maxlego08.menu.api.MenuItemStack; +import fr.maxlego08.menu.api.animation.TitleAnimation; +import fr.maxlego08.menu.api.utils.ClearInvType; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.Nullable; + +public interface ContainerInventory extends Inventory { + + // --- Title Animation --- + + void setTitleAnimation(TitleAnimation animation); + + @Nullable + TitleAnimation getTitleAnimation(); + + // -- Clean Inventory --- + + boolean cleanInventory(); + + default boolean clearInventory() { + return cleanInventory(); + } + + + @Nullable + MenuItemStack getFillItemStack(); + + boolean shouldCancelItemPickup(); + + InventoryType getType(); + + boolean isClickLimiterEnabled(); + + ClearInvType getClearInvType(); +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/BedrockInventory.java b/API/src/main/java/fr/maxlego08/menu/api/inventory/bedrock/BedrockInventory.java similarity index 95% rename from API/src/main/java/fr/maxlego08/menu/api/BedrockInventory.java rename to API/src/main/java/fr/maxlego08/menu/api/inventory/bedrock/BedrockInventory.java index dc106101..f8e350b9 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/BedrockInventory.java +++ b/API/src/main/java/fr/maxlego08/menu/api/inventory/bedrock/BedrockInventory.java @@ -1,5 +1,7 @@ -package fr.maxlego08.menu.api; +package fr.maxlego08.menu.api.inventory.bedrock; +import fr.maxlego08.menu.api.Inventory; +import fr.maxlego08.menu.api.MenuItemStack; import fr.maxlego08.menu.api.button.Button; import fr.maxlego08.menu.api.button.bedrock.BedrockButton; import fr.maxlego08.menu.api.button.dialogs.InputButton; @@ -97,7 +99,7 @@ default InventoryResult openInventory(Player player, InventoryEngine InventoryEn } @Override - default void postOpenInventory(Player player, InventoryEngine InventoryEngine) {}; + default void postOpenInventory(Player player, InventoryEngine InventoryEngine) {} @Override default void closeInventory(Player player, InventoryEngine InventoryEngine) {} diff --git a/API/src/main/java/fr/maxlego08/menu/api/DialogInventory.java b/API/src/main/java/fr/maxlego08/menu/api/inventory/dialog/DialogInventory.java similarity index 95% rename from API/src/main/java/fr/maxlego08/menu/api/DialogInventory.java rename to API/src/main/java/fr/maxlego08/menu/api/inventory/dialog/DialogInventory.java index e81be6c1..772b8048 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/DialogInventory.java +++ b/API/src/main/java/fr/maxlego08/menu/api/inventory/dialog/DialogInventory.java @@ -1,5 +1,7 @@ -package fr.maxlego08.menu.api; +package fr.maxlego08.menu.api.inventory.dialog; +import fr.maxlego08.menu.api.Inventory; +import fr.maxlego08.menu.api.MenuItemStack; import fr.maxlego08.menu.api.button.Button; import fr.maxlego08.menu.api.button.dialogs.BodyButton; import fr.maxlego08.menu.api.button.dialogs.InputButton; @@ -12,16 +14,17 @@ import fr.maxlego08.menu.api.utils.dialogs.record.ActionButtonRecord; import fr.maxlego08.menu.api.utils.dialogs.record.ZDialogInventoryBuild; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -public interface DialogInventory extends Inventory{ +public interface DialogInventory extends Inventory { + + @NotNull String getExternalTitle(); boolean canCloseWithEscape(); @@ -201,7 +204,7 @@ default InventoryResult openInventory(Player player, InventoryEngine InventoryEn } @Override - default void postOpenInventory(Player player, InventoryEngine InventoryEngine) {}; + default void postOpenInventory(Player player, InventoryEngine InventoryEngine) {} @Override default void closeInventory(Player player, InventoryEngine InventoryEngine) {} diff --git a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/AttackRangeComponent.java b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/AttackRangeComponent.java index c8ead34f..bdc139eb 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/AttackRangeComponent.java +++ b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/AttackRangeComponent.java @@ -1,24 +1,17 @@ package fr.maxlego08.menu.api.itemstack.components; -import fr.maxlego08.menu.api.context.BuildContext; import fr.maxlego08.menu.api.itemstack.ItemComponent; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -@SuppressWarnings("unused") -public class AttackRangeComponent extends ItemComponent { +public abstract class AttackRangeComponent extends ItemComponent { + protected final float minReach; + protected final float maxReach; + protected final float minCreativeReach; + protected final float maxCreativeReach; + protected final float hitboxMargin; + protected final float mobFactor; - private final float minReach; - private final float maxReach; - private final float minCreativeReach; - private final float maxCreativeReach; - private final float hitboxMargin; - private final float mobFactor; - public AttackRangeComponent(float minReach, float maxReach, float minCreativeReach, float maxCreativeReach, float hitboxMargin, float mobFactor) { + protected AttackRangeComponent(float minReach, float maxReach, float minCreativeReach, float maxCreativeReach, float hitboxMargin, float mobFactor) { this.minReach = minReach; this.maxReach = maxReach; this.minCreativeReach = minCreativeReach; @@ -50,19 +43,4 @@ public float getHitboxMargin() { public float getMobFactor() { return this.mobFactor; } - - @Override - public void apply(@NotNull BuildContext context, @NotNull ItemStack itemStack, @Nullable Player player) { - ItemMeta itemMeta = itemStack.getItemMeta(); - if (itemMeta == null) return; - org.bukkit.inventory.meta.components.AttackRangeComponent attackRange = itemMeta.getAttackRange(); - attackRange.setMinReach(this.minReach); - attackRange.setMaxReach(this.maxReach); - attackRange.setMinCreativeReach(this.minCreativeReach); - attackRange.setMaxCreativeReach(this.maxCreativeReach); - attackRange.setHitboxMargin(this.hitboxMargin); - attackRange.setMobFactor(this.mobFactor); - itemStack.setItemMeta(itemMeta); - } - } diff --git a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ItemModelComponent.java b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ItemModelComponent.java index 9d9a7ea3..a333bfe5 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ItemModelComponent.java +++ b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ItemModelComponent.java @@ -2,6 +2,7 @@ import fr.maxlego08.menu.api.context.BuildContext; import fr.maxlego08.menu.api.itemstack.ItemComponent; +import fr.maxlego08.menu.api.placeholder.Placeholder; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -11,13 +12,13 @@ @SuppressWarnings("unused") public class ItemModelComponent extends ItemComponent { - private final @NotNull NamespacedKey itemModel; + private final @NotNull String itemModel; - public ItemModelComponent(@NotNull NamespacedKey itemModel) { + public ItemModelComponent(@NotNull String itemModel) { this.itemModel = itemModel; } - public @NotNull NamespacedKey getItemModel() { + public @NotNull String getItemModel() { return this.itemModel; } @@ -26,7 +27,21 @@ public void apply(@NotNull BuildContext context, @NotNull ItemStack itemStack, @ ItemMeta itemMeta = itemStack.getItemMeta(); if (itemMeta != null) { - itemMeta.setItemModel(this.itemModel); + String s = Placeholder.Placeholders.getPlaceholder().setPlaceholders(player, context.getPlaceholders().parse(this.itemModel)); + NamespacedKey finalItemModel = null; + try { + String[] split = s.split(":", 2); + if (split.length == 2) { + finalItemModel = new NamespacedKey(split[0], split[1]); + } else { + finalItemModel = NamespacedKey.minecraft(s); + } + } catch (Exception _) { + } + + if (finalItemModel != null) { + itemMeta.setItemModel(finalItemModel); + } itemStack.setItemMeta(itemMeta); } diff --git a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/MaxStackSizeComponent.java b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/MaxStackSizeComponent.java index 55f612f7..b019a9d5 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/MaxStackSizeComponent.java +++ b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/MaxStackSizeComponent.java @@ -1,16 +1,9 @@ package fr.maxlego08.menu.api.itemstack.components; -import fr.maxlego08.menu.api.context.BuildContext; import fr.maxlego08.menu.api.itemstack.ItemComponent; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -@SuppressWarnings("unused") -public class MaxStackSizeComponent extends ItemComponent { - private final int maxStackSize; +public abstract class MaxStackSizeComponent extends ItemComponent { + protected final int maxStackSize; public MaxStackSizeComponent(int maxStackSize) { this.maxStackSize = maxStackSize; @@ -19,14 +12,4 @@ public MaxStackSizeComponent(int maxStackSize) { public int getMaxStackSize() { return this.maxStackSize; } - - @Override - public void apply(@NotNull BuildContext context, @NotNull ItemStack itemStack, @Nullable Player player) { - ItemMeta itemMeta = itemStack.getItemMeta(); - if (itemMeta != null) { - itemMeta.setMaxStackSize(this.maxStackSize); - itemStack.setItemMeta(itemMeta); - } - } } - diff --git a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ToolComponent.java b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ToolComponent.java index 0fe8f8ec..24eb46df 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ToolComponent.java +++ b/API/src/main/java/fr/maxlego08/menu/api/itemstack/components/ToolComponent.java @@ -68,13 +68,13 @@ public void apply(@NotNull BuildContext context, @NotNull ItemStack itemStack, @ tool.setDamagePerBlock(this.damagePerBlock); tool.setCanDestroyBlocksInCreative(this.canDestroyBlocksInCreative); for (ZToolRule rule : this.materialRules) { - tool.addRule(rule.getData(), rule.getSpeed(), rule.isCorrectForDrop()); + tool.addRule(rule.data(), rule.speed(), rule.correctForDrop()); } for (ZToolRule> rule : this.materialGroups) { - tool.addRule(rule.getData(), rule.getSpeed(), rule.isCorrectForDrop()); + tool.addRule(rule.data(), rule.speed(), rule.correctForDrop()); } for (ZToolRule> rule : this.tagRules) { - tool.addRule(rule.getData(), rule.getSpeed(), rule.isCorrectForDrop()); + tool.addRule(rule.data(), rule.speed(), rule.correctForDrop()); } itemStack.setItemMeta(itemMeta); } diff --git a/API/src/main/java/fr/maxlego08/menu/api/loader/ButtonLoader.java b/API/src/main/java/fr/maxlego08/menu/api/loader/ButtonLoader.java index bb2707c5..d3b26ef2 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/loader/ButtonLoader.java +++ b/API/src/main/java/fr/maxlego08/menu/api/loader/ButtonLoader.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.IntStream; @@ -31,9 +32,16 @@ public abstract class ButtonLoader { protected final Plugin plugin; protected final String name; - public ButtonLoader(@NotNull Plugin plugin,@NotNull String name) { + protected final List aliases = new ArrayList<>(); + + public ButtonLoader(@NotNull Plugin plugin,@NotNull String name, String... aliases) { this.plugin = plugin; this.name = name; + this.aliases.addAll(List.of(aliases)); + } + + public ButtonLoader(@NotNull Plugin plugin,@NotNull String name) { + this(plugin, name, new String[0]); } /** @@ -78,6 +86,17 @@ public String getName() { return this.name; } + /** + * Gets the list of aliases for the button. + * + * @return An unmodifiable list of aliases. + */ + @Contract(pure = true) + @NotNull + public List getAliases() { + return Collections.unmodifiableList(this.aliases); + } + /** * Gets the plugin from which the button loader originates. * diff --git a/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoriesPlayer.java b/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoriesPlayer.java index d7c9f12a..33b84acc 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoriesPlayer.java +++ b/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoriesPlayer.java @@ -28,6 +28,8 @@ public interface InventoriesPlayer extends Listener { */ void storeInventoryTemporary(@NonNull Player player); + void storeInventoryTemporaryOrClear(@NotNull Player player); + /** * Allows giving the inventory back to the player * diff --git a/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoryPlayer.java b/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoryPlayer.java index e0ba743e..5e3a7752 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoryPlayer.java +++ b/API/src/main/java/fr/maxlego08/menu/api/players/inventory/InventoryPlayer.java @@ -3,6 +3,7 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.List; import java.util.Map; @@ -19,6 +20,8 @@ public interface InventoryPlayer { */ void storeInventory(@NotNull Player player); + void clearInventory(@NonNull Player player); + /** * Allows giving the inventory back to the player * diff --git a/API/src/main/java/fr/maxlego08/menu/api/registry/Registry.java b/API/src/main/java/fr/maxlego08/menu/api/registry/Registry.java new file mode 100644 index 00000000..56282628 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/registry/Registry.java @@ -0,0 +1,33 @@ +package fr.maxlego08.menu.api.registry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public abstract class Registry { + protected final Map registry = new HashMap<>(); + + public void register(@Nullable T key, @NotNull R value) { + registry.put(key, value); + } + + public boolean contains(T key) { + return registry.containsKey(key); + } + + public void unregister(T key) { + registry.remove(key); + } + + public Optional get(T key) { + return Optional.ofNullable(registry.get(key)); + } + + public Set getAllKeys() { + return registry.keySet(); + } +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/registry/RuleLoaderRegistry.java b/API/src/main/java/fr/maxlego08/menu/api/registry/RuleLoaderRegistry.java new file mode 100644 index 00000000..ed927f55 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/registry/RuleLoaderRegistry.java @@ -0,0 +1,50 @@ +package fr.maxlego08.menu.api.registry; + +import fr.maxlego08.menu.api.rules.Rule; +import fr.maxlego08.menu.api.rules.loader.RuleLoader; +import fr.maxlego08.menu.zcore.logger.Logger; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +public abstract class RuleLoaderRegistry extends Registry{ + + protected RuleLoaderRegistry() { + + } + + public void register(@NonNull RuleLoader value) { + register(null, value); + } + + @Override + public void register(@Nullable String key, @NonNull RuleLoader value) { + String type = value.getType().toLowerCase(Locale.ROOT); + super.register(type, value); + for (String alias : value.getAliases()) { + super.register(alias.toLowerCase(Locale.ROOT), value); + } + } + + @Nullable + public Rule loadRule(Map configuration) { + String type = (String) configuration.get("type"); + if (type == null) { + return null; + } + Optional ruleLoader = this.get(type); + if (ruleLoader.isEmpty()) { + Logger.info("Unknown rule type: " + type, "Available types: " + this.getAllKeys()); + return null; + } + return ruleLoader.get().load(configuration); + } + + @Override + public Optional get(@NonNull String key) { + return super.get(key.toLowerCase(Locale.ROOT)); + } +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/requirement/Action.java b/API/src/main/java/fr/maxlego08/menu/api/requirement/Action.java index b9e1f95d..d18731d0 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/requirement/Action.java +++ b/API/src/main/java/fr/maxlego08/menu/api/requirement/Action.java @@ -25,6 +25,7 @@ public abstract class Action { */ private int delay; private float chance; + protected boolean debug = false; /** * The type of the action. @@ -64,6 +65,14 @@ public void setDelay(int delay) { this.delay = delay; } + public boolean isDebug() { + return this.debug; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + @Contract(pure= true) public float getChance() { return this.chance; diff --git a/API/src/main/java/fr/maxlego08/menu/api/rules/Rule.java b/API/src/main/java/fr/maxlego08/menu/api/rules/Rule.java new file mode 100644 index 00000000..89a636ee --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/rules/Rule.java @@ -0,0 +1,9 @@ +package fr.maxlego08.menu.api.rules; + +import org.jetbrains.annotations.NotNull; + +public interface Rule { + + boolean matches(@NotNull RuleContext context); + +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/rules/RuleConfigHelper.java b/API/src/main/java/fr/maxlego08/menu/api/rules/RuleConfigHelper.java new file mode 100644 index 00000000..156e216e --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/rules/RuleConfigHelper.java @@ -0,0 +1,165 @@ +package fr.maxlego08.menu.api.rules; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Helper class for extracting values from rule configuration maps. + * Provides type-safe methods for common configuration value types. + */ +public final class RuleConfigHelper { + + private RuleConfigHelper() { + // Utility class + } + + /** + * Gets a string value from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @return the string value, or null if not present + */ + public static String getString(Map map, String key) { + Object value = map.get(key); + return value != null ? String.valueOf(value) : null; + } + + /** + * Gets a string value with a default fallback. + * + * @param map the configuration map + * @param key the key to look up + * @param defaultValue the default value if not present + * @return the string value or default + */ + public static String getString(Map map, String key, String defaultValue) { + String value = getString(map, key); + return value != null ? value : defaultValue; + } + + /** + * Gets a boolean value from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @param defaultValue the default value if not present + * @return the boolean value or default + */ + public static boolean getBoolean(Map map, String key, boolean defaultValue) { + Object value = map.get(key); + if (value instanceof Boolean b) return b; + if (value != null) return Boolean.parseBoolean(String.valueOf(value)); + return defaultValue; + } + + /** + * Gets an integer value from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @param defaultValue the default value if not present or invalid + * @return the integer value or default + */ + public static int getInt(Map map, String key, int defaultValue) { + Object value = map.get(key); + if (value instanceof Number n) return n.intValue(); + if (value != null) { + try { + return Integer.parseInt(String.valueOf(value)); + } catch (NumberFormatException ignored) { + } + } + return defaultValue; + } + + /** + * Gets a list of strings from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @return list of strings (never null, may be empty) + */ + @NotNull + public static List getStringList(Map map, String key) { + Object raw = map.get(key); + if (!(raw instanceof List list)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Object o : list) { + if (o != null) { + result.add(String.valueOf(o)); + } + } + return result; + } + + /** + * Gets a list of integers from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @return list of integers (never null, may be empty) + */ + public static List getIntegerList(Map map, String key) { + Object raw = map.get(key); + if (!(raw instanceof List list)) { + return List.of(); + } + List result = new ArrayList<>(); + for (Object o : list) { + if (o instanceof Number n) { + result.add(n.intValue()); + } else if (o != null) { + try { + result.add(Integer.parseInt(String.valueOf(o))); + } catch (NumberFormatException ignored) { + } + } + } + return result; + } + + /** + * Gets a list of maps from the configuration map. + * + * @param map the configuration map + * @param key the key to look up + * @return list of maps (never null, may be empty) + */ + public static List> getMapList(Map map, String key) { + Object raw = map.get(key); + if (!(raw instanceof List list)) { + return List.of(); + } + List> result = new ArrayList<>(); + for (Object o : list) { + if (o instanceof Map m) { + try { + //noinspection unchecked + result.add((Map) m); + } catch (ClassCastException ignored) { + } + } + } + return result; + } + + @NotNull + public static > E getEnum(Map map, String key, Class enumClass, E defaultValue) { + String value = getString(map, key); + if (value == null) { + return defaultValue; + } + try { + return Enum.valueOf(enumClass, value.toUpperCase()); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } +} \ No newline at end of file diff --git a/API/src/main/java/fr/maxlego08/menu/api/rules/RuleContext.java b/API/src/main/java/fr/maxlego08/menu/api/rules/RuleContext.java new file mode 100644 index 00000000..69af305d --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/rules/RuleContext.java @@ -0,0 +1,17 @@ +package fr.maxlego08.menu.api.rules; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + +public interface RuleContext { + + ItemStack getItemStack(); + + + Material getMaterial(); + + @Nullable + String getDisplayName(); + +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/rules/loader/RuleLoader.java b/API/src/main/java/fr/maxlego08/menu/api/rules/loader/RuleLoader.java new file mode 100644 index 00000000..c0ab4bbd --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/rules/loader/RuleLoader.java @@ -0,0 +1,22 @@ +package fr.maxlego08.menu.api.rules.loader; + +import fr.maxlego08.menu.api.rules.Rule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public interface RuleLoader { + + @NotNull String getType(); + + @NotNull + default List<@NotNull String> getAliases() { + return Collections.emptyList(); + } + + @Nullable + Rule load(@NotNull Map configuration); +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/InventoryReplacement.java b/API/src/main/java/fr/maxlego08/menu/api/utils/InventoryReplacement.java index 6c3fba06..e74809f1 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/InventoryReplacement.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/InventoryReplacement.java @@ -5,29 +5,13 @@ import java.util.List; import java.util.Objects; -public class InventoryReplacement { - private final String inventoryName; - private final String plugin; - private final List pages; - +public record InventoryReplacement(String inventoryName, String plugin, List pages) { public InventoryReplacement(String inventoryName, String plugin, List pages) { this.inventoryName = inventoryName; this.plugin = plugin == null || plugin.equalsIgnoreCase("") ? "zMenu" : plugin; this.pages = pages; } - public String getInventoryName() { - return this.inventoryName; - } - - public String getPlugin() { - return this.plugin; - } - - public List getPages() { - return this.pages; - } - public boolean shouldTrigger(Inventory inventory, int page) { // Inventory Name diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/Message.java b/API/src/main/java/fr/maxlego08/menu/api/utils/Message.java index 716bbc5b..f54cf1a9 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/Message.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/Message.java @@ -79,7 +79,7 @@ public enum Message implements IMessage { DESCRIPTION_OPEN("Allows you to open an inventory"), DESCRIPTION_SAVE("Allows you to save the item in your hand"), DESCRIPTION_RELOAD("Allows you to reload configuration files"), - DESCRIPTION_VERSION("Show plugin version"), + DESCRIPTION_VERSION("Show plugin value"), DESCRIPTION_LIST("Inventory list"), DESCRIPTION_TEST_DUPE("Test dupe"), DESCRIPTION_OPEN_ITEM("Give open item"), diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/OpenWithItem.java b/API/src/main/java/fr/maxlego08/menu/api/utils/OpenWithItem.java index 62675f79..a66f000a 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/OpenWithItem.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/OpenWithItem.java @@ -10,9 +10,6 @@ import java.util.List; -/** - *

Represents the item that can be interacted with to open a menu.

- */ /** * Represents an item and associated player actions that, when interacted with, trigger opening a specific menu. */ diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/Placeholders.java b/API/src/main/java/fr/maxlego08/menu/api/utils/Placeholders.java index 38fcae3d..33ef520b 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/Placeholders.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/Placeholders.java @@ -9,13 +9,7 @@ import java.util.List; import java.util.Map; -public class Placeholders { - - private final Map placeholders; - - public Placeholders(Map placeholders) { - this.placeholders = placeholders; - } +public record Placeholders(Map placeholders) { public Placeholders() { this(new HashMap<>()); @@ -27,7 +21,7 @@ public Placeholders() { * @param key the key of the placeholder. * @param value the value of the placeholder. */ - public void register(@Nullable String key,@Nullable String value) { + public void register(@Nullable String key, @Nullable String value) { this.placeholders.put(key, value); } @@ -36,8 +30,9 @@ public void register(@Nullable String key,@Nullable String value) { * * @return the map of placeholders. */ + @Override @NotNull - public Map getPlaceholders() { + public Map placeholders() { return this.placeholders; } @@ -87,7 +82,7 @@ public String parse(@NotNull String string) { * @return the parsed string */ @NotNull - public String parse(@NotNull String string,@NotNull String key,@NotNull String value) { + public String parse(@NotNull String string, @NotNull String key, @NotNull String value) { try { if (!string.contains("%")) return string; @@ -131,6 +126,6 @@ public String parse(@NotNull String string,@NotNull String key,@NotNull String v } public void merge(@NotNull Placeholders placeholders) { - this.placeholders.putAll(placeholders.getPlaceholders()); + this.placeholders.putAll(placeholders.placeholders()); } } diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/PlatformType.java b/API/src/main/java/fr/maxlego08/menu/api/utils/PlatformType.java new file mode 100644 index 00000000..c6d38500 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/PlatformType.java @@ -0,0 +1,60 @@ +package fr.maxlego08.menu.api.utils; + +public enum PlatformType { + /** + * Paper server with native Adventure API support. + */ + PAPER, + + /** + * Spigot server requiring Adventure Platform wrapper. + */ + SPIGOT; + + private static PlatformType detectedType; + + /** + * Detects the server platform type by checking for Paper-specific classes. + * + * @return The detected platform type + */ + public static PlatformType detect() { + if (detectedType == null) { + try { + Class.forName("io.papermc.paper.text.PaperComponents"); + detectedType = PAPER; + } catch (ClassNotFoundException e) { + detectedType = SPIGOT; + } + } + return detectedType; + } + + /** + * Gets the detected platform type. + * If detection hasn't been run yet, it will be performed. + * + * @return The platform type + */ + public static PlatformType get() { + return detect(); + } + + /** + * Checks if the current platform is Paper. + * + * @return true if running on Paper, false otherwise + */ + public static boolean isPaper() { + return get() == PAPER; + } + + /** + * Checks if the current platform is Spigot. + * + * @return true if running on Spigot, false otherwise + */ + public static boolean isSpigot() { + return get() == SPIGOT; + } +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/ReflectionsCache.java b/API/src/main/java/fr/maxlego08/menu/api/utils/ReflectionsCache.java new file mode 100644 index 00000000..a3ea6974 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/ReflectionsCache.java @@ -0,0 +1,51 @@ +package fr.maxlego08.menu.api.utils; + +import org.bukkit.plugin.java.JavaPlugin; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.ConfigurationBuilder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ReflectionsCache { + + private static ReflectionsCache instance; + + private final Map cache; + + private ReflectionsCache() { + this.cache = new ConcurrentHashMap<>(); + } + + public static synchronized ReflectionsCache getInstance() { + if (instance == null) { + instance = new ReflectionsCache(); + } + return instance; + } + + public Reflections getOrCreate(JavaPlugin plugin, String packageName) { + if (packageName == null || packageName.trim().isEmpty()) { + throw new IllegalArgumentException("Package name cannot be null or empty"); + } + + ClassLoader classLoader = plugin.getClass().getClassLoader(); + CacheKey key = new CacheKey(packageName, classLoader); + + return cache.computeIfAbsent(key, k -> createReflections(classLoader, packageName)); + } + + private Reflections createReflections(ClassLoader classLoader, String packageName) { + return new Reflections(new ConfigurationBuilder() + .forPackage(packageName, classLoader) + .addClassLoaders(classLoader) + .setScanners(Scanners.TypesAnnotated, Scanners.SubTypes)); + } + + private record CacheKey(String packageName, int classLoaderHash) { + CacheKey(String packageName, ClassLoader classLoader) { + this(packageName, System.identityHashCode(classLoader)); + } + } +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/TagRegistry.java b/API/src/main/java/fr/maxlego08/menu/api/utils/TagRegistry.java new file mode 100644 index 00000000..01f33701 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/TagRegistry.java @@ -0,0 +1,42 @@ +package fr.maxlego08.menu.api.utils; + +import org.bukkit.Material; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public final class TagRegistry { + private static final Map> MATERIAL_TAGS = new HashMap<>(); + + private TagRegistry() { + // Utility class + } + + static { + for (Field field : Tag.class.getDeclaredFields()) { + if (Tag.class.isAssignableFrom(field.getType())) { + try { + Class genericType = (Class) ((java.lang.reflect.ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; + if (Material.class.isAssignableFrom(genericType)) { + //noinspection unchecked + register(field.getName(), (Tag) field.get(null)); + } + } catch (Exception _) { + } + } + } + } + + public static void register(@NotNull String key,@NotNull Tag tag) { + MATERIAL_TAGS.put(key, tag); + } + + @Nullable + public static Tag getMaterialTag(@NotNull String key) { + return MATERIAL_TAGS.get(key); + } +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/TextChange.java b/API/src/main/java/fr/maxlego08/menu/api/utils/TextChange.java new file mode 100644 index 00000000..5d867940 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/TextChange.java @@ -0,0 +1,25 @@ +package fr.maxlego08.menu.api.utils; + +public record TextChange( + TextChangeType type, + String oldText, + String newText, + Character changedChar +) { + public static TextChange compute(String oldText, String newText) { + if (oldText.equals(newText)) + return new TextChange(TextChangeType.EQUAL, oldText, newText, null); + if (newText.isEmpty()) + return new TextChange(TextChangeType.CLEARED, oldText, newText, null); + + int delta = newText.length() - oldText.length(); + + if (delta == 1 && newText.startsWith(oldText)) + return new TextChange(TextChangeType.ADDED, oldText, newText, newText.charAt(newText.length() - 1)); + + if (delta == -1 && oldText.startsWith(newText)) + return new TextChange(TextChangeType.REMOVED, oldText, newText, oldText.charAt(oldText.length() - 1)); + + return new TextChange(TextChangeType.REPLACED, oldText, newText, null); + } +} \ No newline at end of file diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/TextChangeType.java b/API/src/main/java/fr/maxlego08/menu/api/utils/TextChangeType.java new file mode 100644 index 00000000..75caf951 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/TextChangeType.java @@ -0,0 +1,24 @@ +package fr.maxlego08.menu.api.utils; + +public enum TextChangeType { + /** + * One char added + */ + ADDED, + /** + * One char deleted + */ + REMOVED, + /** + * Empty field + */ + CLEARED, + /** + * Multiple chars changed at once + */ + REPLACED, + /** + * No change + */ + EQUAL +} diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java b/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java index 523f2a80..ba612460 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java @@ -4,29 +4,10 @@ * Generic tuple class storing two values of different types. * Useful for returning pairs from methods where a dedicated class is unnecessary. * - * @param Type of the first element - * @param Type of the second element + * @param Type of the first element + * @param Type of the second element + * @param first The first element in the tuple. + * @param second The second element in the tuple. */ -public class Tuples { - /** - * The first element in the tuple. - */ - public final T first; - /** - * The second element in the tuple. - */ - public final U second; - - public Tuples(T first, U second) { - this.first = first; - this.second = second; - } - - public T getFirst() { - return this.first; - } - - public U getSecond() { - return this.second; - } +public record Tuples(T first, U second) { } diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/TypedMapAccessor.java b/API/src/main/java/fr/maxlego08/menu/api/utils/TypedMapAccessor.java index 2e0978cf..a1736a26 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/TypedMapAccessor.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/TypedMapAccessor.java @@ -12,9 +12,7 @@ * This class provides typed access to a {@code Map}, allowing retrieval * of values of different types with or without default values if the key is not present in the map. */ -public class TypedMapAccessor implements MapConfiguration { - - private final Map map; +public record TypedMapAccessor(Map map) implements MapConfiguration { /** * Constructs a TypedMapAccessor with the specified map. @@ -329,10 +327,10 @@ public String toString() { } @SuppressWarnings("unchecked") - public List> getMapList(String key) { + public List> getMapList(String key) { try { return (List>) this.map.getOrDefault(key, new ArrayList>()); - } catch (ClassCastException e){ + } catch (ClassCastException e) { return new ArrayList<>(); } } diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Cuboid.java b/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Cuboid.java index 9049c097..0792bbcf 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Cuboid.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Cuboid.java @@ -593,7 +593,7 @@ public Block getRelativeBlock(int x, int y, int z) { /** * Get a block relative to the lower NE point of the Cuboid in the given - * World. This version of getRelativeBlock() should be used if being called + * World. This value of getRelativeBlock() should be used if being called * many times, to avoid excessive calls to getWorld(). * * @param w - The world diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Region.java b/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Region.java index 8962f032..2e444885 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Region.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/cuboid/Region.java @@ -11,19 +11,13 @@ /** * Represents a group of cuboids as a region, useful for area grouping, protection, or bulk operations. */ -public class Region { - private final List cuboids; - +public record Region(List cuboids) { public Region() { - this.cuboids = new ArrayList<>(); - } - - public Region(List cuboids) { - this.cuboids = cuboids; + this(new ArrayList<>()); } public Region(Cuboid... cuboids) { - this.cuboids = Arrays.asList(cuboids); + this(Arrays.asList(cuboids)); } public void addCube(Cuboid cube) { @@ -39,10 +33,6 @@ public boolean contains(Block block) { return false; } - public List getCuboids() { - return this.cuboids; - } - public Collection getPlayers() { Collection players = new LinkedHashSet<>(); for (Cuboid cuboid : this.cuboids) { diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/dialogs/record/ZDialogInventoryBuild.java b/API/src/main/java/fr/maxlego08/menu/api/utils/dialogs/record/ZDialogInventoryBuild.java index 3118d513..55fb6b76 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/dialogs/record/ZDialogInventoryBuild.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/dialogs/record/ZDialogInventoryBuild.java @@ -2,5 +2,5 @@ import org.jetbrains.annotations.NotNull; -public record ZDialogInventoryBuild(@NotNull String name,@NotNull String externalTitle, boolean canCloseWithEscape) { +public record ZDialogInventoryBuild(@NotNull String name, @NotNull String externalTitle, boolean canCloseWithEscape) { } diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/itemstack/ZToolRule.java b/API/src/main/java/fr/maxlego08/menu/api/utils/itemstack/ZToolRule.java index fbd33959..ec2ccb94 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/itemstack/ZToolRule.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/itemstack/ZToolRule.java @@ -1,25 +1,4 @@ package fr.maxlego08.menu.api.utils.itemstack; -public class ZToolRule { - private final T data; - private final float speed; - private final boolean correctForDrop; - - public ZToolRule(T data, float speed,boolean correctForDrop) { - this.data = data; - this.speed = speed; - this.correctForDrop = correctForDrop; - } - - public T getData() { - return this.data; - } - - public float getSpeed() { - return this.speed; - } - - public boolean isCorrectForDrop() { - return this.correctForDrop; - } +public record ZToolRule(T data, float speed, boolean correctForDrop) { } diff --git a/API/src/main/java/fr/maxlego08/menu/zcore/logger/Logger.java b/API/src/main/java/fr/maxlego08/menu/zcore/logger/Logger.java index 44a3a923..7bce7dce 100644 --- a/API/src/main/java/fr/maxlego08/menu/zcore/logger/Logger.java +++ b/API/src/main/java/fr/maxlego08/menu/zcore/logger/Logger.java @@ -1,12 +1,14 @@ package fr.maxlego08.menu.zcore.logger; -import org.bukkit.Bukkit; - -public record Logger(String prefix) { +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +public abstract class Logger { + protected final String prefix; private static Logger logger; - public Logger(String prefix) { + protected Logger(@NotNull String prefix) { + Preconditions.checkNotNull(prefix, "Prefix cannot be null"); this.prefix = prefix; logger = this; } @@ -23,23 +25,25 @@ public static void info(String message) { getLogger().log(message, LogType.INFO); } - public void log(String message, LogType type) { - Bukkit.getConsoleSender().sendMessage("ยง8[ยงe" + this.prefix + "ยง8] " + type.getColor() + this.getColoredMessage(message)); + public static void info(String message, Object... args) { + getLogger().log(message, args); + } + + public static void info(String message, LogType type, Object... args) { + getLogger().log(message, type, args); } public void log(String message) { - Bukkit.getConsoleSender().sendMessage("ยง8[ยงe" + this.prefix + "ยง8] ยงe" + this.getColoredMessage(message)); + this.log(message, LogType.INFO); } public void log(String message, Object... args) { - this.log(String.format(message, args)); + this.log(message, LogType.INFO, args); } - public void log(String message, LogType type, Object... args) { - this.log(String.format(message, args), type); - } + public abstract void log(@NotNull String message,@NotNull LogType type, @NotNull Object... args); - public void log(String[] messages, LogType type) { + public void log(@NotNull String[] messages,@NotNull LogType type) { for (String message : messages) { this.log(message, type); } @@ -50,19 +54,26 @@ public String getColoredMessage(String message) { } public enum LogType { - ERROR("ยงc"), - INFO("ยง7"), - WARNING("ยง6"), - SUCCESS("ยง2"); + ERROR("ยงc", ""), + INFO("ยง7", ""), + WARNING("ยง6", ""), + SUCCESS("ยง2", ""); + private static boolean isAdventure = false; private final String color; + private final String adventureColorCode; - LogType(String color) { - this.color = color; + LogType(@NotNull String legacyColorCode, @NotNull String adventureColorCode) { + this.color = legacyColorCode; + this.adventureColorCode = adventureColorCode; } public String getColor() { - return this.color; + return isAdventure ? this.adventureColorCode : this.color; + } + + public static void setIsAdventure(boolean isAdventure) { + LogType.isAdventure = isAdventure; } } } diff --git a/API/src/main/java/fr/maxlego08/menu/zcore/utils/PerformanceDebug.java b/API/src/main/java/fr/maxlego08/menu/zcore/utils/PerformanceDebug.java index 75ebfa8c..2b1f2330 100644 --- a/API/src/main/java/fr/maxlego08/menu/zcore/utils/PerformanceDebug.java +++ b/API/src/main/java/fr/maxlego08/menu/zcore/utils/PerformanceDebug.java @@ -3,6 +3,8 @@ import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.enums.PerformanceFilterMode; import fr.maxlego08.menu.zcore.logger.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayDeque; import java.util.ArrayList; @@ -19,23 +21,25 @@ public class PerformanceDebug { private final List records = new ArrayList<>(); private final Deque stack = new ArrayDeque<>(); - private PerformanceDebug(String context, boolean enabled) { + private PerformanceDebug(@Nullable String context, boolean enabled) { this.context = context; this.enabled = enabled; } + @NotNull public static PerformanceDebug disabled() { return DISABLED_INSTANCE; } - public static PerformanceDebug create(String context) { + @NotNull + public static PerformanceDebug create(@NotNull String context) { if (!Configuration.enablePerformanceDebug) { return DISABLED_INSTANCE; } return new PerformanceDebug(context, true); } - public void start(String operationName) { + public void start(@NotNull String operationName) { if (!this.enabled) return; this.stack.push(new ActiveOperation(operationName, System.nanoTime())); } @@ -48,7 +52,7 @@ public void end() { this.records.add(new OperationRecord(active.name, duration)); } - public void measure(String operationName, Runnable action) { + public void measure(@NotNull String operationName, @NotNull Runnable action) { if (!this.enabled) { action.run(); return; @@ -99,7 +103,7 @@ public void printSummary() { Logger.info(String.format("[PerformanceDebug] --- Total: %.2f ms (%d operations recorded) ---", totalMs, this.records.size())); } - private boolean matchesFilter(String operationName, PerformanceFilterMode mode, List patterns) { + private boolean matchesFilter(@NotNull String operationName,@NotNull PerformanceFilterMode mode,@NotNull List patterns) { if (mode == PerformanceFilterMode.DISABLED || patterns.isEmpty()) { return true; } @@ -112,12 +116,12 @@ private boolean matchesFilter(String operationName, PerformanceFilterMode mode, } } - return mode == PerformanceFilterMode.WHITELIST ? matches : !matches; + return (mode == PerformanceFilterMode.WHITELIST) == matches; } - private record ActiveOperation(String name, long startNanos) { + private record ActiveOperation(@NotNull String name, long startNanos) { } - private record OperationRecord(String name, long durationNanos) { + private record OperationRecord(@NotNull String name, long durationNanos) { } } diff --git a/Common/build.gradle.kts b/Common/build.gradle.kts index 72a078a4..698b9f82 100644 --- a/Common/build.gradle.kts +++ b/Common/build.gradle.kts @@ -1,6 +1,9 @@ -group = "fr.maxlego08.menu" -version = "1.1.0.7" +plugins { + alias(libs.plugins.paperweight) +} dependencies { api(projects.api) + api(projects.nms.base) + paperweight.paperDevBundle(libs.versions.paperDevBundle.get()) } \ No newline at end of file diff --git a/Common/src/main/java/fr/maxlego08/menu/ZMenuItemStack.java b/Common/src/main/java/fr/maxlego08/menu/ZMenuItemStack.java index 11db451b..655e176c 100644 --- a/Common/src/main/java/fr/maxlego08/menu/ZMenuItemStack.java +++ b/Common/src/main/java/fr/maxlego08/menu/ZMenuItemStack.java @@ -10,6 +10,7 @@ import fr.maxlego08.menu.api.context.BuildContext; import fr.maxlego08.menu.api.context.ZBuildContext; import fr.maxlego08.menu.api.enchantment.Enchantments; +import fr.maxlego08.menu.api.enums.AmountType; import fr.maxlego08.menu.api.enums.MenuItemRarity; import fr.maxlego08.menu.api.exceptions.ItemEnchantException; import fr.maxlego08.menu.api.font.FontImage; @@ -19,9 +20,9 @@ import fr.maxlego08.menu.api.utils.MapConfiguration; import fr.maxlego08.menu.api.utils.OfflinePlayerCache; import fr.maxlego08.menu.api.utils.Placeholders; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.common.utils.ZUtils; import fr.maxlego08.menu.common.utils.itemstack.MenuItemStackFromItemStack; -import fr.maxlego08.menu.common.utils.nms.NmsVersion; import fr.maxlego08.menu.zcore.logger.Logger; import fr.maxlego08.menu.zcore.utils.PerformanceDebug; import org.bukkit.*; @@ -49,6 +50,7 @@ public class ZMenuItemStack extends ZUtils implements MenuItemStack { private String material; private String targetPlayer; private String amount; + private AmountType amountType = AmountType.SET; private String url; private String data; private String tooltipStyle; @@ -61,7 +63,7 @@ public class ZMenuItemStack extends ZUtils implements MenuItemStack { private Map> translatedLore = new HashMap<>(); private boolean isGlowing; private String modelID; - private NamespacedKey itemModel; + private String itemModel; private String equippedModel; private Map enchantments = new HashMap<>(); private boolean clearDefaultAttributes = false; @@ -144,7 +146,8 @@ public ItemStack build(BuildContext context) { performanceDebug.end(); performanceDebug.start("build.createItemStack"); - ItemStack itemStack = this.applySpecialItemStack(player, offlinePlayer, placeholders, amount, context.getItemStack() != null ? context.getItemStack() : this.createItemStack(player, placeholders, offlinePlayer, amount)); + boolean editContextItem = context.getItemStack() != null; + ItemStack itemStack = this.applySpecialItemStack(player, offlinePlayer, placeholders, amount, editContextItem ? context.getItemStack() : this.createItemStack(player, placeholders, offlinePlayer, amount)); performanceDebug.end(); performanceDebug.start("build.applyItemMeta"); @@ -172,6 +175,17 @@ public ItemStack build(BuildContext context) { } performanceDebug.end(); + performanceDebug.start("build.setStackSize"); + if (this.amountType == AmountType.ADD) { + itemStack.setAmount(Math.max(1, itemStack.getAmount() + amount)); + } else if (this.amountType == AmountType.REMOVE) { + itemStack.setAmount(Math.max(1, itemStack.getAmount() - amount)); + } else if (!editContextItem) { + itemStack.setAmount(Math.max(1, amount)); + } + performanceDebug.end(); + + if (!this.needPlaceholderAPI && Configuration.enableCacheItemStack) { this.cacheItemStack = itemStack; } @@ -278,7 +292,6 @@ private ItemStack applySpecialItemStack(Player player, OfflinePlayer offlinePlay if (this.leatherArmor != null) { itemStack = this.leatherArmor.toItemStack(amount); } - itemStack.setAmount(Math.max(1 , amount)); if (this.durability != null) { int dura = this.parseDura(offlinePlayer == null ? player : offlinePlayer, placeholders); @@ -346,15 +359,15 @@ private void applyFlags(ItemMeta itemMeta) { } private void applyVersionSpecificMeta(ItemStack itemStack, ItemMeta itemMeta, Player player, Placeholders placeholders) { - if (NmsVersion.getCurrentVersion().isNewItemStackAPI()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21"))) { this.buildNewItemStackAPI(itemStack, itemMeta, player, placeholders); } - if (NmsVersion.getCurrentVersion().isNewHeadApi()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20"))) { this.buildTrimAPI(itemStack, itemMeta, player, placeholders); } - if (this.clearDefaultAttributes && this.attributes.isEmpty() && NmsVersion.getCurrentVersion().getVersion() >= NmsVersion.V_1_20_4.getVersion()) { + if (this.clearDefaultAttributes && this.attributes.isEmpty() && MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20.4"))) { itemMeta.setAttributeModifiers(ArrayListMultimap.create()); } } @@ -477,7 +490,10 @@ private void buildNewItemStackAPI(ItemStack itemStack, ItemMeta itemMeta, Player } } if (this.itemModel != null) { - itemMeta.setItemModel(this.itemModel); + NamespacedKey itemModelKey = this.parseItemModel(player, placeholders); + if (itemModelKey != null) { + itemMeta.setItemModel(itemModelKey); + } } if (this.equippedModel != null) { @@ -797,11 +813,38 @@ public void setToolTipStyle(String toolTipStyle) { @Override public NamespacedKey getItemModel() { + return this.parseItemModel(null, new Placeholders()); + } + + @Override + public String getItemModelString() { return this.itemModel; } + private NamespacedKey parseItemModel(@Nullable Player player, @NotNull Placeholders placeholders) { + String itemModel = this.papi(placeholders.parse(this.itemModel), player, true); + if (itemModel == null) { + return null; + } + try { + String[] split = itemModel.split(":", 2); + if (split.length == 2) { + return new NamespacedKey(split[0], split[1]); + } else { + return NamespacedKey.minecraft(itemModel); + } + } catch (Exception e) { + return null; + } + } + @Override public void setItemModel(NamespacedKey itemModel) { + this.itemModel = itemModel.toString(); + } + + @Override + public void setItemModel(String itemModel) { this.itemModel = itemModel; } @@ -840,6 +883,16 @@ public int parseAmount(OfflinePlayer offlinePlayer, Placeholders placeholders) { return amount; } + @Override + public AmountType getAmountType() { + return amountType; + } + + @Override + public void setAmountType(AmountType amountType) { + this.amountType = amountType; + } + /** * Let's know if the ItemStack needs a placeholder, if not then the ItemStack will be cached * @@ -855,6 +908,7 @@ public void setTypeMapAccessor(MapConfiguration configuration) { this.setData(configuration.getString("data", "0")); this.setDurability(configuration.getString("durability", null)); this.setAmount(configuration.getString("amount", "1")); + this.setAmountType(AmountType.valueOf(configuration.getString("amount-type", "SET").toUpperCase())); this.setMaterial(configuration.getString("material", null)); this.setTargetPlayer(configuration.getString("target", null)); this.setUrl(configuration.getString("url", null)); diff --git a/Common/src/main/java/fr/maxlego08/menu/common/MinecraftVersion.java b/Common/src/main/java/fr/maxlego08/menu/common/MinecraftVersion.java new file mode 100644 index 00000000..040220f9 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/MinecraftVersion.java @@ -0,0 +1,114 @@ +package fr.maxlego08.menu.common; + +import fr.maxlego08.menu.zcore.logger.Logger; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class MinecraftVersion implements Comparable { + private static final Map PARSE_CACHE = new ConcurrentHashMap<>(); + + private static MinecraftVersion currentVersion; + + private final int major; + private final int minor; + private final int patch; + + private MinecraftVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static MinecraftVersion getCurrentVersion() { + if (currentVersion == null) { + currentVersion = parse(Bukkit.getBukkitVersion()); + } + return currentVersion; + } + + @NotNull + public static MinecraftVersion parse(@Nullable String rawVersion) { + if (rawVersion == null || rawVersion.isBlank()) { + return new MinecraftVersion(0, 0, 0); + } + return PARSE_CACHE.computeIfAbsent(rawVersion, raw -> { + String clean = raw.contains("-") ? raw.substring(0, raw.indexOf('-')) : raw; + String[] parts = clean.split("\\."); + try { + int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + return new MinecraftVersion(major, minor, patch); + } catch (NumberFormatException e) { + Logger.info("Could not parse Minecraft value '" + raw + "'. Version-gated classes will be skipped. (" + e.getMessage() + ")", Logger.LogType.WARNING); + return new MinecraftVersion(0, 0, 0); + } + }); + } + + /** + * Returns {@code true} if this value is greater than or equal to {@code other}. + */ + public boolean isAtLeast(MinecraftVersion other) { + return compareTo(other) >= 0; + } + + /** + * Returns {@code true} if this value is less than or equal to {@code other}. + */ + public boolean isAtMost(MinecraftVersion other) { + return compareTo(other) <= 0; + } + + public boolean isBefore(MinecraftVersion other) { + return compareTo(other) < 0; + } + + public boolean isAfter(MinecraftVersion other) { + return compareTo(other) > 0; + } + + public int getMajor() { + return this.major; + } + + public int getMinor() { + return this.minor; + } + + public int getPatch() { + return this.patch; + } + + @Override + public int compareTo(MinecraftVersion other) { + if (this.major != other.major) { + return Integer.compare(this.major, other.major); + } + if (this.minor != other.minor) { + return Integer.compare(this.minor, other.minor); + } + return Integer.compare(this.patch, other.patch); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof MinecraftVersion other)) return false; + return this.major == other.major && this.minor == other.minor && this.patch == other.patch; + } + + @Override + public int hashCode() { + return 31 * (31 * major + minor) + patch; + } + + @Override + public String toString() { + return major + "." + minor + (patch != 0 ? "." + patch : ""); + } +} diff --git a/Common/src/main/java/fr/maxlego08/menu/common/VersionFilter.java b/Common/src/main/java/fr/maxlego08/menu/common/VersionFilter.java new file mode 100644 index 00000000..3a7296b7 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/VersionFilter.java @@ -0,0 +1,42 @@ +package fr.maxlego08.menu.common; + +import fr.maxlego08.menu.api.annotations.PaperOnly; +import fr.maxlego08.menu.api.annotations.SinceVersion; +import fr.maxlego08.menu.api.annotations.SpigotOnly; +import fr.maxlego08.menu.api.annotations.UntilVersion; +import fr.maxlego08.menu.api.utils.PlatformType; +import org.jetbrains.annotations.NotNull; + +public class VersionFilter { + + private VersionFilter() {} + + public static boolean passes(@NotNull Class clazz) { + if (clazz.isAnnotationPresent(PaperOnly.class) && !PlatformType.isPaper()) { + return false; + } + if (clazz.isAnnotationPresent(SpigotOnly.class) && PlatformType.isPaper()) { + return false; + } + + MinecraftVersion serverVersion = MinecraftVersion.getCurrentVersion(); + + SinceVersion since = clazz.getAnnotation(SinceVersion.class); + if (since != null) { + MinecraftVersion minimum = MinecraftVersion.parse(since.value()); + if (!serverVersion.isAtLeast(minimum)) { + return false; + } + } + + UntilVersion until = clazz.getAnnotation(UntilVersion.class); + if (until != null) { + MinecraftVersion maximum = MinecraftVersion.parse(until.value()); + return serverVersion.isAtMost(maximum); + } + + return true; + + } + +} diff --git a/Common/src/main/java/fr/maxlego08/menu/common/factory/VariantItemComponentLoaderFactory.java b/Common/src/main/java/fr/maxlego08/menu/common/factory/VariantItemComponentLoaderFactory.java deleted file mode 100644 index 905d90f4..00000000 --- a/Common/src/main/java/fr/maxlego08/menu/common/factory/VariantItemComponentLoaderFactory.java +++ /dev/null @@ -1,124 +0,0 @@ -package fr.maxlego08.menu.common.factory; - -import fr.maxlego08.menu.api.loader.ItemComponentLoader; -import org.jetbrains.annotations.NotNull; - -/** - * Factory interface for platform-specific ItemComponentLoaders for all supported variants. - * Each method returns a loader for a specific entity type/variant. - * If a loader cannot be provided, it may return null but all supported loaders should be implemented in factories. - **/ -public interface VariantItemComponentLoaderFactory { - /** - * Loader for Axolotl variant. Never null if the platform supports Axolotl. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderAxolotl(); - /** - * Loader for Cat collar color. May be null if not supported. - * - * @return @Nullable ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderCatCollar(); - /** - * Loader for Cat variant. Never null if supported by the platform. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderCatVariant(); - /** - * Loader for Chicken variant (regardless of underlying Paper/Bukkit differences). - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderChicken(); - /** - * Loader for Cow variant (handles Paper/Bukkit differences). - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderCow(); - /** - * Loader for Fox variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderFox(); - /** - * Loader for Frog variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderFrog(); - /** - * Loader for Horse variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderHorse(); - /** - * Loader for Llama variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderLlama(); - /** - * Loader for MushroomCow (Mooshroom) variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderMushroomCow(); - /** - * Loader for Painting variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderPainting(); - /** - * Loader for Parrot variant. - * - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderParrot(); - /** - * Loader for Pig variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderPig(); - /** - * Loader for Rabbit variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderRabbit(); - /** - * Loader for Salmon variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderSalmon(); - /** - * Loader for Sheep variant (dye color). - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderSheep(); - /** - * Loader for ShulkerBox variant (dye color). - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderShulkerBox(); - /** - * Loader for TropicalFish base color. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderTropicalFishBaseColor(); - /** - * Loader for TropicalFish pattern color. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderTropicalFishPatternColor(); - /** - * Loader for Villager variant. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderVillager(); - /** - * Loader for Wolf collar color. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderWolfCollar(); - /** - * Loader for Wolf variant type. - * @return @NotNull ItemComponentLoader - */ - @NotNull ItemComponentLoader getLoaderWolfVariant(); -} diff --git a/Common/src/main/java/fr/maxlego08/menu/common/network/ForceChannelPromise.java b/Common/src/main/java/fr/maxlego08/menu/common/network/ForceChannelPromise.java new file mode 100644 index 00000000..6f1ad473 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/network/ForceChannelPromise.java @@ -0,0 +1,10 @@ +package fr.maxlego08.menu.common.network; + +import io.netty.channel.Channel; +import io.netty.channel.DefaultChannelPromise; + +public class ForceChannelPromise extends DefaultChannelPromise { + public ForceChannelPromise(Channel channel) { + super(channel); + } +} \ No newline at end of file diff --git a/Common/src/main/java/fr/maxlego08/menu/common/network/NMSMenuPacketListener.java b/Common/src/main/java/fr/maxlego08/menu/common/network/NMSMenuPacketListener.java new file mode 100644 index 00000000..ab79516e --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/network/NMSMenuPacketListener.java @@ -0,0 +1,242 @@ +package fr.maxlego08.menu.common.network; + +import fr.maxlego08.menu.api.MenuPlugin; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBundlePacket; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.StreamSupport; + +/** + * Intercepts Netty packets per player for ZMenu's custom inventory system. + * Injected before Minecraft's own packet_handler in the pipeline. + */ +public class NMSMenuPacketListener implements Listener { + + private static final String MC_HANDLER = "packet_handler"; + private static final String MENU_HANDLER = "zmenu_packet_handler"; + + private static NMSMenuPacketListener instance; + + private final MenuPlugin plugin; + private final Map handlers = new ConcurrentHashMap<>(); + + private NMSMenuPacketListener(MenuPlugin plugin) { + this.plugin = plugin; + // Inject for players already online (e.g. on reload) + Bukkit.getOnlinePlayers().forEach(this::inject); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + public static void init(MenuPlugin plugin) { + if (instance != null) throw new IllegalStateException("Already initialised"); + instance = new NMSMenuPacketListener(plugin); + } + + public static NMSMenuPacketListener get() { + if (instance == null) throw new IllegalStateException("Not initialised"); + return instance; + } + + /** Call on plugin disable to clean up all injected handlers. */ + public void shutdown() { + Bukkit.getOnlinePlayers().forEach(this::eject); + instance = null; + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Public API + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Block an outgoing packet type from reaching the player. + * Useful to suppress vanilla inventory updates while a custom GUI is open. + */ + public void discardOutgoing(Player player, Class> type) { + handler(player).discardRules.add(type); + } + + public void stopDiscarding(Player player, Class> type) { + handler(player).discardRules.remove(type); + } + + /** + * Send a packet directly to the player, bypassing normal server logic. + */ + public void sendPacket(Player player, Packet packet) { + handler(player).sendPacket(packet); + } + + /** + * Send multiple packets as a single bundle. + */ + public void sendPackets(Player player, List> packets) { + if (packets.isEmpty()) return; + sendPacket(player, new ClientboundBundlePacket(packets)); + } + + /** + * Redirect an incoming packet type into a queue instead of letting it reach the server. + * Use this to intercept inventory clicks, slot changes, etc. + */ + public > void redirectIncoming( + Player player, + Class type, + PacketQueue> queue) { + handler(player).redirections.put(type, queue); + } + + public void stopRedirecting(Player player, Class> type) { + handler(player).redirections.remove(type); + } + + /** + * Listen to an incoming packet type WITHOUT blocking it. + * The packet still reaches the server; a copy lands in your queue. + */ + @SuppressWarnings("unchecked") + public > void listenIncoming( + Player player, + Class type, + PacketQueue> queue) { + handler(player).listeners.put(type, queue); + } + + public void stopListening(Player player, Class> type) { + handler(player).listeners.remove(type); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Events + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + @EventHandler(priority = EventPriority.LOWEST) + private void onJoin(PlayerJoinEvent e) { + inject(e.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + private void onQuit(PlayerQuitEvent e) { + handlers.remove(e.getPlayer().getUniqueId()); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Internals + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private void inject(Player player) { + if (handlers.containsKey(player.getUniqueId())) return; + Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel; + MenuPacketHandler h = new MenuPacketHandler(channel); + handlers.put(player.getUniqueId(), h); + channel.pipeline().addBefore(MC_HANDLER, MENU_HANDLER, h); + } + + private void eject(Player player) { + handlers.remove(player.getUniqueId()); + Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel; + try { + channel.pipeline().remove(MENU_HANDLER); + } catch (NoSuchElementException ignored) { + } + } + + private MenuPacketHandler handler(Player player) { + MenuPacketHandler h = handlers.get(player.getUniqueId()); + if (h == null) throw new IllegalStateException("No handler for player: " + player.getName()); + return h; + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Inner handler + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private static class MenuPacketHandler extends ChannelDuplexHandler { + + private final Set>> discardRules = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private final Map>, + PacketQueue>> redirections = new ConcurrentHashMap<>(); + + private final Map>, + PacketQueue>> listeners = new ConcurrentHashMap<>(); + + private final Channel channel; + + MenuPacketHandler(Channel channel) { + this.channel = channel; + } + + void sendPacket(Packet packet) { + if (!channel.eventLoop().inEventLoop()) { + channel.eventLoop().execute(() -> sendPacket(packet)); + return; + } + + channel.writeAndFlush(packet, new ForceChannelPromise(channel.newPromise().channel())); + } + + + @SuppressWarnings("unchecked") + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof Packet) || promise instanceof ForceChannelPromise) { + ctx.write(msg, promise); + return; + } + + if (msg instanceof ClientboundBundlePacket bundle) { + var kept = StreamSupport.stream(bundle.subPackets().spliterator(), false) + .>map(p -> filterOutgoing((Packet) p)) + .filter(Objects::nonNull) + .toList(); + + if (kept.isEmpty()) promise.setSuccess(); + else ctx.write(new ClientboundBundlePacket(kept), promise); + } else { + var out = filterOutgoing((Packet) msg); + if (out == null) promise.setSuccess(); + else ctx.write(out, promise); + } + } + + private @Nullable

> P filterOutgoing(P packet) { + return discardRules.contains(packet.getClass()) ? null : packet; + } + + @SuppressWarnings("unchecked") + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof Packet packet)) { + super.channelRead(ctx, msg); + return; + } + + @SuppressWarnings("unchecked") + var p = (Packet) packet; + + var listener = listeners.get(packet.getClass()); + if (listener != null) listener.offer(p); + + var redirect = redirections.get(packet.getClass()); + if (redirect != null) redirect.offer(p); + else super.channelRead(ctx, packet); + } + } +} \ No newline at end of file diff --git a/Common/src/main/java/fr/maxlego08/menu/common/network/PacketQueue.java b/Common/src/main/java/fr/maxlego08/menu/common/network/PacketQueue.java new file mode 100644 index 00000000..35ff8127 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/network/PacketQueue.java @@ -0,0 +1,238 @@ +package fr.maxlego08.menu.common.network; + +import com.tcoded.folialib.wrapper.task.WrappedTask; +import fr.maxlego08.menu.api.MenuPlugin; +import net.minecraft.network.protocol.Packet; +import org.bukkit.entity.Player; + +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.function.Function; + +public class PacketQueue> { + + private final Queue queue; + private final Map, Consumer> handlers; + private final Consumer fallback; + private final boolean discard; + private final boolean directDispatch; + private WrappedTask task; + + private PacketQueue(Builder builder) { + this.queue = builder.concurrent ? new ConcurrentLinkedQueue<>() : new LinkedList<>(); + this.handlers = Map.copyOf(builder.handlers); + this.fallback = builder.fallback; + this.discard = builder.discard; + this.directDispatch = builder.directDispatch; + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Queue access + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** Raw queue โ€” hand this to MenuPacketListener if needed. */ + public Queue queue() { + return queue; + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Offer + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Called by MenuPacketListener on the Netty I/O thread. + *

+ * If {@code discard} is enabled the packet is silently dropped. + * If {@code directDispatch} is enabled the handler fires immediately + * on the Netty thread โ€” only safe for thread-safe, Bukkit-API-free handlers. + * Otherwise the packet is queued for later {@link #dispatch()} on the main thread. + */ + public void offer(T packet) { + if (discard) return; + if (directDispatch) { + dispatchSingle(packet); + return; + } + queue.offer(packet); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Dispatch + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Drain every queued packet and fire the matching consumer. + * Must be called on the correct thread (main / entity thread). + * No-op when {@code directDispatch} is enabled since packets never queue. + */ + public void dispatch() { + T packet; + while ((packet = queue.poll()) != null) dispatchSingle(packet); + } + + /** + * Drain exactly one queued packet. + * Returns {@code true} if a packet was dispatched. + */ + public boolean dispatchOne() { + T packet = queue.poll(); + if (packet == null) return false; + dispatchSingle(packet); + return true; + } + + @SuppressWarnings("unchecked") + private void dispatchSingle(T packet) { + Consumer handler = handlers.get(packet.getClass()); + if (handler != null) handler.accept(packet); + else if (fallback != null) fallback.accept(packet); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Scheduling + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Schedules a repeating task that calls {@link #dispatch()} every tick. + * The queue owns the task lifecycle โ€” call {@link #cancel()} to stop it. + *

+ * Accepts a scheduler function to keep PacketQueue decoupled from FoliaLib: + *

{@code
+     * queue.schedule(r -> plugin.getScheduler().runAtEntityTimer(player, r, 1L, 1L));
+     * }
+ */ + public PacketQueue schedule(Function scheduler) { + this.task = scheduler.apply(this::dispatch); + return this; + } + + /** + * Convenience overload when you don't need a custom interval. + * Schedules at 1-tick period on the player's entity thread. + */ + public PacketQueue schedule(MenuPlugin plugin, Player player) { + return schedule(r -> plugin.getScheduler().runAtEntityTimer(player, r, 1L, 1L)); + } + + /** Cancels the scheduled dispatch task if one was started via {@link #schedule}. */ + public void cancel() { + if (task != null) { + task.cancel(); + task = null; + } + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Utility + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + public boolean isEmpty() { return queue.isEmpty(); } + public int size() { return queue.size(); } + public void clear() { queue.clear(); } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Static factories + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * Silently discards every packet โ€” useful to block a packet type + * without processing it. + */ + public static > PacketQueue empty() { + return new PacketQueue<>(new Builder().discard(true).concurrent(false)); + } + + /** + * Packets are queued but never dispatched automatically. + * Drain manually via {@link #dispatch()} or {@link #dispatchOne()}. + */ + public static > PacketQueue unhandled() { + return PacketQueue.builder().build(); + } + + /** + * Queue with a single catch-all handler for every packet type. + */ + public static > PacketQueue of(Consumer handler) { + return PacketQueue.builder().orElse(handler).build(); + } + + /** + * Queue with a single typed handler. + */ + public static

> PacketQueue

of(Class

type, Consumer handler) { + return PacketQueue.

builder().on(type, handler).build(); + } + + public static > Builder builder() { + return new Builder<>(); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Builder + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + public static final class Builder> { + + private boolean concurrent = true; + private boolean discard = false; + private boolean directDispatch = false; + private final Map, Consumer> handlers = new LinkedHashMap<>(); + private Consumer fallback; + + public Builder concurrent(boolean concurrent) { + this.concurrent = concurrent; + return this; + } + + /** + * When {@code true}, every incoming packet is silently dropped. + * No handlers are called and nothing is queued. + */ + public Builder discard(boolean discard) { + this.discard = discard; + return this; + } + + /** + * When {@code true}, handlers fire immediately on the Netty I/O thread + * instead of being queued for the main thread. + *

+ * Only enable this if your handlers are thread-safe and do not touch + * the Bukkit API. Using this with {@link org.bukkit.entity.Player} + * methods or any scheduler will cause corruption or crashes. + */ + public Builder directDispatch(boolean directDispatch) { + this.directDispatch = directDispatch; + return this; + } + + /** + * Register a typed handler for a specific packet subclass. + *

{@code
+         * .on(ServerboundRenameItemPacket.class, p -> handleRename(p.getName()))
+         * }
+ */ + @SuppressWarnings("unchecked") + public

Builder on(Class

type, Consumer handler) { + handlers.put(type, (Consumer) handler); + return this; + } + + /** + * Catch-all โ€” fires for any packet whose class has no specific handler. + */ + public Builder orElse(Consumer fallback) { + this.fallback = fallback; + return this; + } + + public PacketQueue build() { + return new PacketQueue<>(this); + } + } +} \ No newline at end of file diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/MessageUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/MessageUtils.java index ce9596bf..7081e740 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/MessageUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/MessageUtils.java @@ -3,7 +3,7 @@ import fr.maxlego08.menu.api.MenuPlugin; import fr.maxlego08.menu.api.utils.IMessage; import fr.maxlego08.menu.api.utils.Message; -import fr.maxlego08.menu.common.utils.nms.NmsVersion; +import fr.maxlego08.menu.common.MinecraftVersion; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -130,7 +130,7 @@ protected final Class getNMSClass(String name) { protected void title(Player player, String title, String subtitle, int fadeInTime, int showTime, int fadeOutTime) { - if (NmsVersion.nmsVersion.isNewMaterial()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.13"))) { player.sendTitle(title, subtitle, fadeInTime, showTime, fadeOutTime); return; } diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/ZUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/ZUtils.java index 6ac42804..329581ac 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/ZUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/ZUtils.java @@ -3,9 +3,9 @@ import fr.maxlego08.menu.api.MenuPlugin; import fr.maxlego08.menu.api.utils.EnumInventory; import fr.maxlego08.menu.api.utils.Message; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.common.enums.Permission; import fr.maxlego08.menu.common.utils.nms.NMSUtils; -import fr.maxlego08.menu.common.utils.nms.NmsVersion; import fr.maxlego08.menu.zcore.logger.Logger; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; @@ -45,7 +45,7 @@ public abstract class ZUtils extends MessageUtils { private static Material[] byId; static { - if (!NmsVersion.nmsVersion.isNewMaterial()) { + if (!MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.13"))) { byId = new Material[0]; for (Material material : Material.values()) { if (byId.length <= material.getId()) { @@ -57,7 +57,7 @@ public abstract class ZUtils extends MessageUtils { } protected String findPlayerLocale(Player player) { - if (NmsVersion.getCurrentVersion().getVersion() >= NmsVersion.V_1_13.getVersion()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.13"))) { try { return player != null ? player.getLocale() : null; } catch (Exception exception) { @@ -452,7 +452,7 @@ private void applyTextureUrl(ItemStack itemStack, String url) { protected Object getPrivateField(Object object, String field) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Class clazz = object.getClass(); - Field objectField = field.equals("commandMap") ? clazz.getDeclaredField(field) : field.equals("knownCommands") ? NmsVersion.nmsVersion.isNewMaterial() ? clazz.getSuperclass().getDeclaredField(field) : clazz.getDeclaredField(field) : null; + Field objectField = field.equals("commandMap") ? clazz.getDeclaredField(field) : field.equals("knownCommands") ? MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.13")) ? clazz.getSuperclass().getDeclaredField(field) : clazz.getDeclaredField(field) : null; objectField.setAccessible(true); Object result = objectField.get(object); objectField.setAccessible(false); diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/itemstack/MenuItemStackFromItemStack.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/itemstack/MenuItemStackFromItemStack.java index 5edc1a38..d2739d16 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/itemstack/MenuItemStackFromItemStack.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/itemstack/MenuItemStackFromItemStack.java @@ -4,8 +4,8 @@ import fr.maxlego08.menu.api.InventoryManager; import fr.maxlego08.menu.api.itemstack.Firework; import fr.maxlego08.menu.api.itemstack.Potion; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.common.utils.nms.ItemStackUtils; -import fr.maxlego08.menu.common.utils.nms.NmsVersion; import org.bukkit.FireworkEffect; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.*; @@ -24,7 +24,7 @@ public static ZMenuItemStack fromItemStack(InventoryManager manager, ItemStack i menuItemStack.setMaterial(itemStack.getType().name()); int amount = itemStack.getAmount(); if (amount > 1) menuItemStack.setAmount(String.valueOf(itemStack.getAmount())); - if (NmsVersion.getCurrentVersion().isItemLegacy()) { + if (MinecraftVersion.getCurrentVersion().isBefore(MinecraftVersion.parse("1.13"))) { int durability = itemStack.getDurability(); if (durability > 0) menuItemStack.setDurability(durability); int data = itemStack.getData().getData(); @@ -45,7 +45,7 @@ public static ZMenuItemStack fromItemStack(InventoryManager manager, ItemStack i menuItemStack.setFlags(new ArrayList<>(itemMeta.getItemFlags())); menuItemStack.setEnchantments(itemMeta.getEnchants()); - if (NmsVersion.getCurrentVersion().isCustomModelData() && itemMeta.hasCustomModelData()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.14")) && itemMeta.hasCustomModelData()) { menuItemStack.setModelID(itemMeta.getCustomModelData()); } diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackCompound.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackCompound.java index c6e796eb..3cf202e5 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackCompound.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackCompound.java @@ -1,6 +1,8 @@ package fr.maxlego08.menu.common.utils.nms; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.common.utils.nms.ItemStackUtils.EnumReflectionItemStack; +import fr.maxlego08.menu.nms.NMSHandler; import org.bukkit.inventory.ItemStack; public class ItemStackCompound { @@ -12,14 +14,14 @@ public class ItemStackCompound { // Static block to initialize the itemStackCompound based on the NmsVersion static { - fr.maxlego08.menu.common.utils.nms.NmsVersion nmsVersion = fr.maxlego08.menu.common.utils.nms.NmsVersion.nmsVersion; - if (nmsVersion == fr.maxlego08.menu.common.utils.nms.NmsVersion.V_1_18_2) { + MinecraftVersion currentVersion = MinecraftVersion.getCurrentVersion(); + if (currentVersion.equals(MinecraftVersion.parse("1.18.2"))) { itemStackCompound = new ItemStackCompound(EnumReflectionCompound.V1_18_2); - } else if (nmsVersion.getVersion() >= 1200) { + } else if (currentVersion.isAtLeast(MinecraftVersion.parse("1.12"))) { itemStackCompound = new ItemStackCompound(EnumReflectionCompound.V1_12); - } else if (nmsVersion.getVersion() >= 1190) { + } else if (currentVersion.isAtLeast(MinecraftVersion.parse("1.19"))) { itemStackCompound = new ItemStackCompound(EnumReflectionCompound.V1_19); - } else if (nmsVersion.getVersion() >= 1170) { + } else if (currentVersion.isAtLeast(MinecraftVersion.parse("1.17"))) { itemStackCompound = new ItemStackCompound(EnumReflectionCompound.V1_17); } else itemStackCompound = new ItemStackCompound(EnumReflectionCompound.V1_8_8); } @@ -29,7 +31,7 @@ public class ItemStackCompound { /** * Constructs an ItemStackCompound instance based on the given EnumReflectionCompound. * - * @param reflection The EnumReflectionCompound representing the NBT tag reflection version. + * @param reflection The EnumReflectionCompound representing the NBT tag reflection value. */ public ItemStackCompound(EnumReflectionCompound reflection) { super(); @@ -77,6 +79,8 @@ public ItemStack applyCompound(ItemStack itemStack, Object compoundObject) throw * @return The modified ItemStack. */ public ItemStack setString(ItemStack itemStack, String key, String value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setString(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); if (compoundObject == null) return null; @@ -97,6 +101,8 @@ public ItemStack setString(ItemStack itemStack, String key, String value) { * @return The string value. */ public String getString(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getString(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); if (compoundObject == null) return null; @@ -117,6 +123,8 @@ public String getString(ItemStack itemStack, String key) { * @return The double value. */ public double getDouble(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getDouble(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); if (compoundObject == null) return 0; @@ -136,6 +144,8 @@ public double getDouble(ItemStack itemStack, String key) { * @return The long value. */ public long getLong(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getLong(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); if (compoundObject == null) return 0; @@ -154,6 +164,8 @@ public long getLong(ItemStack itemStack, String key) { * @return The integer value. */ public int getInt(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getInt(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); if (compoundObject == null) return 0; @@ -173,6 +185,8 @@ public int getInt(ItemStack itemStack, String key) { * @return The float value. */ public float getFloat(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getFloat(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); return (float) compoundObject.getClass().getMethod(this.reflection.getMethodGetFloat(), String.class).invoke(compoundObject, new Object[]{key}); @@ -191,6 +205,8 @@ public float getFloat(ItemStack itemStack, String key) { * @return The boolean value. */ public boolean getBoolean(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.getBoolean(itemStack, key); try { Object compoundObject = this.getCompound(itemStack); return (boolean) compoundObject.getClass().getMethod(this.reflection.getMethodGetBoolean(), String.class).invoke(compoundObject, new Object[]{key}); @@ -210,6 +226,8 @@ public boolean getBoolean(ItemStack itemStack, String key) { * @return The modified ItemStack. */ public ItemStack setInt(ItemStack itemStack, String key, int value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setInt(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); compoundObject.getClass().getMethod(this.reflection.getMethodSetInt(), String.class, int.class).invoke(compoundObject, key, value); @@ -230,6 +248,8 @@ public ItemStack setInt(ItemStack itemStack, String key, int value) { * @return The modified ItemStack. */ public ItemStack setLong(ItemStack itemStack, String key, long value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setLong(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); compoundObject.getClass().getMethod(this.reflection.getMethodSetLong(), String.class, long.class).invoke(compoundObject, key, value); @@ -250,6 +270,8 @@ public ItemStack setLong(ItemStack itemStack, String key, long value) { * @return The modified ItemStack. */ public ItemStack setFloat(ItemStack itemStack, String key, float value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setFloat(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); compoundObject.getClass().getMethod(this.reflection.getMethodSetFloat(), String.class, float.class).invoke(compoundObject, key, value); @@ -270,6 +292,8 @@ public ItemStack setFloat(ItemStack itemStack, String key, float value) { * @return The modified ItemStack. */ public ItemStack setBoolean(ItemStack itemStack, String key, boolean value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setBoolean(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); @@ -291,6 +315,8 @@ public ItemStack setBoolean(ItemStack itemStack, String key, boolean value) { * @return The modified ItemStack. */ public ItemStack setDouble(ItemStack itemStack, String key, double value) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.setDouble(itemStack, key, value); try { Object compoundObject = this.getCompound(itemStack); compoundObject.getClass().getMethod(this.reflection.getMethodSetDouble(), String.class, double.class).invoke(compoundObject, key, value); @@ -310,6 +336,8 @@ public ItemStack setDouble(ItemStack itemStack, String key, double value) { * @return True if the key is present, false otherwise. */ public boolean isKey(ItemStack itemStack, String key) { + NMSHandler handler = NMSProvider.getHandler(); + if (handler != null) return handler.hasKey(itemStack, key); try { Object nbttagCompound = this.getCompound(itemStack); if (nbttagCompound == null) return false; diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java index cdd70f66..be1e9cfc 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java @@ -1,6 +1,7 @@ package fr.maxlego08.menu.common.utils.nms; import fr.maxlego08.menu.api.configuration.Configuration; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.common.utils.Base64; import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; @@ -15,6 +16,7 @@ public class ItemStackUtils { private static final fr.maxlego08.menu.common.utils.nms.NmsVersion NMS_VERSION = fr.maxlego08.menu.common.utils.nms.NmsVersion.nmsVersion; private static final Map itemStackSerialized = new HashMap<>(); + private static final MinecraftVersion MINECRAFT_VERSION = MinecraftVersion.getCurrentVersion(); public static String serializeItemStack(ItemStack paramItemStack) { @@ -26,7 +28,7 @@ public static String serializeItemStack(ItemStack paramItemStack) { return itemStackSerialized.get(paramItemStack); } - if (fr.maxlego08.menu.common.utils.nms.NmsVersion.getCurrentVersion().isAttributItemStack()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20.5"))) { return Base64ItemStack.encode(paramItemStack); } @@ -67,7 +69,7 @@ public static ItemStack deserializeItemStack(String paramString) { return null; } - if (fr.maxlego08.menu.common.utils.nms.NmsVersion.getCurrentVersion().isAttributItemStack()) { + if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20.5"))) { return Base64ItemStack.decode(paramString); } @@ -99,7 +101,7 @@ public static ItemStack deserializeItemStack(String paramString) { if (NMS_VERSION == fr.maxlego08.menu.common.utils.nms.NmsVersion.V_1_11 || NMS_VERSION == fr.maxlego08.menu.common.utils.nms.NmsVersion.V_1_12) { Constructor localConstructor = localClass2.getConstructor(localClass1); localObject2 = localConstructor.newInstance(localObject1); - } else if (!NMS_VERSION.isItemLegacy()) { + } else if (MINECRAFT_VERSION.isAtMost(MinecraftVersion.parse("1.13"))) { localObject2 = localClass2.getMethod("a", new Class[]{localClass1}).invoke(null, localObject1); } else { diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSProvider.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSProvider.java new file mode 100644 index 00000000..fd43d903 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSProvider.java @@ -0,0 +1,61 @@ +package fr.maxlego08.menu.common.utils.nms; + +import fr.maxlego08.menu.api.utils.ReflectionsCache; +import fr.maxlego08.menu.common.MinecraftVersion; +import fr.maxlego08.menu.nms.NMSHandler; +import fr.maxlego08.menu.nms.NMSVersion; +import fr.maxlego08.menu.zcore.logger.Logger; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import org.reflections.Reflections; + +import java.util.Set; + +public class NMSProvider { + + private static NMSHandler handler; + + public static NMSHandler getHandler() { + if (handler == null) { + handler = loadHandler(); + } + return handler; + } + + private static NMSHandler loadHandler() { + MinecraftVersion current = MinecraftVersion.getCurrentVersion(); + JavaPlugin plugin = (JavaPlugin) Bukkit.getPluginManager().getPlugin("zMenu"); + if (plugin == null) return null; + + Reflections reflections = ReflectionsCache.getInstance().getOrCreate(plugin, "fr.maxlego08.menu.nms"); + Set> candidates = reflections.getTypesAnnotatedWith(NMSVersion.class); + + Class bestMatch = null; + MinecraftVersion bestVersion = null; + + for (Class clazz : candidates) { + if (!NMSHandler.class.isAssignableFrom(clazz)) continue; + + NMSVersion annotation = clazz.getAnnotation(NMSVersion.class); + MinecraftVersion required = MinecraftVersion.parse(annotation.value()); + + if (current.isAtLeast(required)) { + if (bestVersion == null || required.isAfter(bestVersion)) { + bestVersion = required; + bestMatch = (Class) clazz; + } + } + } + + if (bestMatch != null) { + try { + return bestMatch.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + Logger.info("Could not instantiate NMS handler " + bestMatch.getName() + ": " + e.getMessage()); + } + } + + return null; + } +} + diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSUtils.java index df76458f..90642df9 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NMSUtils.java @@ -5,9 +5,9 @@ public class NMSUtils { /** - * Get minecraft serveur version + * Get minecraft serveur value * - * @return version + * @return value */ public static double getNMSVersion() { if (version != 0) @@ -24,7 +24,7 @@ public static double getNMSVersion() { } /** - * Check if minecraft version has shulker + * Check if minecraft value has shulker * * @return boolean */ @@ -33,7 +33,7 @@ public static boolean hasShulker() { } /** - * Check if minecraft version has barrel + * Check if minecraft value has barrel * * @return booleab */ @@ -44,7 +44,7 @@ public static boolean hasBarrel() { } /** - * check if version is granther than 1.13 + * check if value is granther than 1.13 * * @return boolean */ @@ -53,7 +53,7 @@ public static boolean isNewVersion() { } /** - * Check if version has one hand + * Check if value has one hand * * @return boolean */ @@ -62,7 +62,7 @@ public static boolean isOneHand() { } /** - * Check is version is minecraft 1.7 + * Check is value is minecraft 1.7 * * @return boolean */ @@ -71,7 +71,7 @@ public static boolean isVeryOldVersion() { } public static double version = getNMSVersion(); /** - * Check if version has itemmeta unbreakable + * Check if value has itemmeta unbreakable * * @return boolean */ @@ -80,7 +80,7 @@ public static boolean isUnbreakable() { } /** - * Check if version is old version of minecraft with old material system + * Check if value is old value of minecraft with old material system * * @return boolean */ @@ -90,7 +90,7 @@ public static boolean isOldVersion() { } /** - * Check if server vesion is new version + * Check if server vesion is new value * * @return boolean */ @@ -101,7 +101,7 @@ public static boolean isNewNMSVersion() { } /** - * Allows to check if the version has the colors in hex + * Allows to check if the value has the colors in hex * * @return boolean */ @@ -111,7 +111,7 @@ public static boolean isHexColor() { } /** - * Allows to check if the version has the colors in hex + * Allows to check if the value has the colors in hex * * @return boolean */ @@ -121,7 +121,7 @@ public static boolean isComponentColor() { } /** - * Check if server version is new version + * Check if server value is new value * * @return boolean */ @@ -133,7 +133,7 @@ public static boolean isNewNBTVersion() { } /** - * Check if server version is new version + * Check if server value is new value * * @return boolean */ diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NmsVersion.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NmsVersion.java index 9d27dc7e..aadb88d6 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NmsVersion.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/NmsVersion.java @@ -1,5 +1,6 @@ package fr.maxlego08.menu.common.utils.nms; +import fr.maxlego08.menu.common.MinecraftVersion; import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.Bukkit; @@ -60,9 +61,7 @@ public enum NmsVersion { V_1_21_10(12110), V_1_21_11(12111), - UNKNOWN(Integer.MAX_VALUE) - - ; + UNKNOWN(Integer.MAX_VALUE); public static final NmsVersion nmsVersion = getNmsVersion(); private final int version; @@ -75,12 +74,14 @@ public enum NmsVersion { * Gets the current version of the Bukkit server. * * @return The NmsVersion instance corresponding to the current version. + * @deprecated Use {@link MinecraftVersion#getCurrentVersion()} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public static NmsVersion getCurrentVersion() { return nmsVersion; } - private static NmsVersion getNmsVersion(){ + private static NmsVersion getNmsVersion() { Matcher matcher = Pattern.compile("(?\\d+\\.\\d+)(?\\.\\d+)?").matcher(Bukkit.getBukkitVersion()); int currentVersion = matcher.find() ? Integer.parseInt(matcher.group("version").replace(".", "") + (matcher.group("patch") != null ? matcher.group("patch").replace(".", "") : "0")) : 0; @@ -93,11 +94,11 @@ private static NmsVersion getNmsVersion(){ if (currentVersion > highestSupportedVersionEnum.version) { Logger.info(String.format( - "Running Minecraft %s (newer than highest supported version %s). " + - "Please report this version to help us add support. " + - "Check for plugin updates if you experience issues.", - currentVersion, - highestSupportedVersionEnum.name() + "Running Minecraft %s (newer than highest supported version %s). " + + "Please report this version to help us add support. " + + "Check for plugin updates if you experience issues.", + currentVersion, + highestSupportedVersionEnum.name() ), Logger.LogType.WARNING); return UNKNOWN; } @@ -116,161 +117,218 @@ private static NmsVersion getNmsVersion(){ } /** - * Checks if the current version supports PlayerProfiles. - * - * @return True if PlayerProfiles are supported, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.18.1"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean hasPlayerProfiles() { return this.version >= NmsVersion.V_1_18_1.version; } /** - * Checks if the current version uses obfuscated names. - * - * @return True if names are obfuscated, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.17"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean hasObfuscatedNames() { return this.version >= NmsVersion.V_1_17.version; } /** - * Checks if the current version supports components. - * - * @param isPaper True if the server uses Paper, else False. - * @return True if components are supported, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.16.5"))} instead (combined with your Paper check). */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isComponent(boolean isPaper) { return isPaper && this.version >= NmsVersion.V_1_16_5.version; } /** - * Checks if the current version is a legacy item version. - * - * @return True if the version is legacy, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isBefore(MinecraftVersion.parse("1.13"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isItemLegacy() { return this.version < NmsVersion.V_1_13.version; } /** - * Checks if the current version supports PersistentDataContainer. - * - * @return True if PersistentDataContainer is supported, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.14"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isPdcVersion() { return this.version >= NmsVersion.V_1_14.version; } /** - * Checks if the current version is a legacy version for Skull owners. - * - * @return True if the version is legacy, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtMost(MinecraftVersion.parse("1.12"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isSkullOwnerLegacy() { return this.version <= NmsVersion.V_1_12.version; } /** - * Checks if the current version supports CustomModelData. - * - * @return True if CustomModelData is supported, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.14"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isCustomModelData() { return this.version >= NmsVersion.V_1_14.version; } /** - * Checks if the current version is a hexadecimal version. - * - * @return True if the version is hexadecimal, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.16"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isHexVersion() { return this.version >= NmsVersion.V_1_16.version; } /** - * Checks if the current version is an Attribute version. - * - * @return True if the version is Attribute, else False. + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAfter(MinecraftVersion.parse("1.8.8"))} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isAttributeVersion() { return this.version != NmsVersion.V_1_8_8.version; } /** - * Gets the version number associated with the enumeration. - * - * @return The version number. + * @deprecated Use {@link MinecraftVersion#getMinor()} or comparisons via {@link MinecraftVersion} instead. */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public int getVersion() { return this.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20.5"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isAttributItemStack() { return this.version >= NmsVersion.V_1_20_5.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().equals(MinecraftVersion.parse("1.8.8"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isOneHand() { return this.version == NmsVersion.V_1_8_8.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.14"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isBarrel() { return this.version >= V_1_14.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.9"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isShulker() { return this.version >= V_1_9.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.13"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewMaterial() { return this.version >= V_1_13.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.18"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewNBTVersion() { return this.version >= V_1_18.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewHeadApi() { return this.version >= V_1_20.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.17"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewNMSVersion() { return this.version >= V_1_17.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.11"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_11OrNewer() { return this.version >= V_1_11.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.12"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_12OrNewer() { return this.version >= V_1_12.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewItemStackAPI() { return this.version >= V_1_21.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.4"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isNewItemModelAPI() { return this.version >= V_1_21_4.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.7"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean isDialogsVersion() { return this.version >= V_1_21_7.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.2"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_21_2OrNewer() { return this.version >= V_1_21_2.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.5"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_21_5OrNewer() { return this.version >= V_1_21_5.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.9"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_21_9OrNewer() { return this.version >= V_1_21_9.version; } + /** + * @deprecated Use {@code MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.21.11"))} instead. + */ + @Deprecated(since = "1.1.1.4", forRemoval = true) public boolean is1_21_11OrNewer() { return this.version >= V_1_21_11.version; } -} +} \ No newline at end of file diff --git a/src/main/java/fr/maxlego08/menu/inventory/VInventory.java b/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java similarity index 84% rename from src/main/java/fr/maxlego08/menu/inventory/VInventory.java rename to Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java index d83dc1ff..174902cb 100644 --- a/src/main/java/fr/maxlego08/menu/inventory/VInventory.java +++ b/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java @@ -1,7 +1,7 @@ package fr.maxlego08.menu.inventory; -import fr.maxlego08.menu.ZMenuPlugin; import fr.maxlego08.menu.api.InventoryListener; +import fr.maxlego08.menu.api.MenuPlugin; import fr.maxlego08.menu.api.animation.PlayerTitleAnimation; import fr.maxlego08.menu.api.animation.TitleAnimation; import fr.maxlego08.menu.api.configuration.Configuration; @@ -11,6 +11,7 @@ import fr.maxlego08.menu.api.exceptions.InventoryOpenException; import fr.maxlego08.menu.api.utils.ClearInvType; import fr.maxlego08.menu.common.utils.ZUtils; +import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -20,6 +21,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; import java.util.HashMap; @@ -28,7 +30,7 @@ public abstract class VInventory extends ZUtils implements Cloneable, BaseInventory { protected int id; - protected ZMenuPlugin plugin; + protected MenuPlugin plugin; protected final Map items = new HashMap<>(); protected final Map playerInventoryItems = new HashMap<>(); protected Player player; @@ -110,7 +112,8 @@ public ItemButton addItem(boolean inPlayerInventory, int slot, ItemStack itemSta this.createDefaultInventory(); if (itemStack == null) { - this.plugin.getLogger().severe("Attention, a null ItemStack was found in slot " + slot + " ! > " + this); + if (Configuration.enableDebug) + Logger.info("Attention, a null ItemStack was found in slot " + slot + " ! > " + this, Logger.LogType.ERROR); return null; } @@ -162,6 +165,11 @@ public void clearItem() { return this.items; } + @Nullable + public ItemButton getItem(int slot) { + return this.items.get(slot); + } + @Override public @NonNull Map getPlayerInventoryItems() { return this.playerInventoryItems; @@ -201,7 +209,7 @@ public String getGuiName() { return this.guiName; } - protected InventoryResult preOpenInventory(@NotNull ZMenuPlugin main, Player player, int page, Object... args) throws InventoryOpenException { + protected InventoryResult preOpenInventory(@NotNull MenuPlugin main, Player player, int page, Object... args) throws InventoryOpenException { this.page = page; this.args = args; @@ -211,9 +219,9 @@ protected InventoryResult preOpenInventory(@NotNull ZMenuPlugin main, Player pla return this.openInventory(main, player, page, args); } - public abstract InventoryResult openInventory(ZMenuPlugin main, Player player, int page, Object... args) throws InventoryOpenException; + public abstract InventoryResult openInventory(MenuPlugin main, Player player, int page, Object... args) throws InventoryOpenException; - protected void onPreClose(InventoryCloseEvent event, ZMenuPlugin plugin, Player player) { + protected void onPreClose(InventoryCloseEvent event, MenuPlugin plugin, Player player) { this.isClose = true; if (this.playerTitleAnimation != null){ this.playerTitleAnimation.stop(); @@ -221,17 +229,21 @@ protected void onPreClose(InventoryCloseEvent event, ZMenuPlugin plugin, Player this.onClose(event, plugin, player); } - protected void onClose(InventoryCloseEvent event, ZMenuPlugin plugin, Player player) { + protected void onClose(InventoryCloseEvent event, MenuPlugin plugin, Player player) { } - protected void onDrag(InventoryDragEvent event, ZMenuPlugin plugin, Player player) { + protected void onInventorySwitch(InventoryCloseEvent event, Player player, VInventory newInventoryEngine) { + this.onPreClose(event, this.plugin, player); } - public @NonNull ZMenuPlugin getPlugin() { + protected void onDrag(InventoryDragEvent event, MenuPlugin plugin, Player player) { + } + + public @NonNull MenuPlugin getPlugin() { return this.plugin; } - public void setPlugin(ZMenuPlugin plugin) { + public void setPlugin(MenuPlugin plugin) { this.plugin = plugin; } @@ -245,9 +257,11 @@ protected VInventory clone() { return null; } - public void postOpen(ZMenuPlugin plugin, Player player, int page, Object[] objects) { + public void postOpen(MenuPlugin plugin, Player player, int page, Object[] objects) { } + protected void onPostOpen(Player player, MenuPlugin plugin, int page, Object[] objects) {} + @Override public @NotNull Inventory getInventory() { return this.inventory; @@ -302,7 +316,7 @@ public boolean isClickLimiterEnabled() { return this.isClickLimiterEnabled; } - public void onInventoryClick(InventoryClickEvent event, ZMenuPlugin plugin, Player player) { + public void onInventoryClick(InventoryClickEvent event, MenuPlugin plugin, Player player) { } } diff --git a/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/AnvilInventoryDefault.java b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/AnvilInventoryDefault.java new file mode 100644 index 00000000..45466b88 --- /dev/null +++ b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/AnvilInventoryDefault.java @@ -0,0 +1,132 @@ +package fr.maxlego08.menu.inventory.inventories; + +import fr.maxlego08.menu.api.MenuPlugin; +import fr.maxlego08.menu.api.button.Button; +import fr.maxlego08.menu.api.engine.AnvilInventoryEngine; +import fr.maxlego08.menu.api.engine.ItemButton; +import fr.maxlego08.menu.api.inventory.AnvilInventory; +import fr.maxlego08.menu.api.requirement.Requirement; +import fr.maxlego08.menu.api.utils.Placeholders; +import fr.maxlego08.menu.api.utils.TextChange; +import fr.maxlego08.menu.api.utils.TextChangeType; +import fr.maxlego08.menu.common.network.NMSMenuPacketListener; +import fr.maxlego08.menu.common.network.PacketQueue; +import fr.maxlego08.menu.inventory.VInventory; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import net.minecraft.network.protocol.game.ServerboundRenameItemPacket; +import net.minecraft.world.item.ItemStack; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class AnvilInventoryDefault extends InventoryDefault implements AnvilInventoryEngine { + private PacketQueue> incomingPackets; + private String currentText = ""; + private volatile int containerId; + + + private boolean firstPacketReceived = false; + + private TextChange updateText(String newText) { + if (newText == null) newText = ""; + TextChange change = TextChange.compute(currentText, newText); + currentText = newText; + return change; + } + + @Override + protected void onPostOpen(Player player, MenuPlugin plugin, int page, Object[] objects) { + + this.containerId = ((CraftPlayer) player).getHandle().containerMenu.containerId; + + super.onPostOpen(player, plugin, page, objects); + } + + @Override + public @NotNull String getCurrentText() { + return this.currentText; + } + + @Override + public void postOpen(MenuPlugin plugin, Player player, int page, Object[] objects) { + incomingPackets = PacketQueue.>builder() + .on(ServerboundRenameItemPacket.class, packet -> { + if (!(this.getMenuInventory() instanceof AnvilInventory anvilInventory)) return; + + TextChange textChange = updateText(packet.getName()); + + if (!firstPacketReceived && textChange.type() == TextChangeType.EQUAL) { + // The client sends an initial packet with the default text when the menu is opened, ignore it + firstPacketReceived = true; + return; + } + + Placeholders placeholders = new Placeholders(); + + ItemButton item = this.getItem(2); + + if (item != null) { + ItemStack nmsCopy = CraftItemStack.asNMSCopy(item.getDisplayItem()); + ClientboundContainerSetSlotPacket clientboundContainerSetSlotPacket = new ClientboundContainerSetSlotPacket( + containerId, + 8, + 2, + nmsCopy + ); + + NMSMenuPacketListener.get().sendPacket(player, clientboundContainerSetSlotPacket); + } + + placeholders.register("type", textChange.type().name()); + placeholders.register("old_text", textChange.oldText()); + switch (textChange.type()) { + case ADDED, REMOVED -> { + placeholders.register("char", String.valueOf(textChange.changedChar())); + placeholders.register("new_text", textChange.newText()); + } + case CLEARED -> placeholders.register("new_text", ""); + case REPLACED, EQUAL -> placeholders.register("new_text", textChange.newText()); + } + + for (Requirement requirement : anvilInventory.getRenameRequirements()) { + requirement.execute(player, null, this, placeholders); + } + + List