diff --git a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java
index ece5aa2..e804ccf 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/BossBarListener.java
@@ -19,7 +19,6 @@
import org.eclipse.jdt.annotation.NonNull;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
import world.bentobox.aoneblock.events.MagicBlockEvent;
@@ -28,6 +27,7 @@
import world.bentobox.bentobox.api.events.island.IslandExitEvent;
import world.bentobox.bentobox.api.metadata.MetaDataValue;
import world.bentobox.bentobox.api.user.User;
+import world.bentobox.bentobox.util.Util;
import world.bentobox.bentobox.database.objects.Island;
public class BossBarListener implements Listener {
@@ -35,11 +35,6 @@ public class BossBarListener implements Listener {
private static final String AONEBLOCK_BOSSBAR = "aoneblock.bossbar";
public static final String AONEBLOCK_ACTIONBAR = "aoneblock.actionbar";
- private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
- .character('&')
- .hexColors() // Enables support for modern hex codes (e.g., FF0000) alongside legacy codes.
- .build();
-
public BossBarListener(AOneBlock addon) {
super();
this.addon = addon;
@@ -78,16 +73,21 @@ public void onFlagChange(FlagSettingChangeEvent e) {
}
/**
- * Converts a string containing Bukkit color codes ('&') into an Adventure Component.
+ * Converts a formatted string into an Adventure Component.
+ *
+ * Accepts MiniMessage tags, {@code &} or {@code §} legacy codes, hex ({@code RRGGBB}), or a
+ * mixture of them. Handling {@code §} matters here because translations arrive already
+ * converted to {@code §} codes by BentoBox - a serializer bound to {@code &} would leave those
+ * in the output as literal text.
*
- * @param legacyString The string with Bukkit color and format codes.
+ * @param text The string with color and format codes.
* @return The resulting Adventure Component.
*/
- public static Component bukkitToAdventure(String legacyString) {
- if (legacyString == null) {
+ public static Component bukkitToAdventure(String text) {
+ if (text == null) {
return Component.empty();
}
- return LEGACY_SERIALIZER.deserialize(legacyString);
+ return Util.parseMiniMessageOrLegacy(text);
}
private void tryToShowActionBar(UUID uuid, Island island) {
diff --git a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
index 5ceff03..9e29080 100644
--- a/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
+++ b/src/main/java/world/bentobox/aoneblock/listeners/HoloListener.java
@@ -15,7 +15,6 @@
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
-import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.bentobox.util.Util;
import world.bentobox.aoneblock.dataobjects.OneBlockIslands;
@@ -131,6 +130,10 @@ private Location getHologramLocation(Island island) {
/**
* Creates a new hologram (TextDisplay) at the given location.
* Caches the hologram for future reference.
+ *
+ * The text may use MiniMessage tags, {@code &} or {@code §} legacy codes, hex
+ * ({@code RRGGBB}), or a mixture. Phase file hologram lines are read straight from YAML and
+ * never see BentoBox's translation, so this is the only place their formatting is resolved.
*
* @param pos the location to create the hologram at
* @param text the text to display
@@ -140,7 +143,7 @@ private void createHologram(Location pos, String text) {
display.setAlignment(TextDisplay.TextAlignment.CENTER);
display.setBillboard(Billboard.CENTER);
display.setPersistent(true);
- display.text(LegacyComponentSerializer.legacyAmpersand().deserialize(text));
+ display.text(Util.parseMiniMessageOrLegacy(text));
activeHolograms.add(pos);
}
diff --git a/src/main/resources/phases/0_plains.yml b/src/main/resources/phases/0_plains.yml
index f235a97..afda882 100644
--- a/src/main/resources/phases/0_plains.yml
+++ b/src/main/resources/phases/0_plains.yml
@@ -118,7 +118,13 @@
# -------------------------------------------------------------------------
# KEY = position within this phase, counting from 0 - same numbering as
# fixedBlocks above.
- # VALUE = the text, with & colour codes.
+ # VALUE = the text. Any of these work, and they can be mixed:
+ # &a&lGood Luck! legacy colour and format codes
+ # 7FF55Good Luck! hex colour
+ # Good Luck! MiniMessage tags
+ # MiniMessage also gives you gradients, e.g.
+ # Good Luck!
+ # Use \n for a line break.
# The very first hologram, shown before phase 1 starts, is in the locale file
# rather than here.
holograms:
diff --git a/src/test/java/world/bentobox/aoneblock/listeners/BossBarListenerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/BossBarListenerTest.java
index 8bc4bec..599fec6 100644
--- a/src/test/java/world/bentobox/aoneblock/listeners/BossBarListenerTest.java
+++ b/src/test/java/world/bentobox/aoneblock/listeners/BossBarListenerTest.java
@@ -1,5 +1,7 @@
package world.bentobox.aoneblock.listeners;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
@@ -21,6 +23,9 @@
import org.mockito.Mock;
import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import world.bentobox.aoneblock.AOneBlock;
import world.bentobox.aoneblock.CommonTestSetup;
import world.bentobox.aoneblock.Settings;
@@ -141,4 +146,69 @@ void testBossBarNotShownWhenDisabledInConfig() {
mockedBukkit.verify(() -> Bukkit.createBossBar(anyString(), any(), any()), never());
verify(bossBar, never()).addPlayer(any());
}
+
+ /**
+ * Serializes to legacy section codes so a test can assert on the formatting that actually
+ * comes out, without depending on how the component tree happens to be nested.
+ */
+ private static String legacy(Component c) {
+ return LegacyComponentSerializer.legacySection().serialize(c);
+ }
+
+ /**
+ * MiniMessage tags used to be rendered as literal text because the serializer only understood
+ * legacy codes.
+ */
+ @Test
+ void testBukkitToAdventureParsesMiniMessage() {
+ String result = legacy(BossBarListener.bukkitToAdventure("Plains"));
+ assertEquals("Plains", PlainTextComponentSerializer.plainText()
+ .serialize(BossBarListener.bukkitToAdventure("Plains")));
+ assertTrue(result.contains("\u00a7a"), "expected green in " + result);
+ assertTrue(result.contains("\u00a7l"), "expected bold in " + result);
+ }
+
+ /**
+ * MiniMessage gradients, which legacy codes cannot express at all.
+ */
+ @Test
+ void testBukkitToAdventureParsesGradient() {
+ Component c = BossBarListener.bukkitToAdventure("Plains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ }
+
+ /**
+ * Translations reach this method already converted to section codes by BentoBox, so a
+ * serializer bound to '&' would leave them in the output as literal text.
+ */
+ @Test
+ void testBukkitToAdventureParsesSectionCodes() {
+ Component c = BossBarListener.bukkitToAdventure("§aPlains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ }
+
+ /**
+ * Legacy '&' codes must keep working - every existing locale file uses them.
+ */
+ @Test
+ void testBukkitToAdventureParsesLegacyAmpersand() {
+ Component c = BossBarListener.bukkitToAdventure("&aPlains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ assertTrue(legacy(c).contains("\u00a7a"), "expected green in " + legacy(c));
+ }
+
+ /**
+ * Hex colours, which the previous serializer supported here and must not regress.
+ */
+ @Test
+ void testBukkitToAdventureParsesHex() {
+ Component c = BossBarListener.bukkitToAdventure("7FF55Plains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ assertEquals(TextColor.fromHexString("#55FF55"), c.color());
+ }
+
+ @Test
+ void testBukkitToAdventureNullIsEmpty() {
+ assertEquals(Component.empty(), BossBarListener.bukkitToAdventure(null));
+ }
}
diff --git a/src/test/java/world/bentobox/aoneblock/listeners/HoloListenerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/HoloListenerTest.java
index 2b1dfd9..df9e99f 100644
--- a/src/test/java/world/bentobox/aoneblock/listeners/HoloListenerTest.java
+++ b/src/test/java/world/bentobox/aoneblock/listeners/HoloListenerTest.java
@@ -1,6 +1,8 @@
package world.bentobox.aoneblock.listeners;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -24,6 +26,12 @@
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
+import org.mockito.ArgumentCaptor;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import org.junit.jupiter.api.AfterEach;
@@ -198,4 +206,62 @@ void testProcess() {
verify(sch).runTaskLater(isNull(), any(Runnable.class), anyLong());
}
+ /**
+ * Captures the component the hologram was actually given.
+ */
+ private Component displayed(String hologramLine) {
+ when(phase.getHologramLine(anyInt())).thenReturn(hologramLine);
+ // process() writes the line to the data object then reads it straight back, and that
+ // object is a mock, so the read has to be stubbed too or it returns the setUp default.
+ when(is.getHologram()).thenReturn(hologramLine);
+ hl.process(island, is, phase);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Component.class);
+ verify(hologram).text(captor.capture());
+ return captor.getValue();
+ }
+
+ /**
+ * Phase file hologram lines are read straight from YAML, so this is the only place their
+ * formatting is resolved. MiniMessage tags used to appear as literal text.
+ */
+ @Test
+ void testHologramParsesMiniMessage() {
+ Component c = displayed("Plains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ String legacy = LegacyComponentSerializer.legacySection().serialize(c);
+ assertTrue(legacy.contains("\u00a7a"), "expected green in " + legacy);
+ assertTrue(legacy.contains("\u00a7l"), "expected bold in " + legacy);
+ }
+
+ /**
+ * Legacy '&' codes must keep working - every existing phase file uses them.
+ */
+ @Test
+ void testHologramParsesLegacyAmpersand() {
+ Component c = displayed("&aGood Luck!");
+ assertEquals("Good Luck!", PlainTextComponentSerializer.plainText().serialize(c));
+ assertTrue(LegacyComponentSerializer.legacySection().serialize(c).contains("\u00a7a"));
+ }
+
+ /**
+ * Hex was not supported here before - the serializer was built without hex enabled.
+ */
+ @Test
+ void testHologramParsesHex() {
+ Component c = displayed("7FF55Good Luck!");
+ assertEquals("Good Luck!", PlainTextComponentSerializer.plainText().serialize(c));
+ assertEquals(TextColor.fromHexString("#55FF55"), c.color());
+ }
+
+ /**
+ * The starting hologram comes from the locale file via User.getTranslation, which hands back
+ * section codes. A serializer bound to '&' left those in as literal text.
+ */
+ @Test
+ void testHologramParsesSectionCodes() {
+ Component c = displayed("\u00a7aWelcome");
+ assertEquals("Welcome", PlainTextComponentSerializer.plainText().serialize(c));
+ assertTrue(LegacyComponentSerializer.legacySection().serialize(c).contains("\u00a7a"));
+ }
+
}