Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ than a gap.

The NATS auth-callout scopes each pod to the subjects declared in its bundle `events:` block, so `proxy.system.*` and `proxy.transfer.*` must be listed there — an undeclared subject is denied and the message vanishes.

## Maintenance in the server list

During an active maintenance window, every proxy reports a red `Wartungsarbeiten`
label in the server list's protocol/version field. Once the window ends or is
disabled, the normal version and player-count display returns on the next ping.

The maintenance document's `motd` is optional in effect: set it to an empty string
to keep the normal MOTD. A nonblank value overrides the description only while
maintenance is active. New maintenance drafts default to an empty override.

## Build

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.velocitypowered.api.plugin.Plugin
import com.velocitypowered.api.proxy.Player
import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier
import com.velocitypowered.api.proxy.server.ServerPing
import com.velocitypowered.api.scheduler.ScheduledTask
import gg.grounds.BuildInfo
import gg.grounds.i18n.Translations
Expand Down Expand Up @@ -50,6 +51,7 @@ import gg.grounds.proxy.velocity.tab.PlayerDisplayQueryImpl
import gg.grounds.proxy.velocity.tab.TabList
import io.nats.client.Subscription
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import net.kyori.adventure.text.minimessage.MiniMessage
Expand Down Expand Up @@ -526,8 +528,10 @@ constructor(private val proxy: ProxyServer, private val logger: Logger) {
fun onProxyPing(event: ProxyPingEvent) {
val count = networkPlayerCount
val motd = motdManager
val maintenanceMotd = maintenanceStore?.let { maintenance.motd() }
if (count == null && motd == null && maintenanceMotd == null) return
val now = Instant.now()
val maintenanceMotd = maintenanceStore?.let { maintenance.motd(now) }
val maintenanceProtocol = maintenanceStore?.let { maintenance.protocolName(now) }
if (count == null && motd == null && maintenanceProtocol == null) return

val builder = event.ping.asBuilder()
count?.let { builder.onlinePlayers(it) }
Expand All @@ -540,6 +544,7 @@ constructor(private val proxy: ProxyServer, private val logger: Logger) {
}
}
maintenanceMotd?.let { builder.description(MiniMessage.miniMessage().deserialize(it)) }
maintenanceProtocol?.let { builder.version(ServerPing.Version(-1, it)) }
event.ping = builder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ internal constructor(
}
val motd =
string("motd").also {
if (it.isEmpty() || it.length > 4096)
if (it.length > 4096)
throw MaintenanceStoreException("Maintenance motd is invalid")
}
val announcement =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,13 @@ class MaintenanceEngine(private val clock: Clock = Clock.systemUTC()) {
return crossed.minOrNull()?.let(::listOf) ?: emptyList()
}

@Synchronized
fun protocolName(now: Instant = clock.instant()): String? =
if (activeAt(now)) "§cWartungsarbeiten" else null

@Synchronized
fun motd(now: Instant = clock.instant()): String? =
document
?.takeIf {
it.enabled &&
!now.isBefore(it.startsAt) &&
(it.endsAt == null || now.isBefore(it.endsAt))
}
?.let { render(it.motd, now) }
document?.takeIf { activeAt(now) && it.motd.isNotBlank() }?.let { render(it.motd, now) }

@Synchronized
fun announcementMessage(now: Instant = clock.instant()): String? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class MaintenanceConfigStoreTest {
@Test
fun `an empty maintenance motd preserves the normal server description`() {
val valid = json("123e4567-e89b-12d3-a456-426614174000")
assertEquals(
"",
MaintenanceConfigStore.parse(valid.replace("\"motd\":\"motd\"", "\"motd\":\"\"")).motd,
)
}

@Test
fun `accepts canonical UUIDs of every version including floodgate UUIDs`() {
val document = MaintenanceConfigStore.parse(json("123e4567-e89b-12d3-a456-426614174000"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class MaintenanceEngineTest {
@Test
fun `maintenance protocol label follows the window on every proxy without overriding the motd`() {
val start = Instant.parse("2026-10-20T20:00:00Z")
val document = active(start, endsAt = start.plusSeconds(60)).copy(motd = "")
val proxies = listOf(MaintenanceEngine(), MaintenanceEngine())
proxies.forEach { engine ->
engine.adopt(document, start.minusSeconds(1))
assertEquals(null, engine.protocolName(start.minusSeconds(1)))
assertEquals("§cWartungsarbeiten", engine.protocolName(start))
assertEquals(null, engine.motd(start))
assertEquals(null, engine.protocolName(start.plusSeconds(60)))
engine.adopt(document.copy(enabled = false), start)
assertEquals(null, engine.protocolName(start))
}
}

@Test
fun `an optional maintenance motd is rendered only during the active window`() {
val start = Instant.parse("2026-10-20T20:00:00Z")
val engine = MaintenanceEngine()
engine.adopt(active(start, endsAt = start.plusSeconds(60)), start)
assertEquals(null, engine.motd(start.minusSeconds(1)))
assertEquals("motd", engine.motd(start))
assertEquals(null, engine.motd(start.plusSeconds(60)))
engine.adopt(active(start).copy(motd = " "), start)
assertEquals(null, engine.motd(start))
}

private val id = UUID.fromString("00000000-0000-0000-0000-000000000001")
private val player = UUID.fromString("00000000-0000-0000-0000-000000000002")

Expand Down
Loading