From 2dc5b93133252f8710455dada6d1eceacfb555eb Mon Sep 17 00:00:00 2001 From: PiggyPiglet Date: Wed, 21 Jan 2026 22:14:46 +0800 Subject: [PATCH] WIP custom component replacer no regex!! --- .../configuration/PlaceholderAPIConfig.java | 5 +- src/main/resources/config.yml | 3 + .../clip/placeholderapi/PAPIComponents.java | 6 +- .../replacer/ComponentReplacer.java | 177 ++++++++++++++++++ 4 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 src/paper/java/me/clip/placeholderapi/replacer/ComponentReplacer.java diff --git a/src/main/java/me/clip/placeholderapi/configuration/PlaceholderAPIConfig.java b/src/main/java/me/clip/placeholderapi/configuration/PlaceholderAPIConfig.java index 57fc9b6..54e666f 100644 --- a/src/main/java/me/clip/placeholderapi/configuration/PlaceholderAPIConfig.java +++ b/src/main/java/me/clip/placeholderapi/configuration/PlaceholderAPIConfig.java @@ -87,8 +87,11 @@ public final class PlaceholderAPIConfig { return plugin.getConfig().getString("boolean.false", "false"); } + public boolean useAdventureProvidedReplacer() { + return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false); + } + public boolean detectMaliciousExpansions() { return plugin.getConfig().getBoolean("detect_malicious_expansions", true); } - } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0fe9cda..03d25ee 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -6,6 +6,8 @@ # Expansions: https://placeholderapi.com/ecloud # Wiki: https://wiki.placeholderapi.com/ # Discord: https://helpch.at/discord +# If you're seeing performance issues with plugins that use component replacement, or things don't look quite right, +# switch to the replacer provided by adventure itself by changing use_adventure_provided_replacer to true # No placeholders are provided with this plugin by default. # Download placeholders: /papi ecloud check_updates: true @@ -16,4 +18,5 @@ boolean: 'false': 'no' date_format: MM/dd/yy HH:mm:ss detect_malicious_expansions: true +use_adventure_provided_replacer: false debug: false diff --git a/src/paper/java/me/clip/placeholderapi/PAPIComponents.java b/src/paper/java/me/clip/placeholderapi/PAPIComponents.java index d35a78d..86e39c9 100644 --- a/src/paper/java/me/clip/placeholderapi/PAPIComponents.java +++ b/src/paper/java/me/clip/placeholderapi/PAPIComponents.java @@ -20,6 +20,7 @@ package me.clip.placeholderapi; +import me.clip.placeholderapi.replacer.ComponentReplacer; import me.clip.placeholderapi.replacer.ExactReplacer; import me.clip.placeholderapi.replacer.RelationalExactReplacer; import me.clip.placeholderapi.replacer.Replacer; @@ -49,8 +50,9 @@ public final class PAPIComponents { @NotNull public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) { // TODO: explore a custom TextReplacementRenderer which doesn't use regex for performance benefits i.e. merge CharsReplacer with kyori TextReplacementRenderer - return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) -> - builder.content(PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion)))); + return ComponentReplacer.replace(player, component); +// return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) -> +// builder.content(PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion)))); } /** diff --git a/src/paper/java/me/clip/placeholderapi/replacer/ComponentReplacer.java b/src/paper/java/me/clip/placeholderapi/replacer/ComponentReplacer.java new file mode 100644 index 0000000..bee4eb0 --- /dev/null +++ b/src/paper/java/me/clip/placeholderapi/replacer/ComponentReplacer.java @@ -0,0 +1,177 @@ +package me.clip.placeholderapi.replacer; + +import me.clip.placeholderapi.PlaceholderAPI; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.adventure.text.*; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.DataComponentValue; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.Style; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ComponentReplacer { + @NotNull + public static Component replace(@Nullable final OfflinePlayer player, @NotNull final Component component) { + return rebuild(player, component); + } + + @NotNull + private static Component rebuild(@Nullable final OfflinePlayer player, @NotNull final Component component) { + Component rebuilt; + + if (component instanceof TextComponent) { + final TextComponent text = (TextComponent) component; + final String replaced = PlaceholderAPI.setPlaceholders(player, text.content()); + + rebuilt = Component.text(replaced); + } else if (component instanceof TranslatableComponent) { + final TranslatableComponent translatable = (TranslatableComponent) component; + final List arguments = new ArrayList<>(); + + for (final ComponentLike arg : translatable.arguments()) { + arguments.add(rebuild(player, arg.asComponent())); + } + + rebuilt = Component.translatable(translatable.key(), arguments); + } else if (component instanceof KeybindComponent) { + final KeybindComponent keybind = (KeybindComponent) component; + rebuilt = Component.keybind(keybind.keybind()); + } else if (component instanceof ScoreComponent) { + final ScoreComponent score = (ScoreComponent) component; + rebuilt = Component.score(score.name(), score.objective()); + } else if (component instanceof SelectorComponent) { + final SelectorComponent selector = (SelectorComponent) component; + rebuilt = Component.selector(selector.pattern()); + } else { + rebuilt = Component.empty(); + } + + rebuilt = rebuilt.style(rebuildStyle(player, component.style())); + + if (!component.children().isEmpty()) { + final List children = new ArrayList<>(); + for (Component child : component.children()) { + children.add(rebuild(player, child)); + } + rebuilt = rebuilt.children(children); + } + + return rebuilt; + } + + @NotNull + private static Style rebuildStyle(@Nullable final OfflinePlayer player, @NotNull final Style style) { + final Style.Builder builder = style.toBuilder(); + final ClickEvent click = style.clickEvent(); + + if (click != null) { + builder.clickEvent(rebuildClickEvent(player, click)); + } + + final HoverEvent hover = style.hoverEvent(); + + if (hover != null) { + builder.hoverEvent(rebuildHoverEvent(player, hover)); + } + + return builder.build(); + } + + @NotNull + private static ClickEvent rebuildClickEvent(@Nullable final OfflinePlayer player, @NotNull final ClickEvent click) { + final ClickEvent.Payload payload = click.payload(); + + if (!(payload instanceof ClickEvent.Payload.Text)) { + return click; + } + + final String original = ((ClickEvent.Payload.Text) payload).value(); + final String replaced = PlaceholderAPI.setPlaceholders(player, original); + + final ClickEvent.Action action = click.action(); + + switch (action) { + case OPEN_URL: + return ClickEvent.openUrl(replaced); + case OPEN_FILE: + return ClickEvent.openFile(replaced); + case RUN_COMMAND: + return ClickEvent.runCommand(replaced); + case SUGGEST_COMMAND: + return ClickEvent.suggestCommand(replaced); + case COPY_TO_CLIPBOARD: + return ClickEvent.copyToClipboard(replaced); + default: + return click; + } + } + + @NotNull + private static HoverEvent rebuildHoverEvent(@Nullable final OfflinePlayer player, @NotNull final HoverEvent hover) { + final Object value = hover.value(); + + if (value instanceof Component) { + final Component rebuilt = rebuild(player, (Component) value); + return HoverEvent.showText(rebuilt); + } + + if (value instanceof HoverEvent.ShowItem) { + return rebuildShowItem(player, (HoverEvent.ShowItem) value); + } + + if (value instanceof HoverEvent.ShowEntity) { + final HoverEvent.ShowEntity entity = (HoverEvent.ShowEntity) value; + + Component rebuiltName = null; + if (entity.name() != null) { + rebuiltName = rebuild(player, entity.name()); + } + + return HoverEvent.showEntity(entity.type(), entity.id(), rebuiltName); + } + + return hover; + } + + @NotNull + private static HoverEvent rebuildShowItem(@Nullable final OfflinePlayer player, @NotNull final HoverEvent.ShowItem item) { + final BinaryTagHolder nbt = item.nbt(); + + if (nbt != null && !nbt.string().isEmpty()) { + final String replaced = PlaceholderAPI.setPlaceholders(player, nbt.string()); + + return HoverEvent.showItem(item.item(), item.count(), BinaryTagHolder.binaryTagHolder(replaced)); + } + + //I'm not 100% sure this is how we're meant to support data components but let's give it a go and see if it causes any issues :) + final Map components = item.dataComponents(); + + if (!components.isEmpty()) { + final Map rebuilt = new HashMap<>(); + + for (final Map.Entry entry : components.entrySet()) { + final DataComponentValue value = entry.getValue(); + + if (!(value instanceof BinaryTagHolder)) { + rebuilt.put(entry.getKey(), value); + continue; + } + + rebuilt.put(entry.getKey(), BinaryTagHolder.binaryTagHolder(PlaceholderAPI.setPlaceholders(player, ((BinaryTagHolder) value).string()))); + } + + return HoverEvent.showItem(item.item(), item.count(), rebuilt); + } + + return HoverEvent.showItem(item); + } +} \ No newline at end of file