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 },