From d82c45dcdc91e1d93508efbace0457d9fc3a2e95 Mon Sep 17 00:00:00 2001 From: Hendrik Brombeer Date: Fri, 24 Jul 2026 16:19:09 +0200 Subject: [PATCH] feat(presence): report the proxy's region, and count players by proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session now carries where the proxy is, and the network can be counted per proxy rather than only per backend server. REGION is passed through exactly as the environment gives it, empty included. An absent region is a normal value all the way down — the column is nullable, the proto field tolerates empty, and inventing a plausible location for a proxy that never declared one would be worse than admitting we do not know. An empty region string becomes null at the API boundary so callers have a single shape for "unknown" rather than having to check for both. --- common/build.gradle.kts | 2 +- .../presence/GrpcPlayerPresenceClient.kt | 20 ++++++++++++++++++- velocity/build.gradle.kts | 4 ++-- .../listener/PlayerConnectionListener.kt | 11 +++++++++- .../grounds/presence/PlayerPresenceService.kt | 18 +++++++++++++++-- .../presence/PlayerSessionQueryImpl.kt | 20 +++++++++++++++++++ 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 72e0b6a..7e78ee3 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -11,7 +11,7 @@ repositories { } dependencies { - protobuf("gg.grounds:library-grpc-contracts-player:0.4.0") + protobuf("gg.grounds:library-grpc-contracts-player:0.5.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.4") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.4") diff --git a/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt index 140a494..3815bd9 100644 --- a/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt +++ b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt @@ -1,5 +1,7 @@ package gg.grounds.player.presence +import gg.grounds.grpc.player.CountPlayersByProxyReply +import gg.grounds.grpc.player.CountPlayersByProxyRequest import gg.grounds.grpc.player.CountPlayersByServerReply import gg.grounds.grpc.player.CountPlayersByServerRequest import gg.grounds.grpc.player.GetPlayerSessionRequest @@ -25,7 +27,12 @@ private constructor( private val channel: ManagedChannel, private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, ) : AutoCloseable { - fun tryLogin(playerId: UUID, playerName: String = "", proxyId: String = ""): PlayerLoginResult { + fun tryLogin( + playerId: UUID, + playerName: String = "", + proxyId: String = "", + region: String = "", + ): PlayerLoginResult { return try { val reply = stub @@ -35,6 +42,7 @@ private constructor( .setPlayerId(playerId.toString()) .setPlayerName(playerName) .setProxyId(proxyId) + .setRegion(region) .build() ) PlayerLoginResult.Success(reply) @@ -136,6 +144,16 @@ private constructor( } } + fun countPlayersByProxy(): CountPlayersByProxyReply? { + return try { + stub + .withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .countPlayersByProxy(CountPlayersByProxyRequest.newBuilder().build()) + } catch (e: RuntimeException) { + null + } + } + fun updateServer(playerId: UUID, serverName: String): Boolean { return try { stub diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index b2c1bda..aca2526 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -14,14 +14,14 @@ dependencies { implementation(project(":common")) // plugin-proxy owns the ProxyServiceRegistry at runtime — compileOnly, never shaded, or the // registry this plugin writes into would be a different class from the one chat/social read. - compileOnly("gg.grounds:plugin-proxy-api:0.2.0") + compileOnly("gg.grounds:plugin-proxy-api:0.3.0") implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.0.4") implementation("tools.jackson.module:jackson-module-kotlin:3.0.4") implementation("io.grpc:grpc-netty-shaded:1.78.0") // compileOnly above is not visible to tests; PlayerSessionQueryImplTest needs the interface's // types. - testImplementation("gg.grounds:plugin-proxy-api:0.2.0") + testImplementation("gg.grounds:plugin-proxy-api:0.3.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.4") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.4") testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.13.4") diff --git a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt index 7a6f1a4..dc42694 100644 --- a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt +++ b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt @@ -46,7 +46,9 @@ class PlayerConnectionListener( return EventTask.async { // The name is what makes the session findable from another proxy — without it a player // exists in presence but nobody can /msg them. PROXY_ID says which proxy holds them. - when (val result = playerPresenceService.tryLogin(playerId, name, proxyId())) { + when ( + val result = playerPresenceService.tryLogin(playerId, name, proxyId(), region()) + ) { is PlayerLoginResult.Success -> { if (handleSuccess(event, name, playerId, result.reply)) { return@async @@ -160,6 +162,13 @@ class PlayerConnectionListener( private fun proxyId(): String = System.getenv("PROXY_ID") ?: "" + /** + * Empty when the proxy declares no region. Passed through as-is rather than defaulted to + * something plausible: an invented location is worse than an absent one, and the session schema + * treats unknown as a normal value. + */ + private fun region(): String = System.getenv("REGION") ?: "" + private fun watchForAbandonedLogin(playerId: UUID, name: String) { pendingLogins.add(playerId) proxy.scheduler diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt index 9b4520a..3b8fe5a 100644 --- a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt @@ -1,5 +1,6 @@ package gg.grounds.presence +import gg.grounds.grpc.player.CountPlayersByProxyReply import gg.grounds.grpc.player.CountPlayersByServerReply import gg.grounds.grpc.player.PlayerLogoutReply import gg.grounds.grpc.player.PlayerSessionInfo @@ -22,9 +23,14 @@ class PlayerPresenceService : AutoCloseable { client = GrpcPlayerPresenceClient.create(target) } - fun tryLogin(playerId: UUID, playerName: String, proxyId: String): PlayerLoginResult { + fun tryLogin( + playerId: UUID, + playerName: String, + proxyId: String, + region: String, + ): PlayerLoginResult { return try { - client.tryLogin(playerId, playerName, proxyId) + client.tryLogin(playerId, playerName, proxyId, region) } catch (e: RuntimeException) { PlayerLoginResult.Error(e.message ?: e::class.java.name) } @@ -66,6 +72,14 @@ class PlayerPresenceService : AutoCloseable { } } + fun countPlayersByProxy(): CountPlayersByProxyReply? { + return try { + client.countPlayersByProxy() + } catch (e: RuntimeException) { + null + } + } + fun updateServer(playerId: UUID, serverName: String): Boolean { return try { client.updateServer(playerId, serverName) diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerSessionQueryImpl.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerSessionQueryImpl.kt index 68a9d49..6b41011 100644 --- a/velocity/src/main/kotlin/gg/grounds/presence/PlayerSessionQueryImpl.kt +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerSessionQueryImpl.kt @@ -1,9 +1,12 @@ package gg.grounds.presence +import gg.grounds.grpc.player.CountPlayersByProxyReply import gg.grounds.grpc.player.CountPlayersByServerReply import gg.grounds.proxy.api.NetworkPlayerCounts +import gg.grounds.proxy.api.NetworkProxyCounts import gg.grounds.proxy.api.PlayerSessionInfo import gg.grounds.proxy.api.PlayerSessionQuery +import gg.grounds.proxy.api.ProxyPlayers import java.util.UUID /** @@ -28,6 +31,9 @@ class PlayerSessionQueryImpl(private val presenceService: PlayerPresenceService) override fun countPlayersByServer(): NetworkPlayerCounts? = presenceService.countPlayersByServer()?.let(::toNetworkPlayerCounts) + override fun countPlayersByProxy(): NetworkProxyCounts? = + presenceService.countPlayersByProxy()?.let(::toNetworkProxyCounts) + /** * A session with no usable id or name tells the caller nothing — drop it rather than * half-answer. @@ -41,6 +47,7 @@ class PlayerSessionQueryImpl(private val presenceService: PlayerPresenceService) proxyId = session.proxyId.takeIf { it.isNotEmpty() }, server = session.serverName.takeIf { it.isNotEmpty() }, connectedAt = session.connectedAtMillis, + region = session.region.takeIf { it.isNotEmpty() }, ) } @@ -48,6 +55,19 @@ class PlayerSessionQueryImpl(private val presenceService: PlayerPresenceService) * `servers` has one row per occupied backend server — a server nobody is on is absent, not a * zero entry. */ + /** + * `proxies` has one row per occupied proxy. An empty region string means the proxy declares + * none — mapped to null rather than kept as "", so callers have one shape for "unknown". + */ + internal fun toNetworkProxyCounts(reply: CountPlayersByProxyReply): NetworkProxyCounts = + NetworkProxyCounts( + proxies = + reply.proxiesList.map { + ProxyPlayers(it.proxyId, it.region.takeIf(String::isNotEmpty), it.players) + }, + total = reply.total, + ) + internal fun toNetworkPlayerCounts(reply: CountPlayersByServerReply): NetworkPlayerCounts = NetworkPlayerCounts( byServer = reply.serversList.associate { it.serverName to it.players },