From 6fa42ed339bbf228965d79fb3ba717bea93755fd Mon Sep 17 00:00:00 2001 From: BlitzOffline <52609756+BlitzOffline@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:12:47 +0300 Subject: [PATCH] Add context placeholders --- .../deluxemenus/action/ClickAction.java | 15 +- .../deluxemenus/action/ClickActionTask.java | 22 ++- .../deluxemenus/action/ClickHandler.java | 27 ++- .../command/subcommand/ExecuteCommand.java | 14 +- .../deluxemenus/config/DeluxeMenusConfig.java | 17 +- .../deluxemenus/listener/PlayerListener.java | 29 ++- .../extendedclip/deluxemenus/menu/Menu.java | 7 +- .../deluxemenus/menu/MenuHolder.java | 43 +++-- .../deluxemenus/menu/MenuItem.java | 92 +++++---- .../internal/ClickContextResolver.java | 39 ++++ .../placeholder/internal/ClickSnapshot.java | 73 +++++++ .../placeholder/internal/ContextResolver.java | 17 ++ .../internal/InternalPlaceholderResolver.java | 86 +++++++++ .../internal/ItemContextResolver.java | 41 ++++ .../placeholder/internal/ItemSnapshot.java | 78 ++++++++ .../internal/MenuContextResolver.java | 37 ++++ .../placeholder/internal/MenuSnapshot.java | 66 +++++++ .../internal/PlaceholderContext.java | 178 ++++++++++++++++++ .../internal/ViewerContextResolver.java | 27 +++ .../placeholder/internal/ViewerSnapshot.java | 36 ++++ .../requirement/HasExpRequirement.java | 7 +- .../requirement/HasItemRequirement.java | 39 ++-- .../requirement/HasMetaRequirement.java | 9 +- .../requirement/HasMoneyRequirement.java | 7 +- .../requirement/HasPermissionRequirement.java | 5 +- .../HasPermissionsRequirement.java | 5 +- .../requirement/InputResultRequirement.java | 7 +- .../requirement/IsNearRequirement.java | 3 +- .../requirement/IsObjectRequirement.java | 5 +- .../requirement/JavascriptRequirement.java | 5 +- .../requirement/RegexMatchesRequirement.java | 9 +- .../deluxemenus/requirement/Requirement.java | 3 +- .../requirement/RequirementList.java | 11 +- .../requirement/StringLengthRequirement.java | 5 +- .../deluxemenus/utils/StringUtils.java | 31 ++- 35 files changed, 955 insertions(+), 140 deletions(-) create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickContextResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickSnapshot.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ContextResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/InternalPlaceholderResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemContextResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemSnapshot.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuContextResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuSnapshot.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/PlaceholderContext.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerContextResolver.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerSnapshot.java diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java index 59d61685..985bfc04 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.action; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; import java.util.concurrent.ThreadLocalRandom; @@ -99,16 +100,17 @@ public void setChance(@Nullable final String chance) { /** * Get the parsed delay of this action. If the delay is null or can't be parsed to a {@link Long}, the delay will be 0. * - * @param holder the holder to parse placeholders in the delay for. + * @param holder the holder to parse placeholders in the delay for. + * @param context the context to resolve internal placeholders in the delay against. * @return the parsed delay */ @SuppressWarnings("UnstableApiUsage") - public long getDelay(@NotNull final MenuHolder holder) { + public long getDelay(@NotNull final MenuHolder holder, @NotNull final PlaceholderContext context) { if (delay == null || delay.isEmpty()) { return 0; } - final var parsed = Longs.tryParse(holder.setPlaceholdersAndArguments(delay)); + final var parsed = Longs.tryParse(holder.setPlaceholdersAndArguments(delay, context)); return parsed == null ? 0 : parsed; } @@ -116,16 +118,17 @@ public long getDelay(@NotNull final MenuHolder holder) { * Parses the chance of this action and tries it. If {@link #getChance()} is null this will return true but if it * can't be parsed to a {@link Double}, this will return false. * - * @param holder the holder to parse placeholders in the chance for. + * @param holder the holder to parse placeholders in the chance for. + * @param context the context to resolve internal placeholders in the chance against. * @return true if the chance has passed, false otherwise */ @SuppressWarnings("UnstableApiUsage") - public boolean checkChance(@NotNull final MenuHolder holder) { + public boolean checkChance(@NotNull final MenuHolder holder, @NotNull final PlaceholderContext context) { if (chance == null) { return true; } - final Double parsedChance = Doubles.tryParse(holder.setPlaceholdersAndArguments(this.chance)); + final Double parsedChance = Doubles.tryParse(holder.setPlaceholdersAndArguments(this.chance, context)); if (parsedChance == null) { return false; } diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index 6d728108..4849cb48 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -4,6 +4,7 @@ import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.*; import net.kyori.adventure.text.minimessage.MiniMessage; import net.md_5.bungee.api.ChatMessageType; @@ -25,6 +26,9 @@ public class ClickActionTask extends BukkitRunnable { private final String exec; // Ugly hack to get around the fact that arguments are not available at task execution time private final Map arguments; + // Same idea, for the same reason: the menu, item and click this action came from no longer + // exist by the time a delayed action runs, so they are snapshotted at click time. + private final PlaceholderContext context; private final boolean parsePlaceholdersInArguments; private final boolean parsePlaceholdersAfterArguments; @@ -36,6 +40,20 @@ public ClickActionTask( @NotNull final Map arguments, final boolean parsePlaceholdersInArguments, final boolean parsePlaceholdersAfterArguments + ) { + this(plugin, uuid, actionType, exec, arguments, parsePlaceholdersInArguments, + parsePlaceholdersAfterArguments, PlaceholderContext.EMPTY); + } + + public ClickActionTask( + @NotNull final DeluxeMenus plugin, + @NotNull final UUID uuid, + @NotNull final ActionType actionType, + @NotNull final String exec, + @NotNull final Map arguments, + final boolean parsePlaceholdersInArguments, + final boolean parsePlaceholdersAfterArguments, + @NotNull final PlaceholderContext context ) { this.plugin = plugin; this.uuid = uuid; @@ -44,6 +62,7 @@ public ClickActionTask( this.arguments = arguments; this.parsePlaceholdersInArguments = parsePlaceholdersInArguments; this.parsePlaceholdersAfterArguments = parsePlaceholdersAfterArguments; + this.context = context; } @Override @@ -64,7 +83,8 @@ public void run() { this.arguments, target, this.parsePlaceholdersInArguments, - this.parsePlaceholdersAfterArguments); + this.parsePlaceholdersAfterArguments, + this.context); switch (actionType) { case META: diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickHandler.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickHandler.java index 0a1d1dc0..99c066af 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickHandler.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickHandler.java @@ -1,9 +1,18 @@ -package com.extendedclip.deluxemenus.action; - -import com.extendedclip.deluxemenus.menu.MenuHolder; -import org.jetbrains.annotations.NotNull; - -public interface ClickHandler { - - void onClick(@NotNull final MenuHolder menuHolder); -} +package com.extendedclip.deluxemenus.action; + +import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; +import org.jetbrains.annotations.NotNull; + +public interface ClickHandler { + + void onClick(@NotNull final MenuHolder menuHolder, @NotNull final PlaceholderContext context); + + /** + * Runs the handler with menu context only. Used by the open and close handlers, where there is no + * item and no click to report. + */ + default void onClick(@NotNull final MenuHolder menuHolder) { + onClick(menuHolder, PlaceholderContext.of(menuHolder)); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java index c1ce0da1..6828e9d7 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java @@ -7,6 +7,7 @@ import com.extendedclip.deluxemenus.config.DeluxeMenusConfig; import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.Messages; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -79,17 +80,22 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List handler, final @NotNull Optional requirements) { + private boolean handleClick(final @NotNull Player player, final @NotNull MenuHolder holder, final @NotNull PlaceholderContext context, final @NotNull Optional handler, final @NotNull Optional requirements) { if (handler.isEmpty()) { return false; } @@ -203,18 +212,18 @@ private boolean handleClick(final @NotNull Player player, final @NotNull MenuHol if (requirements.isPresent()) { final ClickHandler denyHandler = requirements.get().getDenyHandler(); - if (!requirements.get().evaluate(holder)) { + if (!requirements.get().evaluate(holder, context)) { if (denyHandler == null) { return true; } - denyHandler.onClick(holder); + denyHandler.onClick(holder, context); return true; } } this.cache.put(player.getUniqueId(), System.currentTimeMillis()); - handler.get().onClick(holder); + handler.get().onClick(holder, context); return true; } diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 615bedf7..8bd9ae50 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -7,6 +7,7 @@ import com.extendedclip.deluxemenus.events.DeluxeMenusPreOpenMenuEvent; import com.extendedclip.deluxemenus.menu.command.RegistrableMenuCommand; import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.requirement.RequirementList; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.StringUtils; @@ -285,6 +286,9 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map h.onClick(holder)); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index b101a365..9b002bfd 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.StringUtils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -97,10 +98,19 @@ public Optional getMenu() { } public @NotNull String setPlaceholdersAndArguments(final @NotNull String string) { - if (parsePlaceholdersAfterArguments) { - return setPlaceholders(setArguments(string)); - } - return setArguments(setPlaceholders(string)); + return setPlaceholdersAndArguments(string, PlaceholderContext.of(this)); + } + + public @NotNull String setPlaceholdersAndArguments(final @NotNull String string, + final @NotNull PlaceholderContext context) { + return StringUtils.replacePlaceholdersAndArguments( + string, + this.typedArgs, + this.placeholderPlayer != null ? this.placeholderPlayer : getViewer(), + this.parsePlaceholdersInArguments, + this.parsePlaceholdersAfterArguments, + context + ); } public @NotNull String setPlaceholders(final @NotNull String string) { @@ -155,7 +165,7 @@ public void refreshMenu() { if (item.options().viewRequirements().isPresent()) { - if (item.options().viewRequirements().get().evaluate(this)) { + if (item.options().viewRequirements().get().evaluate(this, PlaceholderContext.of(this).withItem(item))) { m = true; active.add(item); break; @@ -284,35 +294,46 @@ public void run() { continue; } + ItemMeta meta = i.getItemMeta(); + + // Without the item context, an item mixing PlaceholderAPI and internal + // placeholders would lose its internal values on the first update tick. + // The context is grown in the same order MenuItem.getItemStack grows it, so + // an update tick resolves item placeholders exactly like the initial build: + // no stack values while the amount is being computed, then the stack, then + // the new display name before the lore is parsed. + PlaceholderContext context = PlaceholderContext.of(getHolder()).withItem(item); + int amt = i.getAmount(); if (item.options().dynamicAmount().isPresent()) { try { - amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get())); + amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get(), context)); if (amt <= 0) { amt = 1; } } catch (Exception exception) { plugin.printStacktrace( "Something went wrong while updating item in slot " + item.options().slot() + - ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get()), + ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get(), context), exception ); } } - ItemMeta meta = i.getItemMeta(); + i.setAmount(amt); + context = context.withItemStack(i, meta); if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { - meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); + meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get(), context))); + context = context.withItemStack(i, meta); } if (item.options().loreHasPlaceholders()) { - meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); + meta.setLore(item.getMenuItemLore(getHolder(), context, item.options().lore())); } i.setItemMeta(meta); - i.setAmount(amt); } } } diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java index 79fbd8eb..06d3aadf 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java @@ -7,6 +7,7 @@ import com.extendedclip.deluxemenus.menu.options.MenuItemOptions; import com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent; import com.extendedclip.deluxemenus.nbt.NbtProvider; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.ItemUtils; import com.extendedclip.deluxemenus.utils.StringUtils; @@ -92,6 +93,11 @@ public static ItemStack base64ToItemStack(String data) { public ItemStack getItemStack(@NotNull final MenuHolder holder) { final Player viewer = holder.getViewer(); + // Built once and threaded through every parse call below instead of being rebuilt per + // string. It is reassigned once the stack exists, so that %item.material% and friends + // resolve in the display name and lore. + PlaceholderContext context = PlaceholderContext.of(holder).withItem(this); + ItemStack itemStack = null; int amount = 1; @@ -99,11 +105,11 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { String lowercaseStringMaterial = stringMaterial.toLowerCase(Locale.ROOT); if (ItemUtils.isPlaceholderOption(lowercaseStringMaterial)) { - stringMaterial = holder.setPlaceholdersAndArguments(stringMaterial.substring(PLACEHOLDER_PREFIX.length())); + stringMaterial = holder.setPlaceholdersAndArguments(stringMaterial.substring(PLACEHOLDER_PREFIX.length()), context); lowercaseStringMaterial = stringMaterial.toLowerCase(Locale.ENGLISH); } if (ItemUtils.isItemStackOption(lowercaseStringMaterial)) { - stringMaterial = holder.setPlaceholdersAndArguments(stringMaterial.substring(STACK_PREFIX.length())); + stringMaterial = holder.setPlaceholdersAndArguments(stringMaterial.substring(STACK_PREFIX.length()), context); ItemStack base64Item = base64ToItemStack(stringMaterial); if (base64Item != null) { itemStack = base64Item; @@ -136,7 +142,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (pluginHook != null) { itemStack = pluginHook.getItem( viewer, - holder.setPlaceholdersAndArguments(stringMaterial.substring(pluginHook.getPrefix().length())) + holder.setPlaceholdersAndArguments(stringMaterial.substring(pluginHook.getPrefix().length()), context) ); } @@ -194,7 +200,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (meta != null) { if (this.options.rgb().isPresent()) { - final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); + final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get(), context)); if (color != null) { meta.setColor(color); } @@ -215,7 +221,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } if (this.options.damage().isPresent()) { - final String parsedDamage = holder.setPlaceholdersAndArguments(this.options.damage().get()); + final String parsedDamage = holder.setPlaceholdersAndArguments(this.options.damage().get(), context); try { int damage = Integer.parseInt(parsedDamage); if (damage > 0) { @@ -239,7 +245,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (this.options.dynamicAmount().isPresent()) { try { - final int dynamicAmount = (int) Double.parseDouble(holder.setPlaceholdersAndArguments(this.options.dynamicAmount().get())); + final int dynamicAmount = (int) Double.parseDouble(holder.setPlaceholdersAndArguments(this.options.dynamicAmount().get(), context)); amount = Math.max(dynamicAmount, 1); } catch (final NumberFormatException ignored) { } @@ -259,19 +265,26 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (VersionHelper.IS_CUSTOM_MODEL_DATA && this.options.customModelData().isPresent()) { try { - final int modelData = Integer.parseInt(holder.setPlaceholdersAndArguments(this.options.customModelData().get())); + final int modelData = Integer.parseInt(holder.setPlaceholdersAndArguments(this.options.customModelData().get(), context)); itemMeta.setCustomModelData(modelData); } catch (final Exception ignored) { } } if (VersionHelper.IS_CUSTOM_MODEL_DATA_COMPONENT && this.options.customModelDataComponent().isPresent()) { - itemMeta.setCustomModelDataComponent(parseCustomModelDataComponent(this.options.customModelDataComponent().get(), itemMeta.getCustomModelDataComponent(), holder)); + itemMeta.setCustomModelDataComponent(parseCustomModelDataComponent(this.options.customModelDataComponent().get(), itemMeta.getCustomModelDataComponent(), holder, context)); } + // From here on the stack, its amount and its model data are known, so item placeholders can + // be resolved in everything that follows - most importantly the display name and the lore. + context = context.withItemStack(itemStack, itemMeta); + if (this.options.displayName().isPresent()) { - final String displayName = holder.setPlaceholdersAndArguments(this.options.displayName().get()); + final String displayName = holder.setPlaceholdersAndArguments(this.options.displayName().get(), context); itemMeta.setDisplayName(StringUtils.color(displayName)); + // Re-snapshot so the lore below can use %item.display_name%. Inside display_name itself + // it is still unset, which is why it resolves to "" there. + context = context.withItemStack(itemStack, itemMeta); } List lore = new ArrayList<>(); @@ -286,15 +299,15 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { lore.addAll(itemLore); break; case TOP: // DM lore is added at the top - lore.addAll(getMenuItemLore(holder, this.options.lore())); + lore.addAll(getMenuItemLore(holder, context, this.options.lore())); lore.addAll(itemLore); break; case BOTTOM: // DM lore is bottom at the bottom lore.addAll(itemLore); - lore.addAll(getMenuItemLore(holder, this.options.lore())); + lore.addAll(getMenuItemLore(holder, context, this.options.lore())); break; case OVERRIDE: // Lore from DM overrides the lore from the item - lore.addAll(getMenuItemLore(holder, this.options.lore())); + lore.addAll(getMenuItemLore(holder, context, this.options.lore())); break; } @@ -306,15 +319,15 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (VersionHelper.HAS_DATA_COMPONENTS) { if (this.options.hideTooltip().isPresent()) { - String hideTooltip = holder.setPlaceholdersAndArguments(this.options.hideTooltip().get()); + String hideTooltip = holder.setPlaceholdersAndArguments(this.options.hideTooltip().get(), context); itemMeta.setHideTooltip(Boolean.parseBoolean(hideTooltip)); } if (this.options.enchantmentGlintOverride().isPresent()) { - String enchantmentGlintOverride = holder.setPlaceholdersAndArguments(this.options.enchantmentGlintOverride().get()); + String enchantmentGlintOverride = holder.setPlaceholdersAndArguments(this.options.enchantmentGlintOverride().get(), context); itemMeta.setEnchantmentGlintOverride(Boolean.parseBoolean(enchantmentGlintOverride)); } if (this.options.rarity().isPresent()) { - String rarity = holder.setPlaceholdersAndArguments(this.options.rarity().get()); + String rarity = holder.setPlaceholdersAndArguments(this.options.rarity().get(), context); try { itemMeta.setRarity(ItemRarity.valueOf(rarity.toUpperCase())); } catch (IllegalArgumentException e) { @@ -328,11 +341,11 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } if (VersionHelper.HAS_TOOLTIP_STYLE) { if (this.options.tooltipStyle().isPresent()) { - NamespacedKey tooltipStyle = NamespacedKey.fromString(holder.setPlaceholdersAndArguments(this.options.tooltipStyle().get())); + NamespacedKey tooltipStyle = NamespacedKey.fromString(holder.setPlaceholdersAndArguments(this.options.tooltipStyle().get(), context)); if (tooltipStyle != null) itemMeta.setTooltipStyle(tooltipStyle); } if (this.options.itemModel().isPresent()) { - NamespacedKey itemModel = NamespacedKey.fromString(holder.setPlaceholdersAndArguments(this.options.itemModel().get())); + NamespacedKey itemModel = NamespacedKey.fromString(holder.setPlaceholdersAndArguments(this.options.itemModel().get(), context)); if (itemModel != null) itemMeta.setItemModel(itemModel); } } @@ -342,8 +355,8 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { final Optional trimPatternName = this.options.trimPattern(); if (trimMaterialName.isPresent() && trimPatternName.isPresent()) { - final TrimMaterial trimMaterial = Registry.TRIM_MATERIAL.match(holder.setPlaceholdersAndArguments(trimMaterialName.get())); - final TrimPattern trimPattern = Registry.TRIM_PATTERN.match(holder.setPlaceholdersAndArguments(trimPatternName.get())); + final TrimMaterial trimMaterial = Registry.TRIM_MATERIAL.match(holder.setPlaceholdersAndArguments(trimMaterialName.get(), context)); + final TrimPattern trimPattern = Registry.TRIM_PATTERN.match(holder.setPlaceholdersAndArguments(trimPatternName.get(), context)); if (trimMaterial != null && trimPattern != null) { final ArmorTrim armorTrim = new ArmorTrim(trimMaterial, trimPattern); @@ -385,7 +398,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (itemMeta instanceof LeatherArmorMeta && this.options.rgb().isPresent()) { final LeatherArmorMeta leatherArmorMeta = (LeatherArmorMeta) itemMeta; - final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); + final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get(), context)); if (color != null) { leatherArmorMeta.setColor(color); } else { @@ -399,7 +412,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { itemStack.setItemMeta(leatherArmorMeta); } else if (itemMeta instanceof FireworkEffectMeta && this.options.rgb().isPresent()) { final FireworkEffectMeta fireworkEffectMeta = (FireworkEffectMeta) itemMeta; - final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get())); + final Color color = parseRGBColor(holder.setPlaceholdersAndArguments(this.options.rgb().get(), context)); if (color != null) { fireworkEffectMeta.setEffect(FireworkEffect.builder().withColor(color).build()); } else { @@ -436,7 +449,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { final BlockData blockData = blockDataMeta.getBlockData(itemStack.getType()); if (blockData instanceof Light) { final Light light = (Light) blockData; - final String parsedLightLevel = holder.setPlaceholdersAndArguments(this.options.lightLevel().get()); + final String parsedLightLevel = holder.setPlaceholdersAndArguments(this.options.lightLevel().get(), context); try { final int lightLevel = Math.min(Integer.parseInt(parsedLightLevel), light.getMaximumLevel()); light.setLevel(Math.max(lightLevel, 0)); @@ -479,7 +492,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { if (NbtProvider.isAvailable()) { if (this.options.nbtString().isPresent()) { - final String tag = holder.setPlaceholdersAndArguments(this.options.nbtString().get()); + final String tag = holder.setPlaceholdersAndArguments(this.options.nbtString().get(), context); if (tag.contains(":")) { final String[] parts = tag.split(":", 2); itemStack = NbtProvider.setString(itemStack, parts[0], parts[1]); @@ -487,7 +500,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } if (this.options.nbtByte().isPresent()) { - final String tag = holder.setPlaceholdersAndArguments(this.options.nbtByte().get()); + final String tag = holder.setPlaceholdersAndArguments(this.options.nbtByte().get(), context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setByte(itemStack, parts[0], Byte.parseByte(parts[1])); @@ -495,7 +508,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } if (this.options.nbtShort().isPresent()) { - final String tag = holder.setPlaceholdersAndArguments(this.options.nbtShort().get()); + final String tag = holder.setPlaceholdersAndArguments(this.options.nbtShort().get(), context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setShort(itemStack, parts[0], Short.parseShort(parts[1])); @@ -503,7 +516,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } if (this.options.nbtInt().isPresent()) { - final String tag = holder.setPlaceholdersAndArguments(this.options.nbtInt().get()); + final String tag = holder.setPlaceholdersAndArguments(this.options.nbtInt().get(), context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setInt(itemStack, parts[0], Integer.parseInt(parts[1])); @@ -511,7 +524,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } for (String nbtTag : this.options.nbtStrings()) { - final String tag = holder.setPlaceholdersAndArguments(nbtTag); + final String tag = holder.setPlaceholdersAndArguments(nbtTag, context); if (tag.contains(":")) { final String[] parts = tag.split(":", 2); itemStack = NbtProvider.setString(itemStack, parts[0], parts[1]); @@ -519,7 +532,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } for (String nbtTag : this.options.nbtBytes()) { - final String tag = holder.setPlaceholdersAndArguments(nbtTag); + final String tag = holder.setPlaceholdersAndArguments(nbtTag, context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setByte(itemStack, parts[0], Byte.parseByte(parts[1])); @@ -527,7 +540,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } for (String nbtTag : this.options.nbtShorts()) { - final String tag = holder.setPlaceholdersAndArguments(nbtTag); + final String tag = holder.setPlaceholdersAndArguments(nbtTag, context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setShort(itemStack, parts[0], Short.parseShort(parts[1])); @@ -535,7 +548,7 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } for (String nbtTag : this.options.nbtInts()) { - final String tag = holder.setPlaceholdersAndArguments(nbtTag); + final String tag = holder.setPlaceholdersAndArguments(nbtTag, context); if (tag.contains(":")) { final String[] parts = tag.split(":"); itemStack = NbtProvider.setInt(itemStack, parts[0], Integer.parseInt(parts[1])); @@ -570,8 +583,14 @@ private boolean isHeadItem(@NotNull final String material) { } protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNull final List lore) { + return getMenuItemLore(holder, PlaceholderContext.of(holder).withItem(this), lore); + } + + protected List getMenuItemLore(@NotNull final MenuHolder holder, + @NotNull final PlaceholderContext context, + @NotNull final List lore) { return lore.stream() - .map(holder::setPlaceholdersAndArguments) + .map(line -> holder.setPlaceholdersAndArguments(line, context)) .map(StringUtils::color) .map(line -> line.split("\n")) .flatMap(Arrays::stream) @@ -583,12 +602,13 @@ protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNul private @NotNull org.bukkit.inventory.meta.components.CustomModelDataComponent parseCustomModelDataComponent( @NotNull final CustomModelDataComponent unparsedComponent, @NotNull final org.bukkit.inventory.meta.components.CustomModelDataComponent component, - @NotNull final MenuHolder holder + @NotNull final MenuHolder holder, + @NotNull final PlaceholderContext context ) { if (!unparsedComponent.colors().isEmpty()) { final List colors = unparsedComponent.colors() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(this::parseRGBColor) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -598,7 +618,7 @@ protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNul if (!unparsedComponent.flags().isEmpty()) { final List flags = unparsedComponent.flags() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(Boolean::parseBoolean) .collect(Collectors.toList()); component.setFlags(flags); @@ -607,7 +627,7 @@ protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNul if (!unparsedComponent.floats().isEmpty()) { final List floats = unparsedComponent.floats() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(Float::parseFloat) .collect(Collectors.toList()); component.setFloats(floats); @@ -616,7 +636,7 @@ protected List getMenuItemLore(@NotNull final MenuHolder holder, @NotNul if (!unparsedComponent.strings().isEmpty()) { final List strings = unparsedComponent.strings() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .collect(Collectors.toList()); component.setStrings(strings); } diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickContextResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickContextResolver.java new file mode 100644 index 00000000..8f26939f --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickContextResolver.java @@ -0,0 +1,39 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ClickContextResolver implements ContextResolver { + + @Override + public @Nullable String resolve(final @NotNull String key, final @NotNull PlaceholderContext context) { + final ClickSnapshot click = context.click(); + + if (click == null) { + return null; + } + + switch (key) { + case "type": + return click.type(); + case "action": + return click.action(); + case "slot": + return String.valueOf(click.slot()); + case "raw_slot": + return String.valueOf(click.rawSlot()); + case "hotbar_button": + return String.valueOf(click.hotbarButton()); + case "cursor_material": + return click.cursorMaterial(); + case "is_left": + return InternalPlaceholderResolver.bool(click.left()); + case "is_right": + return InternalPlaceholderResolver.bool(click.right()); + case "is_shift": + return InternalPlaceholderResolver.bool(click.shift()); + default: + return null; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickSnapshot.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickSnapshot.java new file mode 100644 index 00000000..3dba6053 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ClickSnapshot.java @@ -0,0 +1,73 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; + +/** + * Immutable snapshot of an {@code InventoryClickEvent}. + *

+ * The event itself is never kept: it is read once, at click time, and discarded. That is what lets + * a delayed action still report the click that started it. + */ +public final class ClickSnapshot { + + private final String type; + private final String action; + private final int slot; + private final int rawSlot; + private final int hotbarButton; + private final String cursorMaterial; + private final boolean left; + private final boolean right; + private final boolean shift; + + public ClickSnapshot(final @NotNull String type, final @NotNull String action, final int slot, + final int rawSlot, final int hotbarButton, + final @NotNull String cursorMaterial, final boolean left, + final boolean right, final boolean shift) { + this.type = type; + this.action = action; + this.slot = slot; + this.rawSlot = rawSlot; + this.hotbarButton = hotbarButton; + this.cursorMaterial = cursorMaterial; + this.left = left; + this.right = right; + this.shift = shift; + } + + public @NotNull String type() { + return type; + } + + public @NotNull String action() { + return action; + } + + public int slot() { + return slot; + } + + public int rawSlot() { + return rawSlot; + } + + public int hotbarButton() { + return hotbarButton; + } + + public @NotNull String cursorMaterial() { + return cursorMaterial; + } + + public boolean left() { + return left; + } + + public boolean right() { + return right; + } + + public boolean shift() { + return shift; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ContextResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ContextResolver.java new file mode 100644 index 00000000..16fb5f84 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ContextResolver.java @@ -0,0 +1,17 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Resolves the keys of a single internal placeholder context. + */ +public interface ContextResolver { + + /** + * @param key the part after the dot, always lower case + * @param context the context to resolve against + * @return the replacement value, or null to leave the placeholder in the text untouched + */ + @Nullable String resolve(final @NotNull String key, final @NotNull PlaceholderContext context); +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/InternalPlaceholderResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/InternalPlaceholderResolver.java new file mode 100644 index 00000000..31a3cbc3 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/InternalPlaceholderResolver.java @@ -0,0 +1,86 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resolves DeluxeMenus' own {@code %.%} placeholders. + *

+ * The dot is what keeps this from colliding with PlaceholderAPI: PAPI's own pattern requires + * {@code %} followed by {@code [a-zA-Z0-9]+} followed by {@code _}, and no context name contains an + * underscore, so PAPI can never match one of these. Placeholders belonging to another plugin that + * happen to contain a dot are left alone too, because only the registered context names below are + * ever substituted. + *

+ * This is a static utility because the ordering of the whole parsing pipeline lives in + * {@link com.extendedclip.deluxemenus.utils.StringUtils}, which has no plugin instance. + */ +public class InternalPlaceholderResolver { + + private static final Pattern PATTERN = Pattern + .compile("%(?[a-zA-Z0-9]+)\\.(?[a-zA-Z0-9_.]+)%"); + + private static final Map RESOLVERS = new HashMap<>(); + + static { + RESOLVERS.put("menu", new MenuContextResolver()); + RESOLVERS.put("viewer", new ViewerContextResolver()); + RESOLVERS.put("item", new ItemContextResolver()); + RESOLVERS.put("click", new ClickContextResolver()); + } + + private static volatile String trueValue = "true"; + private static volatile String falseValue = "false"; + + private InternalPlaceholderResolver() { + } + + /** + * Sets the strings booleans are rendered as. Read from {@code config.yml} on load and reload. + */ + public static void setBooleanValues(final @NotNull String trueValue, final @NotNull String falseValue) { + InternalPlaceholderResolver.trueValue = trueValue; + InternalPlaceholderResolver.falseValue = falseValue; + } + + public static @NotNull String bool(final boolean value) { + return value ? trueValue : falseValue; + } + + /** + * Replaces every known internal placeholder in the input. + *

+ * Unknown contexts and unknown keys are left in the text untouched, and replacement values are + * never scanned again, so a display name containing a {@code %} cannot cause a loop. + */ + public static @NotNull String resolve(final @NotNull String input, final @NotNull PlaceholderContext context) { + if (input.indexOf('%') == -1) { + return input; + } + + final Matcher matcher = PATTERN.matcher(input); + + if (!matcher.find()) { + return input; + } + + final StringBuilder builder = new StringBuilder(); + + do { + final ContextResolver resolver = RESOLVERS.get(matcher.group("context").toLowerCase()); + final String value = resolver == null + ? null + : resolver.resolve(matcher.group("key").toLowerCase(), context); + + matcher.appendReplacement(builder, Matcher.quoteReplacement(value == null ? matcher.group() : value)); + } while (matcher.find()); + + matcher.appendTail(builder); + + return builder.toString(); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemContextResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemContextResolver.java new file mode 100644 index 00000000..79f7d917 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemContextResolver.java @@ -0,0 +1,41 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ItemContextResolver implements ContextResolver { + + @Override + public @Nullable String resolve(final @NotNull String key, final @NotNull PlaceholderContext context) { + final ItemSnapshot item = context.item(); + + if (item == null) { + return null; + } + + switch (key) { + case "slot": + return String.valueOf(item.slot()); + case "row": + return String.valueOf(item.slot() / 9 + 1); + case "column": + return String.valueOf(item.slot() % 9 + 1); + case "priority": + return String.valueOf(item.priority()); + case "update": + return InternalPlaceholderResolver.bool(item.update()); + // The values below only exist once the ItemStack has been built. Before that they are + // left in the text untouched, which is what happens inside a view_requirement. + case "material": + return item.material(); + case "amount": + return item.amount() == null ? null : String.valueOf(item.amount()); + case "model_data": + return item.material() == null ? null : (item.modelData() == null ? "" : item.modelData()); + case "display_name": + return item.material() == null ? null : (item.displayName() == null ? "" : item.displayName()); + default: + return null; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemSnapshot.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemSnapshot.java new file mode 100644 index 00000000..6dee2f38 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ItemSnapshot.java @@ -0,0 +1,78 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Immutable snapshot of the menu item a {@link PlaceholderContext} was created for. + *

+ * Config-side values ({@link #slot()}, {@link #priority()}, {@link #update()}) are known as soon as + * the {@code MenuItem} is known. The built-stack values ({@link #material()}, {@link #amount()}, + * {@link #modelData()}, {@link #displayName()}) only exist once the {@code ItemStack} has been + * built, so they are filled in later through {@link #withStack(String, int, String, String)} and are + * null until then. + */ +public final class ItemSnapshot { + + private final int slot; + private final int priority; + private final boolean update; + + private final String material; + private final Integer amount; + private final String modelData; + private final String displayName; + + public ItemSnapshot(final int slot, final int priority, final boolean update) { + this(slot, priority, update, null, null, null, null); + } + + private ItemSnapshot(final int slot, final int priority, final boolean update, + final @Nullable String material, final @Nullable Integer amount, + final @Nullable String modelData, final @Nullable String displayName) { + this.slot = slot; + this.priority = priority; + this.update = update; + this.material = material; + this.amount = amount; + this.modelData = modelData; + this.displayName = displayName; + } + + /** + * Returns a copy of this snapshot with the built-stack values filled in. + */ + public @NotNull ItemSnapshot withStack(final @Nullable String material, final int amount, + final @Nullable String modelData, + final @Nullable String displayName) { + return new ItemSnapshot(slot, priority, update, material, amount, modelData, displayName); + } + + public int slot() { + return slot; + } + + public int priority() { + return priority; + } + + public boolean update() { + return update; + } + + public @Nullable String material() { + return material; + } + + public @Nullable Integer amount() { + return amount; + } + + public @Nullable String modelData() { + return modelData; + } + + public @Nullable String displayName() { + return displayName; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuContextResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuContextResolver.java new file mode 100644 index 00000000..bf7d14f3 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuContextResolver.java @@ -0,0 +1,37 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class MenuContextResolver implements ContextResolver { + + @Override + public @Nullable String resolve(final @NotNull String key, final @NotNull PlaceholderContext context) { + final MenuSnapshot menu = context.menu(); + + if (menu == null) { + return null; + } + + switch (key) { + case "name": + return menu.name(); + case "title": + return menu.title(); + case "type": + return menu.type(); + case "size": + return String.valueOf(menu.size()); + case "rows": + return String.valueOf(menu.size() / 9); + case "item_count": + return menu.itemCount() == null ? null : String.valueOf(menu.itemCount()); + case "open_command": + return menu.openCommand(); + case "has_placeholder_player": + return InternalPlaceholderResolver.bool(menu.hasPlaceholderPlayer()); + default: + return null; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuSnapshot.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuSnapshot.java new file mode 100644 index 00000000..06e3025a --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/MenuSnapshot.java @@ -0,0 +1,66 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Immutable snapshot of the menu a {@link PlaceholderContext} was created for. + *

+ * Only plain values are kept here. Nothing in this class references the {@code MenuHolder}, the + * {@code Menu} or any Bukkit object, which is what makes a context safe to hand to a task running + * on a later tick. + */ +public final class MenuSnapshot { + + private final String name; + private final String title; + private final String type; + private final int size; + private final Integer itemCount; + private final String openCommand; + private final boolean hasPlaceholderPlayer; + + public MenuSnapshot(final @NotNull String name, final @NotNull String title, + final @NotNull String type, final int size, final @Nullable Integer itemCount, + final @NotNull String openCommand, final boolean hasPlaceholderPlayer) { + this.name = name; + this.title = title; + this.type = type; + this.size = size; + this.itemCount = itemCount; + this.openCommand = openCommand; + this.hasPlaceholderPlayer = hasPlaceholderPlayer; + } + + public @NotNull String name() { + return name; + } + + public @NotNull String title() { + return title; + } + + public @NotNull String type() { + return type; + } + + public int size() { + return size; + } + + /** + * Null until the menu's active items are known - before that the count is left literal rather + * than reported as 0. + */ + public @Nullable Integer itemCount() { + return itemCount; + } + + public @NotNull String openCommand() { + return openCommand; + } + + public boolean hasPlaceholderPlayer() { + return hasPlaceholderPlayer; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/PlaceholderContext.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/PlaceholderContext.java new file mode 100644 index 00000000..e5f9bc0d --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/PlaceholderContext.java @@ -0,0 +1,178 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.menu.MenuItem; +import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.utils.VersionHelper; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * The data internal placeholders are resolved against. + *

+ * A context holds only snapshotted values - never a {@code MenuHolder}, a {@code MenuItem}, + * an {@code ItemStack} or an event. Nothing in it can go stale and it does not keep a closed menu + * alive, which is why the same context can be built on the async item-building thread, evaluated on + * the main thread during a click, and stored inside a {@code ClickActionTask} that runs several + * ticks later. + *

+ * A null group means "this context is not available here", and its placeholders are left in the + * text untouched. + */ +public final class PlaceholderContext { + + public static final PlaceholderContext EMPTY = new PlaceholderContext(null, null, null, null); + + private final MenuSnapshot menu; + private final ViewerSnapshot viewer; + private final ItemSnapshot item; + private final ClickSnapshot click; + + private PlaceholderContext(final @Nullable MenuSnapshot menu, final @Nullable ViewerSnapshot viewer, + final @Nullable ItemSnapshot item, final @Nullable ClickSnapshot click) { + this.menu = menu; + this.viewer = viewer; + this.item = item; + this.click = click; + } + + /** + * Snapshots the menu and the viewer of the given holder. + */ + public static @NotNull PlaceholderContext of(final @Nullable MenuHolder holder) { + if (holder == null) { + return EMPTY; + } + + final String menuName = holder.getMenuName(); + final MenuOptions options = menuName == null + ? null + : Menu.getMenuByName(menuName).map(Menu::options).orElse(null); + + // No menu resolved - for example /dm execute against a player who is not in a menu. The + // group stays null so %menu.*% is left literal rather than silently rendering as empty. + final MenuSnapshot menuSnapshot; + if (options == null) { + menuSnapshot = null; + } else { + final List commands = options.commands(); + menuSnapshot = new MenuSnapshot( + menuName, + options.title(), + options.type().name(), + // Menu.openMenu only passes `size` to Bukkit.createInventory for CHEST menus; + // every other type gets its own fixed size, so `size:` would be misleading. + options.type() == InventoryType.CHEST + ? options.size() + : options.type().getDefaultSize(), + holder.getActiveItems() == null ? null : holder.getActiveItems().size(), + commands.isEmpty() ? "" : commands.get(0), + holder.getPlaceholderPlayer() != null + ); + } + + final Player viewer = holder.getViewer(); + final ViewerSnapshot viewerSnapshot = viewer == null ? null : new ViewerSnapshot( + viewer.getName(), + viewer.getUniqueId().toString(), + viewer.getDisplayName() + ); + + return new PlaceholderContext(menuSnapshot, viewerSnapshot, null, null); + } + + /** + * Returns a copy of this context carrying the config side values of the given item. + */ + public @NotNull PlaceholderContext withItem(final @Nullable MenuItem item) { + if (item == null) { + return this; + } + + return new PlaceholderContext(menu, viewer, new ItemSnapshot( + item.options().slot(), + item.options().priority(), + item.options().updatePlaceholders() + ), click); + } + + /** + * Returns a copy of this context whose item snapshot also carries the values of the built + * {@link ItemStack}. The stack and its meta are read here and then dropped. + *

+ * Does nothing when no item is in context yet - call {@link #withItem(MenuItem)} first. + */ + public @NotNull PlaceholderContext withItemStack(final @Nullable ItemStack itemStack, + final @Nullable ItemMeta itemMeta) { + if (item == null || itemStack == null) { + return this; + } + + String modelData = null; + String displayName = null; + + if (itemMeta != null) { + if (VersionHelper.IS_CUSTOM_MODEL_DATA && itemMeta.hasCustomModelData()) { + modelData = String.valueOf(itemMeta.getCustomModelData()); + } + if (itemMeta.hasDisplayName()) { + displayName = itemMeta.getDisplayName(); + } + } + + return new PlaceholderContext(menu, viewer, item.withStack( + itemStack.getType().name(), + itemStack.getAmount(), + modelData, + displayName + ), click); + } + + /** + * Returns a copy of this context carrying a snapshot of the given click. The event is read here + * and then dropped. + */ + public @NotNull PlaceholderContext withClick(final @Nullable InventoryClickEvent event) { + if (event == null) { + return this; + } + + final ItemStack cursor = event.getCursor(); + + return new PlaceholderContext(menu, viewer, item, new ClickSnapshot( + event.getClick().name(), + event.getAction().name(), + event.getSlot(), + event.getRawSlot(), + event.getHotbarButton(), + cursor == null ? "AIR" : cursor.getType().name(), + event.isLeftClick(), + event.isRightClick(), + event.isShiftClick() + )); + } + + public @Nullable MenuSnapshot menu() { + return menu; + } + + public @Nullable ViewerSnapshot viewer() { + return viewer; + } + + public @Nullable ItemSnapshot item() { + return item; + } + + public @Nullable ClickSnapshot click() { + return click; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerContextResolver.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerContextResolver.java new file mode 100644 index 00000000..55c1421c --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerContextResolver.java @@ -0,0 +1,27 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ViewerContextResolver implements ContextResolver { + + @Override + public @Nullable String resolve(final @NotNull String key, final @NotNull PlaceholderContext context) { + final ViewerSnapshot viewer = context.viewer(); + + if (viewer == null) { + return null; + } + + switch (key) { + case "name": + return viewer.name(); + case "uuid": + return viewer.uuid(); + case "display_name": + return viewer.displayName(); + default: + return null; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerSnapshot.java b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerSnapshot.java new file mode 100644 index 00000000..b64052dc --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/placeholder/internal/ViewerSnapshot.java @@ -0,0 +1,36 @@ +package com.extendedclip.deluxemenus.placeholder.internal; + +import org.jetbrains.annotations.NotNull; + +/** + * Immutable snapshot of the player actually viewing the menu. + *

+ * This is not the same player PlaceholderAPI parses against: when a menu is opened with + * {@code -p:}, PAPI parses against the target, so {@code %player_name%} returns the target + * while these values keep pointing at the viewer. + */ +public final class ViewerSnapshot { + + private final String name; + private final String uuid; + private final String displayName; + + public ViewerSnapshot(final @NotNull String name, final @NotNull String uuid, + final @NotNull String displayName) { + this.name = name; + this.uuid = uuid; + this.displayName = displayName; + } + + public @NotNull String name() { + return name; + } + + public @NotNull String uuid() { + return uuid; + } + + public @NotNull String displayName() { + return displayName; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasExpRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasExpRequirement.java index 275a0a10..4ce2d444 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasExpRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasExpRequirement.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.ExpUtils; import org.jetbrains.annotations.NotNull; @@ -20,14 +21,14 @@ public HasExpRequirement(@NotNull final DeluxeMenus plugin, String amt, boolean } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { int amount; int has = level ? holder.getViewer().getLevel() : ExpUtils.getTotalExperience(holder.getViewer()); try { - amount = Integer.parseInt(holder.setPlaceholdersAndArguments(amt)); + amount = Integer.parseInt(holder.setPlaceholdersAndArguments(amt, context)); } catch (final Exception exception) { plugin.printStacktrace( - "Invalid amount found for has exp requirement: " + holder.setPlaceholdersAndArguments(amt), + "Invalid amount found for has exp requirement: " + holder.setPlaceholdersAndArguments(amt, context), exception ); return false; diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java index 9c1a1442..3bf1fb7f 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasItemRequirement.java @@ -3,6 +3,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.hooks.ItemHook; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.requirement.wrappers.ItemWrapper; import com.extendedclip.deluxemenus.utils.StringUtils; import com.extendedclip.deluxemenus.utils.VersionHelper; @@ -31,8 +32,8 @@ public HasItemRequirement(final DeluxeMenus plugin, final ItemWrapper wrapper, f } @Override - public boolean evaluate(MenuHolder holder) { - String materialName = holder.setPlaceholdersAndArguments(wrapper.getMaterial()); + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { + String materialName = holder.setPlaceholdersAndArguments(wrapper.getMaterial(), context); Material material = DeluxeMenus.MATERIALS.get(materialName.toUpperCase()); ItemHook pluginHook = null; if (material == null) { @@ -52,20 +53,20 @@ public boolean evaluate(MenuHolder holder) { int total = 0; for (ItemStack itemToCheck : inventory) { - if (!isRequiredItem(itemToCheck, holder, material, pluginHook)) continue; + if (!isRequiredItem(itemToCheck, holder, context, material, pluginHook)) continue; total += itemToCheck.getAmount(); } if (offHand != null) { for (ItemStack itemToCheck : offHand) { - if (!isRequiredItem(itemToCheck, holder, material, pluginHook)) continue; + if (!isRequiredItem(itemToCheck, holder, context, material, pluginHook)) continue; total += itemToCheck.getAmount(); } } if (armor != null) { for (ItemStack itemToCheck : armor) { - if (!isRequiredItem(itemToCheck, holder, material, pluginHook)) continue; + if (!isRequiredItem(itemToCheck, holder, context, material, pluginHook)) continue; total += itemToCheck.getAmount(); } } @@ -73,11 +74,11 @@ public boolean evaluate(MenuHolder holder) { return invert == (total < wrapper.getAmount()); } - private boolean isRequiredItem(ItemStack itemToCheck, MenuHolder holder, Material material, ItemHook pluginHook) { + private boolean isRequiredItem(ItemStack itemToCheck, MenuHolder holder, PlaceholderContext context, Material material, ItemHook pluginHook) { if (itemToCheck == null || itemToCheck.getType() == Material.AIR) return false; if (pluginHook != null) { - if (!pluginHook.itemMatchesIdentifiers(itemToCheck, holder.setPlaceholdersAndArguments(wrapper.getMaterial().substring(pluginHook.getPrefix().length())))) + if (!pluginHook.itemMatchesIdentifiers(itemToCheck, holder.setPlaceholdersAndArguments(wrapper.getMaterial().substring(pluginHook.getPrefix().length()), context))) return false; } else if (wrapper.getMaterial() != null && itemToCheck.getType() != material) return false; if (wrapper.hasData() && itemToCheck.getDurability() != wrapper.getData()) return false; @@ -114,15 +115,15 @@ private boolean isRequiredItem(ItemStack itemToCheck, MenuHolder holder, Materia } } - if (VersionHelper.IS_CUSTOM_MODEL_DATA_COMPONENT && !isEmptyModelData(wrapper.getCustomModelDataComponent()) && !itemModelComponentContains(holder, metaToCheck.getCustomModelDataComponent(), wrapper.getCustomModelDataComponent())) { + if (VersionHelper.IS_CUSTOM_MODEL_DATA_COMPONENT && !isEmptyModelData(wrapper.getCustomModelDataComponent()) && !itemModelComponentContains(holder, context, metaToCheck.getCustomModelDataComponent(), wrapper.getCustomModelDataComponent())) { return false; } if (wrapper.getName() != null) { if (!metaToCheck.hasDisplayName()) return false; - String name = StringUtils.color(holder.setPlaceholdersAndArguments(wrapper.getName())); - String nameToCheck = StringUtils.color(holder.setPlaceholdersAndArguments(metaToCheck.getDisplayName())); + String name = StringUtils.color(holder.setPlaceholdersAndArguments(wrapper.getName(), context)); + String nameToCheck = StringUtils.color(holder.setPlaceholdersAndArguments(metaToCheck.getDisplayName(), context)); if (wrapper.checkNameContains() && wrapper.checkNameIgnoreCase()) { if (!org.apache.commons.lang3.StringUtils.containsIgnoreCase(nameToCheck, name)) return false; @@ -139,8 +140,8 @@ private boolean isRequiredItem(ItemStack itemToCheck, MenuHolder holder, Materia List loreX = metaToCheck.getLore(); if (loreX == null) return false; - String lore = wrapper.getLoreList().stream().map(holder::setPlaceholdersAndArguments).map(StringUtils::color).collect(Collectors.joining("&&")); - String loreToCheck = loreX.stream().map(holder::setPlaceholdersAndArguments).map(StringUtils::color).collect(Collectors.joining("&&")); + String lore = wrapper.getLoreList().stream().map(line -> holder.setPlaceholdersAndArguments(line, context)).map(StringUtils::color).collect(Collectors.joining("&&")); + String loreToCheck = loreX.stream().map(line -> holder.setPlaceholdersAndArguments(line, context)).map(StringUtils::color).collect(Collectors.joining("&&")); if (wrapper.checkLoreContains() && wrapper.checkLoreIgnoreCase()) { if (!org.apache.commons.lang3.StringUtils.containsIgnoreCase(loreToCheck, lore)) return false; @@ -157,8 +158,8 @@ private boolean isRequiredItem(ItemStack itemToCheck, MenuHolder holder, Materia List loreX = metaToCheck.getLore(); if (loreX == null) return false; - String lore = StringUtils.color(holder.setPlaceholdersAndArguments(wrapper.getLore())); - String loreToCheck = loreX.stream().map(holder::setPlaceholdersAndArguments).map(StringUtils::color).collect(Collectors.joining("&&")); + String lore = StringUtils.color(holder.setPlaceholdersAndArguments(wrapper.getLore(), context)); + String loreToCheck = loreX.stream().map(line -> holder.setPlaceholdersAndArguments(line, context)).map(StringUtils::color).collect(Collectors.joining("&&")); if (wrapper.checkLoreContains() && wrapper.checkLoreIgnoreCase()) { return org.apache.commons.lang3.StringUtils.containsIgnoreCase(loreToCheck, lore); @@ -180,11 +181,11 @@ private boolean isEmptyModelData(@NotNull final com.extendedclip.deluxemenus.men return modelData.colors().isEmpty() && modelData.flags().isEmpty() && modelData.floats().isEmpty() && modelData.strings().isEmpty(); } - private boolean itemModelComponentContains(MenuHolder holder, @NotNull final CustomModelDataComponent modelData, @NotNull final com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent wrapper) { + private boolean itemModelComponentContains(MenuHolder holder, PlaceholderContext context, @NotNull final CustomModelDataComponent modelData, @NotNull final com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent wrapper) { if (!wrapper.colors().isEmpty()) { final List colors = wrapper.colors() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(StringUtils::parseRGBColor) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -199,7 +200,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus if (!wrapper.flags().isEmpty()) { final List flags = wrapper.flags() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(Boolean::parseBoolean) .collect(Collectors.toList()); @@ -213,7 +214,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus if (!wrapper.floats().isEmpty()) { final List floats = wrapper.floats() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .map(Float::parseFloat) .collect(Collectors.toList()); @@ -227,7 +228,7 @@ private boolean itemModelComponentContains(MenuHolder holder, @NotNull final Cus if (!wrapper.strings().isEmpty()) { final List strings = wrapper.strings() .stream() - .map(holder::setPlaceholdersAndArguments) + .map(value -> holder.setPlaceholdersAndArguments(value, context)) .collect(Collectors.toList()); for (String string : strings) { diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasMetaRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasMetaRequirement.java index 383d6171..cb299de1 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasMetaRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasMetaRequirement.java @@ -3,6 +3,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.DataType; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -24,13 +25,13 @@ public HasMetaRequirement(@NotNull final DeluxeMenus plugin, String key, String } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { final Player player = holder.getViewer(); if (player == null) { return false; } - final String parsedKey = holder.setPlaceholdersAndArguments(key); + final String parsedKey = holder.setPlaceholdersAndArguments(key, context); final NamespacedKey namespacedKey = plugin.getPersistentMetaHandler().getKey(parsedKey); if (namespacedKey == null) { return invert; @@ -46,10 +47,10 @@ public boolean evaluate(MenuHolder holder) { return invert; } - final String expectedValue = holder.setPlaceholdersAndArguments(value); + final String expectedValue = holder.setPlaceholdersAndArguments(value, context); // TODO: Is there any reason to parse placeholders in the stored value when reading them? // Placeholders are parsed before value are stored. This means there will (or should) be no placeholders when reading. - final String actualValue = holder.setPlaceholdersAndArguments(String.valueOf(metaValue)); + final String actualValue = holder.setPlaceholdersAndArguments(String.valueOf(metaValue), context); if (type.equals(DataType.STRING) || type.equals(DataType.BOOLEAN)) { return invert != actualValue.equalsIgnoreCase(expectedValue); diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasMoneyRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasMoneyRequirement.java index 81114b64..272b5a21 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasMoneyRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasMoneyRequirement.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import org.jetbrains.annotations.NotNull; public class HasMoneyRequirement extends Requirement { @@ -19,18 +20,18 @@ public HasMoneyRequirement(@NotNull final DeluxeMenus plugin, double amount, boo } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { if (plugin.getVault() == null) { return false; } if (placeholder != null) { try { - String expected = holder.setPlaceholdersAndArguments(placeholder); + String expected = holder.setPlaceholdersAndArguments(placeholder, context); amount = Double.parseDouble(expected); } catch (final NumberFormatException exception) { plugin.printStacktrace( - "Invalid amount found for has money requirement: " + holder.setPlaceholdersAndArguments(placeholder), + "Invalid amount found for has money requirement: " + holder.setPlaceholdersAndArguments(placeholder, context), exception ); } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionRequirement.java index b2c92b1c..d32123c3 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; public class HasPermissionRequirement extends Requirement { @@ -13,8 +14,8 @@ public HasPermissionRequirement(String permission, boolean invert) { } @Override - public boolean evaluate(MenuHolder holder) { - String check = holder.setPlaceholdersAndArguments(perm); + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { + String check = holder.setPlaceholdersAndArguments(perm, context); if (invert) { return !holder.getViewer().hasPermission(check); } else { diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionsRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionsRequirement.java index 05fa0742..516bf137 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionsRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/HasPermissionsRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import java.util.List; @@ -17,9 +18,9 @@ public HasPermissionsRequirement(List permissions, int minimum, boolean } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { final int count = permissions.stream() - .map(holder::setPlaceholdersAndArguments) + .map(permission -> holder.setPlaceholdersAndArguments(permission, context)) .map(holder.getViewer()::hasPermission) .mapToInt(hasPermission -> hasPermission ? 1 : 0) .sum(); diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java index 93932422..e9bb7b47 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/InputResultRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; public class InputResultRequirement extends Requirement { @@ -15,10 +16,10 @@ public InputResultRequirement(RequirementType type, String input, String result) } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { - String parsedInput = holder.setPlaceholdersAndArguments(this.input); - String parsedResult = holder.setPlaceholdersAndArguments(this.result); + String parsedInput = holder.setPlaceholdersAndArguments(this.input, context); + String parsedResult = holder.setPlaceholdersAndArguments(this.result, context); switch (type) { case STRING_CONTAINS: diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java index d207aba7..f12fa8bc 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/IsNearRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import org.bukkit.Location; public class IsNearRequirement extends Requirement { @@ -16,7 +17,7 @@ public IsNearRequirement(Location location, int distance, boolean invert) { } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { if (holder.getViewer() == null) { return false; } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java index 467908fa..225a5d06 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/IsObjectRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.google.common.primitives.Doubles; import com.google.common.primitives.Ints; @@ -20,8 +21,8 @@ public IsObjectRequirement(String input, String object) { } @Override - public boolean evaluate(MenuHolder holder) { - String toCheck = holder.setPlaceholdersAndArguments(input); + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { + String toCheck = holder.setPlaceholdersAndArguments(input, context); switch (object) { case "int": diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java index 56a67760..80b46400 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import com.extendedclip.deluxemenus.utils.DebugLevel; import java.util.logging.Level; import javax.script.ScriptEngineFactory; @@ -39,9 +40,9 @@ public JavascriptRequirement(final @NotNull DeluxeMenus plugin, String expressio } @Override - public boolean evaluate(MenuHolder holder) { + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { - String exp = holder.setPlaceholdersAndArguments(expression); + String exp = holder.setPlaceholdersAndArguments(expression, context); try { engine.put("BukkitPlayer", holder.getViewer()); diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/RegexMatchesRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/RegexMatchesRequirement.java index 9d66b420..1cac250e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/RegexMatchesRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/RegexMatchesRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import java.util.regex.Pattern; public class RegexMatchesRequirement extends Requirement { @@ -16,12 +17,12 @@ public RegexMatchesRequirement(Pattern pattern, String input, boolean invert) { } @Override - public boolean evaluate(MenuHolder holder) { - String toCheck = holder.setPlaceholdersAndArguments(input); + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { + String toCheck = holder.setPlaceholdersAndArguments(input, context); if (invert) { - return !pattern.matcher(holder.setPlaceholdersAndArguments(toCheck)).find(); + return !pattern.matcher(holder.setPlaceholdersAndArguments(toCheck, context)).find(); } else { - return pattern.matcher(holder.setPlaceholdersAndArguments(toCheck)).find(); + return pattern.matcher(holder.setPlaceholdersAndArguments(toCheck, context)).find(); } } } diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/Requirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/Requirement.java index 0f065ff4..920207e0 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/Requirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/Requirement.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.action.ClickHandler; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; public abstract class Requirement { @@ -17,7 +18,7 @@ public Requirement(boolean optional) { this.setOptional(optional); } - public abstract boolean evaluate(MenuHolder holder); + public abstract boolean evaluate(MenuHolder holder, PlaceholderContext context); public ClickHandler getDenyHandler() { return denyHandler; diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementList.java b/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementList.java index 9197ad32..6bf14e4c 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementList.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/RequirementList.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.action.ClickHandler; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import java.util.List; public class RequirementList { @@ -16,19 +17,23 @@ public RequirementList(List requirements) { } public boolean evaluate(MenuHolder holder) { + return evaluate(holder, PlaceholderContext.of(holder)); + } + + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { int successful = 0; for (Requirement r : getRequirements()) { - if (r.evaluate(holder)) { + if (r.evaluate(holder, context)) { successful = successful + 1; if (r.getSuccessHandler() != null) { - r.getSuccessHandler().onClick(holder); + r.getSuccessHandler().onClick(holder, context); } if (this.stopAtSuccess && successful >= minimumRequirements) { break; } } else { if (r.getDenyHandler() != null) { - r.getDenyHandler().onClick(holder); + r.getDenyHandler().onClick(holder, context); } if (!r.isOptional()) { return false; diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/StringLengthRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/StringLengthRequirement.java index 51cd599e..a997ba6d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/StringLengthRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/StringLengthRequirement.java @@ -1,6 +1,7 @@ package com.extendedclip.deluxemenus.requirement; import com.extendedclip.deluxemenus.menu.MenuHolder; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; public class StringLengthRequirement extends Requirement { @@ -15,8 +16,8 @@ public StringLengthRequirement(String input, int min, Integer max) { } @Override - public boolean evaluate(MenuHolder holder) { - String toCheck = holder.setPlaceholdersAndArguments(input); + public boolean evaluate(MenuHolder holder, PlaceholderContext context) { + String toCheck = holder.setPlaceholdersAndArguments(input, context); if (max == null) { return toCheck.length() >= min; } else { diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index 6ae95804..6a39922a 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -4,6 +4,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.extendedclip.deluxemenus.placeholder.internal.InternalPlaceholderResolver; +import com.extendedclip.deluxemenus.placeholder.internal.PlaceholderContext; import me.clip.placeholderapi.PlaceholderAPI; import net.md_5.bungee.api.ChatColor; import org.bukkit.Color; @@ -42,15 +44,32 @@ public static String replacePlaceholdersAndArguments(@NotNull String input, fina final @Nullable Player player, final boolean parsePlaceholdersInsideArguments, final boolean parsePlaceholdersAfterArguments) { - if (player == null) { - return replaceArguments(input, arguments, null, parsePlaceholdersInsideArguments); - } + return replacePlaceholdersAndArguments(input, arguments, player, parsePlaceholdersInsideArguments, + parsePlaceholdersAfterArguments, PlaceholderContext.EMPTY); + } - if (parsePlaceholdersAfterArguments) { - return replacePlaceholders(replaceArguments(input, arguments, player, parsePlaceholdersInsideArguments), player); + /** + * The one place the parsing order lives. Internal placeholders are always resolved last, so a + * menu can build one out of an argument or a PlaceholderAPI result. The resolver never rescans + * its own output, so the substitution cannot cascade further than that. + */ + @NotNull + public static String replacePlaceholdersAndArguments(@NotNull String input, final @Nullable Map arguments, + final @Nullable Player player, + final boolean parsePlaceholdersInsideArguments, + final boolean parsePlaceholdersAfterArguments, + final @NotNull PlaceholderContext context) { + final String parsed; + + if (player == null) { + parsed = replaceArguments(input, arguments, null, parsePlaceholdersInsideArguments); + } else if (parsePlaceholdersAfterArguments) { + parsed = replacePlaceholders(replaceArguments(input, arguments, player, parsePlaceholdersInsideArguments), player); + } else { + parsed = replaceArguments(replacePlaceholders(input, player), arguments, player, parsePlaceholdersInsideArguments); } - return replaceArguments(replacePlaceholders(input, player), arguments, player, parsePlaceholdersInsideArguments); + return InternalPlaceholderResolver.resolve(parsed, context); } @NotNull