diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/DialogTextFormatter.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/DialogTextFormatter.java index c0ac3e10..be5f95e3 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/DialogTextFormatter.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/DialogTextFormatter.java @@ -29,6 +29,9 @@ public static String format(Player player, String text, HashMap String formatted = text; + // UniDialog deserializes legacy color codes again. Preserve all inert + // boundaries until that last component conversion, then strip them from + // the already-parsed component instead of exposing active color syntax. return MessageAPI.colorize(MessageAPI.replacePlaceHolder(formatted, placeholders, true)); } } \ No newline at end of file diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/PaperUniDialogPlatform.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/PaperUniDialogPlatform.java index 7809d67b..5b5bb906 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/PaperUniDialogPlatform.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/PaperUniDialogPlatform.java @@ -12,17 +12,32 @@ import io.github.projectunified.unidialog.paper.dialog.PaperConfirmationDialog; import io.github.projectunified.unidialog.paper.dialog.PaperMultiActionDialog; import io.github.projectunified.unidialog.paper.dialog.PaperNoticeDialog; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; /** * Paper implementation using UniDialog's Paper manager. */ public class PaperUniDialogPlatform extends AbstractUniDialogPlatform { + private static final TextReplacementConfig HIDE_INERT_GUARDS = TextReplacementConfig.builder() + .matchLiteral("\u2060").replacement("").build(); private final PaperDialogManager manager; public PaperUniDialogPlatform(Plugin plugin, String namespace) { super(plugin, namespace); - this.manager = new PaperDialogManager(plugin, namespace); + this.manager = new PaperDialogManager(plugin, namespace, PaperUniDialogPlatform::renderedText); + } + + /** + * Called by UniDialog at its final component deserialization boundary. + * Deserialize while WORD JOINER guards are present, then remove those + * markers from the component tree, never from another parsable string. + */ + static Component renderedText(String prepared) { + return LegacyComponentSerializer.legacySection().deserialize(prepared) + .replaceText(HIDE_INERT_GUARDS); } @Override @@ -275,6 +290,6 @@ private String getInputLabel(UniDialogInput input) { } private String getInputInitial(UniDialogInput input) { - return input.getInitialValue(); + return input.getInitialValue() == null ? null : input.getInitialValue().replace("\u2060", ""); } } \ No newline at end of file diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/SpigotUniDialogPlatform.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/SpigotUniDialogPlatform.java index 4bafd009..36afc096 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/SpigotUniDialogPlatform.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/dialog/SpigotUniDialogPlatform.java @@ -12,6 +12,8 @@ import io.github.projectunified.unidialog.core.payload.DialogPayload; import io.github.projectunified.unidialog.spigot.SpigotDialogManager; import io.github.projectunified.unidialog.spigot.opener.SpigotDialogOpener; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; /** * Spigot implementation using UniDialog's Spigot manager. @@ -69,13 +71,13 @@ public void showNotice(Player player, UniDialogNoticeRequest request) { registerCustomAction(namespace, actionId, request.getCallback()); } - BungeeNoticeDialog dialog = manager.createNoticeDialog().title(request.getTitle()) - .body(builder -> builder.text().text(request.getBody())); + BungeeNoticeDialog dialog = manager.createNoticeDialog().title(renderedText(request.getTitle())) + .body(builder -> builder.text().text(renderedText(request.getBody()))); applyInputs(dialog, request.getInputs()); dialog.action(action -> { - action.label(request.getButtonText()); + action.label(renderedText(request.getButtonText())); action.dynamicCustom(namespace, actionId); }); @@ -97,17 +99,17 @@ public void showConfirmation(Player player, UniDialogConfirmationRequest request } BungeeConfirmationDialog dialog = manager.createConfirmationDialog() - .title(request.getTitle()).body(builder -> builder.text().text(request.getBody())); + .title(renderedText(request.getTitle())).body(builder -> builder.text().text(renderedText(request.getBody()))); applyInputs(dialog, request.getInputs()); dialog.yesAction(action -> { - action.label(request.getYesText()); + action.label(renderedText(request.getYesText())); action.dynamicCustom(namespace, yesActionId); }); dialog.noAction(action -> { - action.label(request.getNoText()); + action.label(renderedText(request.getNoText())); action.dynamicCustom(namespace, noActionId); }); @@ -118,8 +120,8 @@ public void showConfirmation(Player player, UniDialogConfirmationRequest request public void showMultiAction(Player player, UniDialogMultiActionRequest request) { String namespace = resolveNamespace(request.getNamespace()); - BungeeMultiActionDialog dialog = manager.createMultiActionDialog().title(request.getTitle()) - .body(builder -> builder.text().text(request.getBody())).columns(request.getColumns()); + BungeeMultiActionDialog dialog = manager.createMultiActionDialog().title(renderedText(request.getTitle())) + .body(builder -> builder.text().text(renderedText(request.getBody()))).columns(request.getColumns()); applyInputs(dialog, request.getInputs()); @@ -131,10 +133,10 @@ public void showMultiAction(Player player, UniDialogMultiActionRequest request) } dialog.action(action -> { - action.label(button.getText()); + action.label(renderedText(button.getText())); if (button.getTooltip() != null && !button.getTooltip().isEmpty()) { - action.tooltip(button.getTooltip()); + action.tooltip(renderedText(button.getTooltip())); } Integer width = button.getWidth(); @@ -190,13 +192,13 @@ private void applyInput(BungeeNoticeDialog dialog, UniDialog dialog.input(input.getId(), builder -> { if (input.getType() == UniDialogInput.InputType.BOOLEAN) { - builder.booleanInput().label(getInputLabel(input)).initial(input.isInitialBoolean()); + builder.booleanInput().label(renderedText(getInputLabel(input))).initial(input.isInitialBoolean()); } else { String initial = getInputInitial(input); if (initial != null && !initial.isEmpty()) { - builder.textInput().label(getInputLabel(input)).initial(initial); + builder.textInput().label(renderedText(getInputLabel(input))).initial(initial); } else { - builder.textInput().label(getInputLabel(input)); + builder.textInput().label(renderedText(getInputLabel(input))); } } }); @@ -209,13 +211,13 @@ private void applyInput(BungeeConfirmationDialog dialog, Uni dialog.input(input.getId(), builder -> { if (input.getType() == UniDialogInput.InputType.BOOLEAN) { - builder.booleanInput().label(getInputLabel(input)).initial(input.isInitialBoolean()); + builder.booleanInput().label(renderedText(getInputLabel(input))).initial(input.isInitialBoolean()); } else { String initial = getInputInitial(input); if (initial != null && !initial.isEmpty()) { - builder.textInput().label(getInputLabel(input)).initial(initial); + builder.textInput().label(renderedText(getInputLabel(input))).initial(initial); } else { - builder.textInput().label(getInputLabel(input)); + builder.textInput().label(renderedText(getInputLabel(input))); } } }); @@ -228,13 +230,13 @@ private void applyInput(BungeeMultiActionDialog dialog, UniD dialog.input(input.getId(), builder -> { if (input.getType() == UniDialogInput.InputType.BOOLEAN) { - builder.booleanInput().label(getInputLabel(input)).initial(input.isInitialBoolean()); + builder.booleanInput().label(renderedText(getInputLabel(input))).initial(input.isInitialBoolean()); } else { String initial = getInputInitial(input); if (initial != null && !initial.isEmpty()) { - builder.textInput().label(getInputLabel(input)).initial(initial); + builder.textInput().label(renderedText(getInputLabel(input))).initial(initial); } else { - builder.textInput().label(getInputLabel(input)); + builder.textInput().label(renderedText(getInputLabel(input))); } } }); @@ -250,7 +252,27 @@ private String getInputLabel(UniDialogInput input) { return label; } + /** + * UniDialog's Spigot methods deserialize Bungee legacy formatting again. + * Let that final conversion see the inert guards, then drop them from text + * components without reparsing color or click-action syntax. + */ + static BaseComponent renderedText(String prepared) { + BaseComponent component = TextComponent.fromLegacy(prepared); + stripGuards(component); + return component; + } + + private static void stripGuards(BaseComponent component) { + if (component instanceof TextComponent text) { + text.setText(text.getText().replace("\u2060", "")); + } + if (component.getExtra() != null) { + for (BaseComponent extra : component.getExtra()) stripGuards(extra); + } + } + private String getInputInitial(UniDialogInput input) { - return input.getInitialValue(); + return input.getInitialValue() == null ? null : input.getInitialValue().replace("\u2060", ""); } } diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/messages/actionbar/ActionBarManager.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/messages/actionbar/ActionBarManager.java index 83026bc5..dcf29d91 100644 --- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/messages/actionbar/ActionBarManager.java +++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/messages/actionbar/ActionBarManager.java @@ -297,6 +297,15 @@ protected boolean canReplace(ActionBarEntry current, ActionBarMessage nextMessag */ @SuppressWarnings("deprecation") protected void sendNow(Player player, String message) { - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); + // Parse legacy styling while the inert boundaries still separate + // untrusted text from formatting tokens. Remove them only from the + // finished components, after parsing can no longer be influenced. + net.md_5.bungee.api.chat.BaseComponent[] rendered = TextComponent.fromLegacyText(message); + for (net.md_5.bungee.api.chat.BaseComponent component : rendered) { + if (component instanceof TextComponent text) { + text.setText(text.getText().replace("\u2060", "")); + } + } + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, rendered); } } \ No newline at end of file diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/dialog/DialogTextFormatterTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/dialog/DialogTextFormatterTest.java new file mode 100644 index 00000000..194b09f5 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/dialog/DialogTextFormatterTest.java @@ -0,0 +1,66 @@ +package com.bencodez.simpleapi.dialog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +class DialogTextFormatterTest { + + @Test + void removesMarkersOnlyAfterSpigotAndPaperComponentParsing() { + HashMap values = new HashMap<>(); + values.put("site", "\u2060Minecraft Menu\u2060"); + + String formatted = DialogTextFormatter.format(null, "&7Site: &e%site%", values); + assertTrue(formatted.contains("\u2060"), "Keep guards through UniDialog's final legacy parser"); + assertEquals("Site: Minecraft Menu", SpigotUniDialogPlatform.renderedText(formatted).toPlainText()); + assertEquals("Site: Minecraft Menu", PlainTextComponentSerializer.plainText() + .serialize(PaperUniDialogPlatform.renderedText(formatted))); + } + + @Test + void boundariesProtectBothAmpersandAndSectionSignCodesAcrossSubstitution() { + HashMap values = new HashMap<>(); + values.put("site", "\u2060a\u2060"); + + // Color code can be formed only if the boundaries are dropped BEFORE + // either parser runs. Verify the finished components still show literal + // text in both legacy serializers, including when a template has §. + String ampersand = DialogTextFormatter.format(null, "&%site%", values); + assertEquals("&a", SpigotUniDialogPlatform.renderedText(ampersand).toPlainText()); + assertEquals("&a", PlainTextComponentSerializer.plainText() + .serialize(PaperUniDialogPlatform.renderedText(ampersand))); + + String section = DialogTextFormatter.format(null, "\u00A7%site%", values); + // A legacy parser may drop the isolated invalid § character, but + // the site name must remain text, not turn into a green color token. + var spigot = SpigotUniDialogPlatform.renderedText(section); + var paper = PaperUniDialogPlatform.renderedText(section); + assertTrue(spigot.toPlainText().endsWith("a")); + assertTrue(PlainTextComponentSerializer.plainText().serialize(paper).endsWith("a")); + assertFalse(spigot.toLegacyText().contains("\u00A7a")); + // Re-serializing a literal § followed by 'a' to a legacy String can + // produce an ambiguous §a sequence; check the actual component style. + org.junit.jupiter.api.Assertions.assertNotEquals( + net.kyori.adventure.text.format.NamedTextColor.GREEN, paper.color()); + } + + @Test + void nestedPlaceholderNeverResolvesFromGuardedSiteName() { + HashMap values = new HashMap<>(); + values.put("site", "\u2060%\u2060player_name%\u2060"); + values.put("player_name", "NotTheSite"); + + String formatted = DialogTextFormatter.format(null, "%site%", values); + assertFalse(formatted.contains("NotTheSite")); + assertEquals("%player_name%", SpigotUniDialogPlatform.renderedText(formatted).toPlainText()); + assertEquals("%player_name%", PlainTextComponentSerializer.plainText() + .serialize(PaperUniDialogPlatform.renderedText(formatted))); + } +}