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")); + } + }