From 3490599c3dcea918ccbfca19f6fed9e5c41feca3 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Tue, 25 Aug 2026 20:40:08 +0200 Subject: [PATCH 1/2] fix: pad the tab list around the wordmark Vanilla sizes the dark box from player rows, not the header, so the logo overflowed short names. Stretch rows to the wordmark width and give the glyph space on every side. Co-authored-by: Cursor --- .../gg/grounds/proxy/velocity/tab/TabBadge.kt | 10 ++++++-- .../grounds/proxy/velocity/tab/TabGlyphs.kt | 7 ++++++ .../gg/grounds/proxy/velocity/tab/TabName.kt | 23 +++++++++++++++++-- .../gg/grounds/proxy/messages.properties | 3 ++- .../grounds/proxy/velocity/tab/TabListTest.kt | 11 +++++++++ .../grounds/proxy/velocity/tab/TabNameTest.kt | 19 +++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabBadge.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabBadge.kt index daa4077..7e1cf71 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabBadge.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabBadge.kt @@ -6,13 +6,19 @@ import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextColor object TabBadge { - fun chip(label: String, fill: TextColor): Component { + fun width(label: String): Int { val textWidth = VanillaAdvances.width(label) val pad = 4 val inner = max(textWidth + pad, TabGlyphs.LEFT_PX + TabGlyphs.RIGHT_PX + TabGlyphs.MIDDLE_PX) val middles = inner - TabGlyphs.LEFT_PX - TabGlyphs.RIGHT_PX - val badgeWidth = TabGlyphs.LEFT_PX + middles * TabGlyphs.MIDDLE_PX + TabGlyphs.RIGHT_PX + return TabGlyphs.LEFT_PX + middles * TabGlyphs.MIDDLE_PX + TabGlyphs.RIGHT_PX + } + + fun chip(label: String, fill: TextColor): Component { + val textWidth = VanillaAdvances.width(label) + val badgeWidth = width(label) + val middles = badgeWidth - TabGlyphs.LEFT_PX - TabGlyphs.RIGHT_PX val padLeft = (badgeWidth - textWidth) / 2 val padRight = badgeWidth - textWidth - padLeft val gap = TabSpaces.of(-1) diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt index 9353c21..1bee696 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt @@ -11,4 +11,11 @@ object TabGlyphs { const val LEFT_PX = 3 const val MIDDLE_PX = 1 const val RIGHT_PX = 3 + /** Matches `TabFont` bitmap height and the committed `tab/logo.png`. */ + const val LOGO_HEIGHT = 32 + const val LOGO_TEXTURE_WIDTH = 256 + const val LOGO_TEXTURE_HEIGHT = 52 + const val LOGO_ADVANCE = LOGO_TEXTURE_WIDTH * LOGO_HEIGHT / LOGO_TEXTURE_HEIGHT + const val LOGO_PAD = 16 + const val HEADER_WIDTH = LOGO_PAD + LOGO_ADVANCE + LOGO_PAD } diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabName.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabName.kt index f9400b8..a8446b8 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabName.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabName.kt @@ -7,6 +7,8 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor object TabName { + private const val GAP = 2 + fun format(name: String, locale: Locale?, role: PlayerRole?): Component { val colour = role?.colour?.let(TextColor::fromHexString) ?: Palette.TEXT val row = Component.text() @@ -15,16 +17,33 @@ object TabName { ?.takeIf { it.isNotBlank() } ?.let { language -> row.append(TabBadge.chip(language.uppercase(Locale.ROOT), Palette.TEXT_FAINT)) - row.append(Component.text(TabSpaces.of(2)).font(TabGlyphs.FONT)) + row.append(Component.text(TabSpaces.of(GAP)).font(TabGlyphs.FONT)) } role ?.name ?.takeIf { it.isNotBlank() } ?.let { rank -> row.append(TabBadge.chip(rank.uppercase(Locale.ROOT), colour)) - row.append(Component.text(TabSpaces.of(2)).font(TabGlyphs.FONT)) + row.append(Component.text(TabSpaces.of(GAP)).font(TabGlyphs.FONT)) } row.append(Component.text(name, colour)) + val pad = TabGlyphs.HEADER_WIDTH - rowWidth(name, locale, role) + if (pad > 0) { + row.append(Component.text(TabSpaces.of(pad)).font(TabGlyphs.FONT)) + } return row.build() } + + private fun rowWidth(name: String, locale: Locale?, role: PlayerRole?): Int { + var width = VanillaAdvances.width(name) + locale + ?.language + ?.takeIf { it.isNotBlank() } + ?.let { width += TabBadge.width(it.uppercase(Locale.ROOT)) + GAP } + role + ?.name + ?.takeIf { it.isNotBlank() } + ?.let { width += TabBadge.width(it.uppercase(Locale.ROOT)) + GAP } + return width + } } diff --git a/velocity/src/main/resources/gg/grounds/proxy/messages.properties b/velocity/src/main/resources/gg/grounds/proxy/messages.properties index 49278bf..9735175 100644 --- a/velocity/src/main/resources/gg/grounds/proxy/messages.properties +++ b/velocity/src/main/resources/gg/grounds/proxy/messages.properties @@ -1,4 +1,5 @@ -tab.header=\n\uE000\n +# Newlines pad the wordmark vertically; U+E018 is TabSpaces.of(16) on each side. +tab.header=\n\n\n\n\uE018\uE000\uE018\n\n tab.footer=\n Ping \ngrounds.gg tab.server.lobby=Lobby tab.server.game=Game diff --git a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabListTest.kt b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabListTest.kt index 5e41b28..9911aa8 100644 --- a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabListTest.kt +++ b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabListTest.kt @@ -49,6 +49,17 @@ class TabListTest { assertFalse(header.contains("Grounds Network"), header) } + @Test + fun `the header pads the wordmark on every side`() { + val header = plain(messages.render(ProxyMessage.TAB_HEADER, Locale.ENGLISH)) + val side = TabSpaces.of(TabGlyphs.LOGO_PAD) + assertTrue(header.contains("$side${TabGlyphs.LOGO}$side"), header) + val lines = header.lines() + assertTrue(lines.size >= 6, header) + assertTrue(lines.first().isEmpty(), header) + assertTrue(lines.last().isEmpty(), header) + } + @Test fun `the header is not prefixed - the wordmark already says whose network this is`() { val header = plain(messages.render(ProxyMessage.TAB_HEADER, Locale.ENGLISH)) diff --git a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabNameTest.kt b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabNameTest.kt index e9211e2..e5d51ca 100644 --- a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabNameTest.kt +++ b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabNameTest.kt @@ -3,6 +3,7 @@ package gg.grounds.proxy.velocity.tab import gg.grounds.proxy.api.PlayerRole import java.util.Locale import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -30,4 +31,22 @@ class TabNameTest { assertFalse(text.contains("DE"), text) assertTrue(text.contains("Steve"), text) } + + @Test + fun `a short name is padded to the wordmark width`() { + val name = "A" + val text = + PlainTextComponentSerializer.plainText().serialize(TabName.format(name, null, null)) + val pad = TabGlyphs.HEADER_WIDTH - VanillaAdvances.width(name) + assertTrue(pad > 0) + assertTrue(text.contains(TabSpaces.of(pad)), text) + } + + @Test + fun `a name that is already wider than the wordmark is not padded`() { + val name = "A".repeat(40) + val text = + PlainTextComponentSerializer.plainText().serialize(TabName.format(name, null, null)) + assertEquals(name, text) + } } From 54ee9ec40ba33f5a08e42e767e5ad7b9099d8cf0 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Thu, 27 Aug 2026 10:20:31 +0200 Subject: [PATCH 2/2] fix: pad tab rows to the client wordmark advance Minecraft rounds the scaled bitmap width and then adds the 1px glyph gap, so the logo advances 159px rather than the truncated 157. Co-authored-by: Cursor --- .../gg/grounds/proxy/velocity/tab/TabGlyphs.kt | 8 +++++++- .../grounds/proxy/velocity/tab/TabGlyphsTest.kt | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphsTest.kt diff --git a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt index 1bee696..53850f2 100644 --- a/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt +++ b/velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphs.kt @@ -15,7 +15,13 @@ object TabGlyphs { const val LOGO_HEIGHT = 32 const val LOGO_TEXTURE_WIDTH = 256 const val LOGO_TEXTURE_HEIGHT = 52 - const val LOGO_ADVANCE = LOGO_TEXTURE_WIDTH * LOGO_HEIGHT / LOGO_TEXTURE_HEIGHT + /** + * The client rounds the scaled texture width, then adds the 1px gap every bitmap glyph carries + * (the same gap [TabBadge] cancels between slices). Truncating `256 * 32 / 52` is 157; the + * glyph actually advances 159. + */ + const val LOGO_ADVANCE = + (LOGO_TEXTURE_WIDTH * LOGO_HEIGHT + LOGO_TEXTURE_HEIGHT / 2) / LOGO_TEXTURE_HEIGHT + 1 const val LOGO_PAD = 16 const val HEADER_WIDTH = LOGO_PAD + LOGO_ADVANCE + LOGO_PAD } diff --git a/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphsTest.kt b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphsTest.kt new file mode 100644 index 0000000..96a1e6f --- /dev/null +++ b/velocity/src/test/kotlin/gg/grounds/proxy/velocity/tab/TabGlyphsTest.kt @@ -0,0 +1,16 @@ +package gg.grounds.proxy.velocity.tab + +import kotlin.math.roundToInt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TabGlyphsTest { + @Test + fun `logo advance is the rounded scaled width plus the bitmap gap`() { + val scaled = + (TabGlyphs.LOGO_TEXTURE_WIDTH.toDouble() * TabGlyphs.LOGO_HEIGHT / + TabGlyphs.LOGO_TEXTURE_HEIGHT) + .roundToInt() + assertEquals(scaled + 1, TabGlyphs.LOGO_ADVANCE) + } +}