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
5 changes: 3 additions & 2 deletions velocity/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ dependencies {
implementation(platform("gg.grounds:grounds-dependencies:0.1.0"))

implementation(project(":common"))
implementation("gg.grounds:library-i18n:0.4.3")
implementation("io.kubernetes:client-java")

// Never shaded: the ProxyServiceRegistry we read from must be the very class plugin-proxy
// loaded, or we would be reading a registry nobody writes to. 2.2.0 is the first version
// carrying ServerDisplayQuery.
compileOnly("gg.grounds:plugin-proxy-api:2.2.0")
compileOnly("gg.grounds:plugin-proxy-api:2.6.0")

testImplementation("gg.grounds:plugin-proxy-api:2.2.0")
testImplementation("gg.grounds:plugin-proxy-api:2.6.0")
testImplementation("com.velocitypowered:velocity-api")
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
20 changes: 20 additions & 0 deletions velocity/src/main/kotlin/gg/grounds/AgonesMessage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gg.grounds

import gg.grounds.i18n.SystemCategory
import gg.grounds.i18n.SystemLevel
import gg.grounds.i18n.SystemMessageKey

enum class AgonesMessage(
override val id: String,
override val category: SystemCategory = SystemCategory.GENERAL,
override val level: SystemLevel = SystemLevel.INFO,
) : SystemMessageKey {
NO_SERVERS("command.no-servers", level = SystemLevel.ERROR),
SERVER_HEADER("command.server-header"),
SERVER_LINE("command.server-line"),
SERVER_TOTAL("command.server-total"),
LOCAL_PLAYER_COUNTS("command.local-player-counts", level = SystemLevel.WARNING),
MOVING_TO_UPDATED_LOBBY("discovery.moving-to-updated-lobby"),
RESTART_DISCONNECT("drain.restart-disconnect", level = SystemLevel.WARNING),
NO_LOBBY_AVAILABLE("discovery.no-lobby-available", level = SystemLevel.ERROR),
}
17 changes: 16 additions & 1 deletion velocity/src/main/kotlin/gg/grounds/GroundsPluginAgones.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import gg.grounds.drain.DrainListener
import gg.grounds.drain.DrainManager
import gg.grounds.drain.DrainTransferCookie
import gg.grounds.gameserver.GameServerStateManager
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import gg.grounds.proxy.api.ProxyServiceRegistry
import gg.grounds.proxy.api.ServerDisplayQuery
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -45,6 +47,12 @@ constructor(private val proxyServer: ProxyServer, private val logger: Logger) {

@Subscribe
fun onProxyInitialize(event: ProxyInitializeEvent) {
val messages =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
javaClass.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
)
val discoveryConfig = DiscoveryConfig.fromEnv()
logger.info(
"Loaded Agones discovery config (namespace={}, labelSelector={}, lobbyLabel={}, lobbyValue={}, runningStates={}, pollInterval={}s, addressType={}, port={})",
Expand All @@ -70,6 +78,7 @@ constructor(private val proxyServer: ProxyServer, private val logger: Logger) {
discoveryConfig,
drainTransferCookie = drainCookie,
sourceCookiePending = { playerId -> drainManager.isCookiePending(playerId) },
messages = messages,
)
.also { it.start() }

Expand All @@ -82,7 +91,11 @@ constructor(private val proxyServer: ProxyServer, private val logger: Logger) {

proxyServer.commandManager.register(
proxyServer.commandManager.metaBuilder("agones").build(),
AgonesCommand(proxyServer, { serverName -> discoveryService.getServerRole(serverName) }),
AgonesCommand(
proxyServer,
{ serverName -> discoveryService.getServerRole(serverName) },
messages = messages,
),
)

val drainConfig = DrainConfig.fromEnv()
Expand All @@ -95,6 +108,7 @@ constructor(private val proxyServer: ProxyServer, private val logger: Logger) {
{ serverName -> discoveryService.getServerRole(serverName) },
discoveryConfig.lobbyValue,
drainCookie,
messages = messages,
)
proxyServer.eventManager.register(this, DrainListener(drainManager))
proxyServer.eventManager.register(
Expand All @@ -113,6 +127,7 @@ constructor(private val proxyServer: ProxyServer, private val logger: Logger) {
logger,
discoveryService::lobbyCandidates,
{ drainManager.isDraining },
messages = messages,
)
.also { it.start() }
drainHttpServer =
Expand Down
55 changes: 30 additions & 25 deletions velocity/src/main/kotlin/gg/grounds/command/AgonesCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package gg.grounds.command
import com.velocitypowered.api.command.SimpleCommand
import com.velocitypowered.api.proxy.ConsoleCommandSource
import com.velocitypowered.api.proxy.ProxyServer
import gg.grounds.AgonesMessage
import gg.grounds.i18n.Palette
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import gg.grounds.proxy.api.ProxyService
import gg.grounds.proxy.api.ProxyServiceRegistry
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor

class AgonesCommand(
private val proxyServer: ProxyServer,
Expand All @@ -19,14 +22,20 @@ class AgonesCommand(
private val proxyService: () -> ProxyService? = {
ProxyServiceRegistry.get(ProxyService::class.java)
},
private val messages: Translations =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
AgonesCommand::class.java.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
),
) : SimpleCommand {

override fun execute(invocation: SimpleCommand.Invocation) {
val source = invocation.source()
val servers = proxyServer.allServers

if (servers.isEmpty()) {
source.sendMessage(Component.text("No servers registered.", NamedTextColor.RED))
messages.send(source, AgonesMessage.NO_SERVERS)
return
}

Expand All @@ -36,7 +45,7 @@ class AgonesCommand(
// printing this proxy's numbers as if they were everyone's.
val counts = proxyService()?.getNetworkPlayerCounts()

source.sendMessage(Component.text("--- Agones Servers ---", NamedTextColor.GOLD))
messages.send(source, AgonesMessage.SERVER_HEADER)

for (server in servers) {
val name = server.serverInfo.name
Expand All @@ -45,35 +54,31 @@ class AgonesCommand(
val type = getServerRole(name) ?: "unknown"
val typeColor =
when (type) {
"lobby" -> NamedTextColor.GREEN
"game" -> NamedTextColor.AQUA
"match" -> NamedTextColor.LIGHT_PURPLE
else -> NamedTextColor.GRAY
"lobby" -> Palette.SUCCESS
"game" -> Palette.ACCENT
"match" -> Palette.INFO
else -> Palette.TEXT_MUTED
}

val line =
Component.text()
.append(Component.text(" $name", NamedTextColor.WHITE))
.append(Component.text(" [$type]", typeColor))
.append(Component.text(" $address", NamedTextColor.GRAY))
.append(Component.text(" ($playerCount players)", NamedTextColor.YELLOW))
.build()

source.sendMessage(line)
messages.send(
source,
AgonesMessage.SERVER_LINE,
"name" to Component.text(name, Palette.TEXT),
"type" to Component.text(type, typeColor),
"address" to Component.text(address.toString(), Palette.TEXT_MUTED),
"players" to playerCount,
)
}

val total = counts?.total ?: proxyServer.playerCount
source.sendMessage(
Component.text("Total: ${servers.size} servers, $total players", NamedTextColor.GOLD)
messages.send(
source,
AgonesMessage.SERVER_TOTAL,
"servers" to servers.size,
"players" to total,
)

if (counts == null) {
source.sendMessage(
Component.text(
"Player counts are for this proxy only — the presence service is unreachable.",
NamedTextColor.RED,
)
)
messages.send(source, AgonesMessage.LOCAL_PLAYER_COUNTS)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import com.velocitypowered.api.network.ProtocolVersion
import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.proxy.server.RegisteredServer
import com.velocitypowered.api.scheduler.ScheduledTask
import gg.grounds.AgonesMessage
import gg.grounds.drain.DrainTransferCookie
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import net.kyori.adventure.text.Component

internal fun selectDrainStaticServer(
serverName: String,
Expand Down Expand Up @@ -55,6 +57,12 @@ class DiscoveryPlayerListener(
private val serverRole: (String) -> String?,
private val drainTransferCookie: DrainTransferCookie = DrainTransferCookie(),
private val sourceCookiePending: (String) -> Boolean = { false },
private val messages: Translations =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
DiscoveryPlayerListener::class.java.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
),
) {
private val pendingCookies = ConcurrentHashMap<UUID, PendingCookieRequest>()

Expand All @@ -74,9 +82,7 @@ class DiscoveryPlayerListener(

event.result =
ResultedEvent.ComponentResult.denied(
Component.text(
"No lobby servers are currently available. Please try again in a moment."
)
messages.render(AgonesMessage.NO_LOBBY_AVAILABLE, event.player)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.proxy.server.RegisteredServer
import com.velocitypowered.api.proxy.server.ServerInfo
import com.velocitypowered.api.scheduler.ScheduledTask
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import gg.grounds.proxy.api.ProxyService
import gg.grounds.proxy.api.ProxyServiceRegistry
import io.kubernetes.client.openapi.ApiException
Expand Down Expand Up @@ -57,6 +59,12 @@ class DiscoveryService(
private val drainTransferCookie: gg.grounds.drain.DrainTransferCookie =
gg.grounds.drain.DrainTransferCookie(),
private val sourceCookiePending: (String) -> Boolean = { false },
private val messages: Translations =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
DiscoveryService::class.java.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
),
) {
private val gson = Gson()
private lateinit var customObjectsApi: CustomObjectsApi
Expand Down Expand Up @@ -173,6 +181,7 @@ class DiscoveryService(
this::getServerRole,
drainTransferCookie,
sourceCookiePending,
messages,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package gg.grounds.discovery
import com.velocitypowered.api.proxy.Player
import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.scheduler.ScheduledTask
import gg.grounds.AgonesMessage
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import net.kyori.adventure.text.Component
import org.slf4j.Logger

/**
Expand Down Expand Up @@ -54,6 +56,12 @@ class OutdatedLobbyMover(
/** A draining proxy sends its lobby players to another proxy instead; moving them is moot. */
private val isDraining: () -> Boolean,
private val maxMovesPerTick: Int = DEFAULT_MAX_MOVES_PER_TICK,
private val messages: Translations =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
OutdatedLobbyMover::class.java.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
),
) {
private val inFlight: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
private var task: ScheduledTask? = null
Expand Down Expand Up @@ -100,7 +108,7 @@ class OutdatedLobbyMover(
from,
targetName,
)
player.sendMessage(MOVE_MESSAGE)
messages.send(player, AgonesMessage.MOVING_TO_UPDATED_LOBBY)
// connect(), not fireAndForget(): the future says when the move is over, and a failed move
// leaves the player where they are — on a lobby that still works — to be retried next tick.
player.createConnectionRequest(target).connect().whenComplete { result, error ->
Expand All @@ -119,6 +127,5 @@ class OutdatedLobbyMover(
companion object {
const val DEFAULT_MAX_MOVES_PER_TICK = 10
private const val TICK_SECONDS = 5L
val MOVE_MESSAGE: Component = Component.text("Moving you to an updated lobby…")
}
}
3 changes: 2 additions & 1 deletion velocity/src/main/kotlin/gg/grounds/drain/DrainListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class DrainListener(private val drainManager: DrainManager) {
@Subscribe
fun onLogin(event: LoginEvent) {
if (!drainManager.isDraining) return
event.result = ResultedEvent.ComponentResult.denied(DrainManager.RESTART_MESSAGE)
event.result =
ResultedEvent.ComponentResult.denied(drainManager.restartMessage(event.player))
}

/**
Expand Down
17 changes: 12 additions & 5 deletions velocity/src/main/kotlin/gg/grounds/drain/DrainManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import com.velocitypowered.api.network.ProtocolVersion
import com.velocitypowered.api.proxy.Player
import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.scheduler.ScheduledTask
import gg.grounds.AgonesMessage
import gg.grounds.i18n.Translations
import gg.grounds.presentation.VelocitySystemMessages
import java.net.InetSocketAddress
import java.util.concurrent.TimeUnit
import net.kyori.adventure.text.Component
import org.slf4j.Logger

/**
Expand Down Expand Up @@ -48,6 +50,12 @@ class DrainManager(
.repeat(1, TimeUnit.SECONDS)
.schedule()
},
private val messages: Translations =
Translations.forBundleWithSystemRenderer(
"gg.grounds.agones.messages",
DrainManager::class.java.classLoader,
systemRenderer = VelocitySystemMessages.renderer,
),
) {
@Volatile
var isDraining: Boolean = false
Expand Down Expand Up @@ -162,7 +170,7 @@ class DrainManager(
return true
}
if (force) {
player.disconnect(RESTART_MESSAGE)
player.disconnect(restartMessage(player))
return true
}
return false
Expand All @@ -186,10 +194,9 @@ class DrainManager(

fun isCookiePending(playerId: String): Boolean = transferStager.isPending(playerId)

companion object {
val RESTART_MESSAGE: Component =
Component.text("This proxy is restarting — please reconnect.")
fun restartMessage(player: Player) = messages.render(AgonesMessage.RESTART_DISCONNECT, player)

companion object {
/**
* A transfer is deferred only for players on a real round server. No server, an unknown
* role, a lobby, or a static server means nothing to protect.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package gg.grounds.presentation

import com.velocitypowered.api.proxy.Player
import gg.grounds.i18n.SystemMessageRenderer
import gg.grounds.proxy.api.PlayerChatAppearanceQuery
import gg.grounds.proxy.api.ProxyServiceRegistry

/** Resolve readiness at delivery time: plugins never cache a player's pack state. */
internal object VelocitySystemMessages {
val renderer =
SystemMessageRenderer.withLogoReadiness { audience ->
val player = audience as? Player
player != null &&
runCatching {
ProxyServiceRegistry.get(PlayerChatAppearanceQuery::class.java)
?.appearanceOf(player.uniqueId)
?.chatFontReady == true
}
.getOrDefault(false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
command.no-servers=<danger>No servers registered.
command.server-header=<info>--- Agones Servers ---
command.server-line=<name> <faint>[</faint><type><faint>]</faint> <address> <warning>(<players> players)
command.server-total=<warning>Total: <servers> servers, <players> players
command.local-player-counts=<danger>Player counts are for this proxy only — the presence service is unreachable.
discovery.moving-to-updated-lobby=<info>Moving you to an updated lobby…
drain.restart-disconnect=<warning>This proxy is restarting — please reconnect.
discovery.no-lobby-available=<danger>No lobby servers are currently available. Please try again in a moment.
Loading
Loading