diff --git a/src/main/java/world/bentobox/chunkblock/commands/island/IslandChunksCommand.java b/src/main/java/world/bentobox/chunkblock/commands/island/IslandChunksCommand.java index 0d5db2e..0aa6d43 100644 --- a/src/main/java/world/bentobox/chunkblock/commands/island/IslandChunksCommand.java +++ b/src/main/java/world/bentobox/chunkblock/commands/island/IslandChunksCommand.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.Optional; +import net.kyori.adventure.key.Key; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; @@ -23,6 +24,14 @@ public class IslandChunksCommand extends CompositeCommand { /** Widest map that still fits comfortably in chat */ private static final int MAX_MAP_RADIUS = 7; + /** + * Minecraft's built-in fixed-width font. Chat's default font is proportional, so a + * grid built from mixed glyphs comes out ragged — a row's width depends on which + * chunks happen to be claimed. Only the map rows use it; the rest of the chat stays + * in the normal font. + */ + private static final Key MONOSPACE_FONT = Key.key("minecraft", "uniform"); + private ChunkBlock addon; public IslandChunksCommand(CompositeCommand islandCommand, String label, String[] aliases) { @@ -97,7 +106,8 @@ private void showMap(User user, Island island, int unlocked, int max) { row.append(here ? "&b◇" : "&7□"); } } - user.sendMessage("chunkblock.chunks.map.row", "[row]", row.toString()); + user.sendMessage(user.getTranslationAsComponent("chunkblock.chunks.map.row", "[row]", row.toString()) + .font(MONOSPACE_FONT)); } user.sendMessage("chunkblock.chunks.map.legend", "[cost]", String.valueOf(cm.getChunkCost())); } diff --git a/src/test/java/world/bentobox/chunkblock/commands/island/IslandChunksCommandTest.java b/src/test/java/world/bentobox/chunkblock/commands/island/IslandChunksCommandTest.java index a82fe0f..98b7b41 100644 --- a/src/test/java/world/bentobox/chunkblock/commands/island/IslandChunksCommandTest.java +++ b/src/test/java/world/bentobox/chunkblock/commands/island/IslandChunksCommandTest.java @@ -2,6 +2,9 @@ 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.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -10,6 +13,10 @@ import java.util.List; import java.util.Optional; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; + import org.bukkit.Location; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -72,6 +79,12 @@ public void setUp() throws Exception { when(user.getWorld()).thenReturn(world); when(im.getIslandAt(playerLocation)).thenReturn(Optional.of(island)); + // Hand the row string straight back as a component so the test can read it out again + when(user.getTranslationAsComponent(anyString(), any(String[].class))).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + return Component.text((String) args[args.length - 1]); + }); + command = new IslandChunksCommand(ac, "chunks", new String[] { "chunks" }); } @@ -130,12 +143,20 @@ void testMapIsCappedAtTheWidestRowThatFitsChat() { assertEquals(15, mapRows().size()); } - /** The rendered map rows, in order, as passed to the row locale key */ + /** + * The rendered map rows, in order. Rows are sent as components so they can carry the + * monospace font, so they are read back out of the component rather than the arguments. + */ private List mapRows() { - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(user, org.mockito.Mockito.atLeastOnce()).sendMessage(org.mockito.ArgumentMatchers.eq( - "chunkblock.chunks.map.row"), org.mockito.ArgumentMatchers.eq("[row]"), captor.capture()); - return new ArrayList<>(captor.getAllValues()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Component.class); + verify(user, atLeastOnce()).sendMessage(captor.capture()); + List rows = new ArrayList<>(); + for (Component component : captor.getAllValues()) { + assertEquals(Key.key("minecraft", "uniform"), component.font(), + "map rows must be monospaced or the grid comes out ragged"); + rows.add(((TextComponent) component).content()); + } + return rows; } /** The glyph at the middle of a row, colour code included */