diff --git a/src/main/java/world/bentobox/chunkblock/listeners/BossBarListener.java b/src/main/java/world/bentobox/chunkblock/listeners/BossBarListener.java
index 8ad2450..36fe748 100644
--- a/src/main/java/world/bentobox/chunkblock/listeners/BossBarListener.java
+++ b/src/main/java/world/bentobox/chunkblock/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.chunkblock.ChunkBlock;
import world.bentobox.chunkblock.dataobjects.OneBlockIslands;
import world.bentobox.chunkblock.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 BOSSBAR_METADATA = "chunkblock.bossbar";
public static final String ACTIONBAR_METADATA = "chunkblock.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(ChunkBlock addon) {
super();
this.addon = addon;
@@ -94,16 +89,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/chunkblock/listeners/HoloListener.java b/src/main/java/world/bentobox/chunkblock/listeners/HoloListener.java
index 1cf5de3..3827971 100644
--- a/src/main/java/world/bentobox/chunkblock/listeners/HoloListener.java
+++ b/src/main/java/world/bentobox/chunkblock/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.chunkblock.ChunkBlock;
import world.bentobox.bentobox.util.Util;
import world.bentobox.chunkblock.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 bd21f74..66fcf76 100644
--- a/src/main/resources/phases/0_plains.yml
+++ b/src/main/resources/phases/0_plains.yml
@@ -16,6 +16,12 @@
700: CHEST_WITH_WATER_BUCKET
# Hologram Lines to Display
# The First (Before Phase 1) Hologram is Located in your Locale.
+ # The text can use any of these, 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!
holograms:
0: "&aGood Luck!"
biome: PLAINS
diff --git a/src/test/java/world/bentobox/chunkblock/listeners/BossBarListenerTest.java b/src/test/java/world/bentobox/chunkblock/listeners/BossBarListenerTest.java
index 1ac17ba..4bd49b7 100644
--- a/src/test/java/world/bentobox/chunkblock/listeners/BossBarListenerTest.java
+++ b/src/test/java/world/bentobox/chunkblock/listeners/BossBarListenerTest.java
@@ -1,5 +1,7 @@
package world.bentobox.chunkblock.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;
@@ -23,6 +25,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.bentobox.api.events.flags.FlagSettingChangeEvent;
import world.bentobox.bentobox.api.events.island.IslandEnterEvent;
import world.bentobox.bentobox.api.events.island.IslandExitEvent;
@@ -179,4 +184,69 @@ void testAllHandlersInertWhenAddonNeverEnabled() {
verify(bossBar, never()).addPlayer(any());
verify(bossBar, never()).removePlayer(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() {
+ Component c = BossBarListener.bukkitToAdventure("Plains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ assertTrue(legacy(c).contains("\u00a7a"), "expected green in " + legacy(c));
+ assertTrue(legacy(c).contains("\u00a7l"), "expected bold in " + legacy(c));
+ }
+
+ /**
+ * 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("\u00a7aPlains");
+ assertEquals("Plains", PlainTextComponentSerializer.plainText().serialize(c));
+ assertTrue(legacy(c).contains("\u00a7a"), "expected green in " + legacy(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/chunkblock/listeners/HoloListenerTest.java b/src/test/java/world/bentobox/chunkblock/listeners/HoloListenerTest.java
index e06b58b..e77ec3d 100644
--- a/src/test/java/world/bentobox/chunkblock/listeners/HoloListenerTest.java
+++ b/src/test/java/world/bentobox/chunkblock/listeners/HoloListenerTest.java
@@ -1,6 +1,8 @@
package world.bentobox.chunkblock.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;
@@ -29,8 +31,14 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
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.chunkblock.ChunkBlock;
import world.bentobox.chunkblock.CommonTestSetup;
import world.bentobox.chunkblock.Settings;
@@ -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"));
+ }
+
}