diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/BookWrapper.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/BookWrapper.java index ac34c942a7..382e152c53 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/BookWrapper.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/BookWrapper.java @@ -137,7 +137,8 @@ public BookWrapper addToCurrentPage(BaseComponent... baseComponents) { * @return the colorized string */ public String colorize(String s) { - return MessageAPI.colorize(s); + return com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting( + MessageAPI.colorize(s)); } /** diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/Layout.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/Layout.java index fe56a4b6ae..31055bd17d 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/Layout.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bookgui/Layout.java @@ -50,7 +50,8 @@ public Layout addPlaceholder(String toReplace, String replaceWith) { } private String colorize(String s) { - return ChatColor.translateAlternateColorCodes('&', s); + return com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting( + ChatColor.translateAlternateColorCodes('&', s)); } /** diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/inventory/BInventory.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/inventory/BInventory.java index 38c21349ac..4906d4b47e 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/inventory/BInventory.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/inventory/BInventory.java @@ -497,8 +497,9 @@ private void cancelLegacyTimers() { } private String getDisplayName(Player player) { - return PlaceholderUtils.replaceJavascript(player, - PlaceholderUtils.replacePlaceHolder(inventoryName, placeholders)); + return com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting( + PlaceholderUtils.replaceJavascript(player, + PlaceholderUtils.replacePlaceHolder(inventoryName, placeholders))); } private int getProperSize(int size) { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java index 70264008e8..713eb86e2a 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/ItemBuilder.java @@ -42,6 +42,7 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.javascript.JavascriptEngine; +import com.bencodez.advancedcore.api.messages.DisplayTextBoundaries; import com.bencodez.advancedcore.api.messages.PlaceholderUtils; import com.bencodez.advancedcore.api.rewards.RewardDisplayPlaceholders; import com.bencodez.advancedcore.api.misc.MiscUtils; @@ -1736,7 +1737,7 @@ public ItemStack parsePlaceholders(OfflinePlayer player) { setSkullOwner(PlaceholderUtils.replaceJavascript(player, PlaceholderUtils.replacePlaceHolder(skull, placeholders))); } - return is; + return finishedDisplayItem(); } /** @@ -2141,6 +2142,38 @@ public ItemBuilder setUnbreakable(boolean unbreakable) { return this; } + /** + * Creates a final client-facing copy only when protected identifiers are + * present. The builder retains its guarded template for later GUI refreshes, + * which must never re-parse a previously unguarded display name. + */ + private ItemStack finishedDisplayItem() { + ItemMeta sourceMeta = is.getItemMeta(); + if (sourceMeta == null) return is; + String name = sourceMeta.hasDisplayName() ? sourceMeta.getDisplayName() : null; + List lore = sourceMeta.hasLore() ? sourceMeta.getLore() : null; + boolean guarded = name != null && name.indexOf('\u2060') >= 0; + if (lore != null) { + for (String line : lore) { + if (line != null && line.indexOf('\u2060') >= 0) { + guarded = true; + break; + } + } + } + if (!guarded) return is; + ItemStack displayed = is.clone(); + ItemMeta displayMeta = displayed.getItemMeta(); + if (name != null) displayMeta.setDisplayName(DisplayTextBoundaries.afterFormatting(name)); + if (lore != null) { + ArrayList visibleLore = new ArrayList<>(lore.size()); + for (String line : lore) visibleLore.add(DisplayTextBoundaries.afterFormatting(line)); + displayMeta.setLore(visibleLore); + } + displayed.setItemMeta(displayMeta); + return displayed; + } + /** * Retrieves the itemstack from the ItemBuilder. * @@ -2167,7 +2200,7 @@ public ItemStack toItemStack() { if (checkLoreLength) { checkLoreLength(); } - return is; + return finishedDisplayItem(); } /** @@ -2191,7 +2224,7 @@ public ItemStack toItemStack(OfflinePlayer player) { if (checkLoreLength) { checkLoreLength(); } - return is; + return finishedDisplayItem(); } /** @@ -2215,7 +2248,7 @@ public ItemStack toItemStack(Player player) { if (checkLoreLength) { checkLoreLength(); } - return is; + return finishedDisplayItem(); } @Override diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundaries.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundaries.java new file mode 100644 index 0000000000..f8ab50567f --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundaries.java @@ -0,0 +1,56 @@ +package com.bencodez.advancedcore.api.messages; + +import java.util.List; + +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; + +/** + * Drops temporary WORD JOINER formatting guards only at a final player-facing + * sink. The guards must survive placeholder, JavaScript, color, and interactive + * message parsing; removing them earlier would make external identifiers active + * formatting syntax again. + */ +public final class DisplayTextBoundaries { + + private DisplayTextBoundaries() { + } + + public static String afterFormatting(String prepared) { + if (prepared == null || prepared.indexOf('\u2060') < 0) return prepared; + StringBuilder visible = new StringBuilder(prepared.length()); + for (int i = 0; i < prepared.length(); i++) { + char next = prepared.charAt(i); + if (next != '\u2060') { + visible.append(next); + continue; + } + // Some legacy String sinks (inventory titles, item metadata, boss + // bars) still parse § codes on the client. Prevent an incomplete + // § in a trusted template from joining the next site-name letter + // into an active legacy color code when dropping this guard. + if (i > 0 && prepared.charAt(i - 1) == '\u00A7' + && i + 1 < prepared.length() && isLegacyColorCode(prepared.charAt(i + 1))) { + visible.append(' '); + } + } + return visible.toString(); + } + + private static boolean isLegacyColorCode(char code) { + return "0123456789abcdefklmnorx".indexOf(Character.toLowerCase(code)) >= 0; + } + + public static void afterFormatting(BaseComponent component) { + if (component == null) return; + if (component instanceof TextComponent text) { + // A component already passed legacy parsing, so it can retain the + // exact literal text without inserting a String-sink separator. + text.setText(text.getText() == null ? null : text.getText().replace("\u2060", "")); + } + List children = component.getExtra(); + if (children != null) { + for (BaseComponent child : children) afterFormatting(child); + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java index 8e6b270001..60b06e6656 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/messages/PlaceholderUtils.java @@ -62,6 +62,7 @@ public static TextComponent parseJson(String msg) { if (type.equalsIgnoreCase("hover")) { BaseComponent[] hoverContent = TextComponent.fromLegacyText(typeData); + for (BaseComponent hover : hoverContent) DisplayTextBoundaries.afterFormatting(hover); t.setHoverEvent(MessageAPI.getHoverEventSupport().createHoverEvent(hoverContent)); } else if (type.equalsIgnoreCase("command")) { t.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, typeData)); @@ -87,6 +88,9 @@ public static TextComponent parseJson(String msg) { comp.addExtra(new TextComponent(TextComponent.fromLegacyText(msg))); } + // Syntax and legacy-color parsing are finished; restore the literal site + // name for the client without allowing another formatting pass. + DisplayTextBoundaries.afterFormatting(comp); return comp; } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java index ea43811bd5..36a6cbf59c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/BossBar.java @@ -20,7 +20,7 @@ public class BossBar { private org.bukkit.boss.BossBar bossBar; public BossBar(String msg, String barColor, String barStyle, double progress) { - bossBar = Bukkit.createBossBar(MessageAPI.colorize(msg), EffectCompatibility.parseBarColor(barColor), + bossBar = Bukkit.createBossBar(com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(msg)), EffectCompatibility.parseBarColor(barColor), EffectCompatibility.parseBarStyle(barStyle)); bossBar.setProgress(EffectCompatibility.clampProgress(progress)); } @@ -116,7 +116,7 @@ public void setStyle(String barStyle) { public void setTitle(String title) { if (title != null) { - bossBar.setTitle(MessageAPI.colorize(title)); + bossBar.setTitle(com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(title))); } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/Title.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/Title.java index e265052808..be35d7197b 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/Title.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/effects/Title.java @@ -168,7 +168,9 @@ public void send(Player player) { } public void send(Player player, String title, String subtitle, int fadeInTime, int stayTime, int fadeOutTime) { - player.sendTitle(MessageAPI.colorize(title), MessageAPI.colorize(subtitle), fadeInTime, stayTime, fadeOutTime); + player.sendTitle(com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(title)), + com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(subtitle)), + fadeInTime, stayTime, fadeOutTime); } /** @@ -226,7 +228,7 @@ public void updateSubtitle(Player player) { } public void updateSubtitle(Player player, String subtitle) { - player.sendTitle("", MessageAPI.colorize(subtitle), -1, -1, -1); + player.sendTitle("", com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(subtitle)), -1, -1, -1); } /** @@ -252,6 +254,6 @@ public void updateTitle(Player player) { } public void updateTitle(Player player, String title) { - player.sendTitle(MessageAPI.colorize(title), "", -1, -1, -1); + player.sendTitle(com.bencodez.advancedcore.api.messages.DisplayTextBoundaries.afterFormatting(MessageAPI.colorize(title)), "", -1, -1, -1); } } \ No newline at end of file diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundariesTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundariesTest.java new file mode 100644 index 0000000000..4697ec2fa9 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/messages/DisplayTextBoundariesTest.java @@ -0,0 +1,50 @@ +package com.bencodez.advancedcore.api.messages; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.TextComponent; + +class DisplayTextBoundariesTest { + + @Test + void chatRemovesTemporaryMarkersOnlyAfterLegacyParsing() { + TextComponent text = PlaceholderUtils.parseJson("\u00A7eSite: \u2060Minecraft Menu\u2060"); + + assertEquals("Site: Minecraft Menu", text.toPlainText()); + assertFalse(text.toPlainText().contains("\u2060")); + org.junit.jupiter.api.Assertions.assertTrue(text.toLegacyText().contains("\u00A7eSite: Minecraft Menu")); + assertFalse(text.toLegacyText().contains("\u2060")); + } + + @Test + void guardedColorSequenceRemainsLiteralAfterLastParsingPass() { + TextComponent text = PlaceholderUtils.parseJson("&\u2060a"); + + assertEquals("&a", text.toPlainText()); + assertFalse(text.toPlainText().contains("\u2060")); + } + + @Test + void unfinishedSectionColorCodesCannotBecomeActiveAtLegacyStringSinks() { + assertEquals("\u00A7 a", DisplayTextBoundaries.afterFormatting("\u00A7\u2060a\u2060")); + assertEquals("Minecraft Menu", DisplayTextBoundaries.afterFormatting("\u2060Minecraft Menu\u2060")); + assertEquals("&aGreen", DisplayTextBoundaries.afterFormatting("\u2060&\u2060aGreen\u2060")); + } + + @Test + void interactiveActionsStayExactWhileVisibleTextIsClean() { + TextComponent text = PlaceholderUtils.parseJson( + "[Text=\"\u2060Site name\u2060\",Url=\"https://example.com/vote\"]"); + + assertEquals("Site name", text.toPlainText()); + TextComponent link = (TextComponent) text.getExtra().get(1); + ClickEvent click = link.getClickEvent(); + assertNotNull(click); + assertEquals("https://example.com/vote", click.getValue()); + } +}