From 69895216e5419678ff777639c9356171206e9933 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Fri, 16 Jan 2026 18:35:45 +0100 Subject: [PATCH 1/9] feat: implement player login --- build.gradle.kts | 26 +----- common/build.gradle.kts | 33 +++++++ .../player/presence/PlayerLoginResult.kt | 3 + .../player/presence/PlayerLoginStatus.kt | 9 ++ .../player/presence/PlayerLogoutResult.kt | 3 + .../player/presence/PlayerPresenceClient.kt | 11 +++ .../presence/PlayerPresenceClientConfig.kt | 23 +++++ .../presence/grpc/GrpcPlayerPresenceClient.kt | 92 +++++++++++++++++++ settings.gradle.kts | 15 ++- velocity/build.gradle.kts | 24 +---- .../java/gg/grounds/GroundsPluginPlayer.java | 32 ------- .../kotlin/gg/grounds/GroundsPluginPlayer.kt | 57 ++++++++++++ .../kotlin/gg/grounds/config/PluginConfig.kt | 31 +++++++ .../gg/grounds/config/PluginConfigLoader.kt | 67 ++++++++++++++ .../listener/PlayerConnectionListener.kt | 71 ++++++++++++++ .../grounds/presence/PlayerPresenceService.kt | 48 ++++++++++ velocity/src/main/resources/config.yml | 11 +++ 17 files changed, 479 insertions(+), 77 deletions(-) create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt delete mode 100644 velocity/src/main/java/gg/grounds/GroundsPluginPlayer.java create mode 100644 velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt create mode 100644 velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt create mode 100644 velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt create mode 100644 velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt create mode 100644 velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt create mode 100644 velocity/src/main/resources/config.yml diff --git a/build.gradle.kts b/build.gradle.kts index 18cca17..7bfb20d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,25 +1 @@ -plugins { - base - java -} - -group = "gg.grounds" -version = "1.0.0-SNAPSHOT" - -allprojects { - repositories { - maven { - url = uri("https://repo.papermc.io/repository/maven-public/") - } - mavenCentral() - } -} - -subprojects { - apply(plugin = "java") - - java { - sourceCompatibility = JavaVersion.VERSION_25 - targetCompatibility = JavaVersion.VERSION_25 - } -} +plugins { id("gg.grounds.root") version "0.1.1" } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index e69de29..cef9d21 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { id("com.google.protobuf") version "0.9.6" } + +val grpcVersion = "1.78.0" +val protobufVersion = "4.33.4" + +repositories { + maven { + url = uri("https://maven.pkg.github.com/groundsgg/*") + credentials { + username = providers.gradleProperty("github.user").get() + password = providers.gradleProperty("github.token").get() + } + } + mavenCentral() +} + +dependencies { + implementation("com.google.protobuf:protobuf-java:$protobufVersion") + implementation("io.grpc:grpc-protobuf:$grpcVersion") + implementation("io.grpc:grpc-stub:$grpcVersion") + runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") + compileOnly("javax.annotation:javax.annotation-api:1.3.2") + + protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" } + + plugins { create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" } } + + generateProtoTasks { all().forEach { task -> task.plugins { create("grpc") } } } +} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt new file mode 100644 index 0000000..6a14fb1 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt @@ -0,0 +1,3 @@ +package gg.grounds.player.presence + +data class PlayerLoginResult(val status: PlayerLoginStatus, val message: String) diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt new file mode 100644 index 0000000..0044547 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt @@ -0,0 +1,9 @@ +package gg.grounds.player.presence + +enum class PlayerLoginStatus { + UNSPECIFIED, + ACCEPTED, + ALREADY_ONLINE, + INVALID_REQUEST, + ERROR, +} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt new file mode 100644 index 0000000..20d8f17 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt @@ -0,0 +1,3 @@ +package gg.grounds.player.presence + +data class PlayerLogoutResult(val removed: Boolean, val message: String) diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt new file mode 100644 index 0000000..93d899e --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt @@ -0,0 +1,11 @@ +package gg.grounds.player.presence + +import java.util.UUID + +interface PlayerPresenceClient : AutoCloseable { + fun tryLogin(playerId: UUID): PlayerLoginResult + + fun logout(playerId: UUID): PlayerLogoutResult + + override fun close() +} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt new file mode 100644 index 0000000..19a8fa6 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt @@ -0,0 +1,23 @@ +package gg.grounds.player.presence + +import java.time.Duration + +data class PlayerPresenceClientConfig( + val target: String, + val plaintext: Boolean, + val timeout: Duration, +) { + init { + require(target.isNotBlank()) { "target must not be blank" } + require(!timeout.isZero && !timeout.isNegative) { "timeout must be > 0" } + } + + companion object { + fun defaults(): PlayerPresenceClientConfig = + PlayerPresenceClientConfig( + target = "localhost:9000", + plaintext = true, + timeout = Duration.ofSeconds(2), + ) + } +} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt new file mode 100644 index 0000000..773d4b2 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt @@ -0,0 +1,92 @@ +package gg.grounds.player.presence.grpc + +import gg.grounds.grpc.player.LoginStatus +import gg.grounds.grpc.player.PlayerLoginRequest +import gg.grounds.grpc.player.PlayerLogoutRequest +import gg.grounds.grpc.player.PlayerPresenceServiceGrpc +import gg.grounds.player.presence.PlayerLoginResult +import gg.grounds.player.presence.PlayerLoginStatus +import gg.grounds.player.presence.PlayerLogoutResult +import gg.grounds.player.presence.PlayerPresenceClient +import gg.grounds.player.presence.PlayerPresenceClientConfig +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.StatusRuntimeException +import java.util.UUID +import java.util.concurrent.TimeUnit + +class GrpcPlayerPresenceClient +private constructor( + private val channel: ManagedChannel, + private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, + private val config: PlayerPresenceClientConfig, +) : PlayerPresenceClient { + override fun tryLogin(playerId: UUID): PlayerLoginResult { + return try { + val reply = + stub + .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .tryPlayerLogin( + PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() + ) + + PlayerLoginResult(status = map(reply.status), message = reply.message) + } catch (e: StatusRuntimeException) { + PlayerLoginResult(PlayerLoginStatus.ERROR, e.status.toString()) + } catch (e: RuntimeException) { + PlayerLoginResult(PlayerLoginStatus.ERROR, e.message ?: e::class.java.name) + } + } + + override fun logout(playerId: UUID): PlayerLogoutResult { + return try { + val reply = + stub + .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .playerLogout( + PlayerLogoutRequest.newBuilder().setPlayerId(playerId.toString()).build() + ) + PlayerLogoutResult(removed = reply.removed, message = reply.message) + } catch (e: StatusRuntimeException) { + PlayerLogoutResult(false, e.status.toString()) + } catch (e: RuntimeException) { + PlayerLogoutResult(false, e.message ?: e::class.java.name) + } + } + + override fun close() { + channel.shutdown() + try { + if (!channel.awaitTermination(3, TimeUnit.SECONDS)) { + channel.shutdownNow() + channel.awaitTermination(3, TimeUnit.SECONDS) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + channel.shutdownNow() + } + } + + companion object { + fun create(config: PlayerPresenceClientConfig): GrpcPlayerPresenceClient { + val channelBuilder = ManagedChannelBuilder.forTarget(config.target) + if (config.plaintext) { + channelBuilder.usePlaintext() + } + val channel = channelBuilder.build() + val stub = PlayerPresenceServiceGrpc.newBlockingStub(channel) + return GrpcPlayerPresenceClient(channel, stub, config) + } + + private fun map(status: LoginStatus): PlayerLoginStatus { + return when (status) { + LoginStatus.LOGIN_STATUS_ACCEPTED -> PlayerLoginStatus.ACCEPTED + LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> PlayerLoginStatus.ALREADY_ONLINE + LoginStatus.LOGIN_STATUS_INVALID_REQUEST -> PlayerLoginStatus.INVALID_REQUEST + LoginStatus.LOGIN_STATUS_ERROR -> PlayerLoginStatus.ERROR + LoginStatus.LOGIN_STATUS_UNSPECIFIED, + LoginStatus.UNRECOGNIZED -> PlayerLoginStatus.UNSPECIFIED + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e1fd2c3..7a9c346 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,16 @@ -rootProject.name = "grounds-plugin-player" +rootProject.name = "plugin-player" include("common", "velocity") + +pluginManagement { + repositories { + maven { + url = uri("https://maven.pkg.github.com/groundsgg/*") + credentials { + username = providers.gradleProperty("github.user").get() + password = providers.gradleProperty("github.token").get() + } + } + gradlePluginPortal() + } +} diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index f51b3c1..355ee6b 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,23 +1,9 @@ -plugins { - id("com.gradleup.shadow") version "9.3.0" -} +plugins { id("gg.grounds.velocity") version "0.1.1" } + +repositories { mavenCentral() } dependencies { implementation(project(":common")) - compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") - annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") -} - -tasks.build { - dependsOn(tasks.shadowJar) -} - -tasks.jar { - enabled = false -} - -tasks.shadowJar { - archiveBaseName.set(rootProject.name) - archiveClassifier.set("") // Removes the 'all' classifier - archiveVersion.set("") // Removes the version from the jar name + implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.0.3") + implementation("tools.jackson.module:jackson-module-kotlin:3.0.3") } diff --git a/velocity/src/main/java/gg/grounds/GroundsPluginPlayer.java b/velocity/src/main/java/gg/grounds/GroundsPluginPlayer.java deleted file mode 100644 index abc4e1e..0000000 --- a/velocity/src/main/java/gg/grounds/GroundsPluginPlayer.java +++ /dev/null @@ -1,32 +0,0 @@ -package gg.grounds; - -import com.google.inject.Inject; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.plugin.Plugin; -import org.slf4j.Logger; - -@Plugin(id = "grounds-plugin-player", name = "Grounds Player Plugin") -public final class GroundsPluginPlayer { - private final Logger logger; - - @Inject - public GroundsPluginPlayer(Logger logger) { - this.logger = logger; - - this.logger.info("VelocityPlayerPlugin initialized"); - } - - @Subscribe - public void onPostLogin(PostLoginEvent event) { - String name = event.getPlayer().getUsername(); - logger.info("hello {}", name); - } - - @Subscribe - public void onDisconnect(DisconnectEvent event) { - String name = event.getPlayer().getUsername(); - logger.info("bye {}", name); - } -} diff --git a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt new file mode 100644 index 0000000..a29c304 --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt @@ -0,0 +1,57 @@ +package gg.grounds + +import com.google.inject.Inject +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent +import com.velocitypowered.api.plugin.Plugin +import com.velocitypowered.api.plugin.annotation.DataDirectory +import com.velocitypowered.api.proxy.ProxyServer +import gg.grounds.config.PluginConfigLoader +import gg.grounds.listener.PlayerConnectionListener +import gg.grounds.presence.PlayerPresenceService +import java.nio.file.Path +import org.slf4j.Logger + +@Plugin(id = "grounds-plugin-player", name = "Grounds Player Plugin") +class GroundsPluginPlayer +@Inject +constructor( + private val proxy: ProxyServer, + private val logger: Logger, + @param:DataDirectory private val dataDirectory: Path, +) { + private val playerPresenceService = PlayerPresenceService(logger) + + init { + logger.info("VelocityPlayerPlugin initialized") + } + + @Subscribe + fun onInitialize(@Suppress("UNUSED_PARAMETER") event: ProxyInitializeEvent) { + val config = PluginConfigLoader(logger, dataDirectory).loadOrCreate() + val clientConfig = config.playerPresence.toClientConfig() + playerPresenceService.configure(clientConfig) + + proxy.eventManager.register( + this, + PlayerConnectionListener( + logger = logger, + playerPresenceService = playerPresenceService, + messages = config.messages, + ), + ) + + logger.info( + "PlayerPresence gRPC configured (target={}, plaintext={}, timeoutMs={})", + clientConfig.target, + clientConfig.plaintext, + clientConfig.timeout.toMillis(), + ) + } + + @Subscribe + fun onShutdown(@Suppress("UNUSED_PARAMETER") event: ProxyShutdownEvent) { + playerPresenceService.close() + } +} diff --git a/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt b/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt new file mode 100644 index 0000000..22dc6bc --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt @@ -0,0 +1,31 @@ +package gg.grounds.config + +import gg.grounds.player.presence.PlayerPresenceClientConfig +import java.time.Duration + +data class PluginConfig( + val playerPresence: PlayerPresence = PlayerPresence(), + val messages: Messages = Messages(), +) { + data class PlayerPresence( + val target: String = PlayerPresenceClientConfig.defaults().target, + val plaintext: Boolean = PlayerPresenceClientConfig.defaults().plaintext, + val timeoutMillis: Long = PlayerPresenceClientConfig.defaults().timeout.toMillis(), + ) { + fun toClientConfig(): PlayerPresenceClientConfig { + val millis = maxOf(1L, timeoutMillis) + return PlayerPresenceClientConfig( + target = target, + plaintext = plaintext, + timeout = Duration.ofMillis(millis), + ) + } + } + + data class Messages( + val serviceUnavailable: String = "Login service unavailable", + val alreadyOnline: String = "You are already online.", + val invalidRequest: String = "Invalid login request.", + val genericError: String = "Unable to create player session.", + ) +} diff --git a/velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt b/velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt new file mode 100644 index 0000000..d53eda3 --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt @@ -0,0 +1,67 @@ +package gg.grounds.config + +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path +import org.slf4j.Logger +import tools.jackson.databind.DeserializationFeature +import tools.jackson.dataformat.yaml.YAMLMapper +import tools.jackson.module.kotlin.KotlinModule +import tools.jackson.module.kotlin.readValue + +class PluginConfigLoader(private val logger: Logger, private val dataDirectory: Path) { + private val mapper: YAMLMapper = + YAMLMapper.builder() + .addModule(KotlinModule.Builder().build()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build() + + fun loadOrCreate(): PluginConfig { + val yamlFile = dataDirectory.resolve("config.yml") + + return try { + Files.createDirectories(dataDirectory) + + if (Files.notExists(yamlFile)) { + copyDefaultConfig(yamlFile) + } + + Files.newInputStream(yamlFile).use { input -> parseYaml(input) } + } catch (e: Exception) { + logger.warn("Unable to load config from {}", yamlFile, e) + PluginConfig() + } + } + + private fun parseYaml(input: InputStream): PluginConfig { + return try { + mapper.readValue(input) + } catch (e: Exception) { + logger.warn("Unable to parse config.yml; using defaults", e) + PluginConfig() + } + } + + private fun copyDefaultConfig(target: Path) { + val resourceName = "config.yml" + val inputStream = javaClass.classLoader.getResourceAsStream(resourceName) + if (inputStream == null) { + logger.warn( + "Default config resource {} not found; writing generated defaults", + resourceName, + ) + writeYaml(target, PluginConfig()) + return + } + + inputStream.use { input -> + Files.newOutputStream(target).use { output -> input.copyTo(output) } + } + } + + private fun writeYaml(target: Path, config: PluginConfig) { + Files.newOutputStream(target).use { output -> + output.writer(Charsets.UTF_8).use { writer -> mapper.writeValue(writer, config) } + } + } +} diff --git a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt new file mode 100644 index 0000000..f14876c --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt @@ -0,0 +1,71 @@ +package gg.grounds.listener + +import com.velocitypowered.api.event.EventTask +import com.velocitypowered.api.event.ResultedEvent.ComponentResult +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.event.connection.DisconnectEvent +import com.velocitypowered.api.event.connection.LoginEvent +import gg.grounds.config.PluginConfig +import gg.grounds.player.presence.PlayerLoginStatus +import gg.grounds.presence.PlayerPresenceService +import net.kyori.adventure.text.Component +import org.slf4j.Logger + +class PlayerConnectionListener( + private val logger: Logger, + private val playerPresenceService: PlayerPresenceService, + private val messages: PluginConfig.Messages, +) { + @Subscribe + fun onLogin(event: LoginEvent): EventTask { + val playerId = event.player.uniqueId + val name = event.player.username + + return EventTask.async { + val result = playerPresenceService.tryLogin(playerId) + if (result == null) { + event.result = ComponentResult.denied(Component.text(messages.serviceUnavailable)) + return@async + } + + val kickMessage = + when (result.status) { + PlayerLoginStatus.ACCEPTED -> { + logger.info("player session created: {} ({})", name, playerId) + return@async + } + PlayerLoginStatus.ALREADY_ONLINE -> messages.alreadyOnline + PlayerLoginStatus.INVALID_REQUEST -> messages.invalidRequest + PlayerLoginStatus.UNSPECIFIED, + PlayerLoginStatus.ERROR -> messages.genericError + } + + logger.warn( + "player session rejected: {} ({}) status={} message={}", + name, + playerId, + result.status, + result.message, + ) + + event.result = ComponentResult.denied(Component.text(kickMessage)) + } + } + + @Subscribe + fun onDisconnect(event: DisconnectEvent): EventTask { + val playerId = event.player.uniqueId + val name = event.player.username + + return EventTask.async { + val result = playerPresenceService.logout(playerId) ?: return@async + logger.info( + "player session logout: {} ({}) removed={} message={}", + name, + playerId, + result.removed, + result.message, + ) + } + } +} diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt new file mode 100644 index 0000000..6d1c6be --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt @@ -0,0 +1,48 @@ +package gg.grounds.presence + +import gg.grounds.player.presence.PlayerLoginResult +import gg.grounds.player.presence.PlayerLogoutResult +import gg.grounds.player.presence.PlayerPresenceClient +import gg.grounds.player.presence.PlayerPresenceClientConfig +import gg.grounds.player.presence.grpc.GrpcPlayerPresenceClient +import java.util.UUID +import org.slf4j.Logger + +class PlayerPresenceService(private val logger: Logger) : AutoCloseable { + @Volatile private var client: PlayerPresenceClient? = null + + fun configure(config: PlayerPresenceClientConfig) { + close() + client = GrpcPlayerPresenceClient.create(config) + } + + fun tryLogin(playerId: UUID): PlayerLoginResult? { + val current = client ?: return null + return try { + current.tryLogin(playerId) + } catch (e: RuntimeException) { + logger.warn("player presence tryLogin failed: {}", playerId, e) + null + } + } + + fun logout(playerId: UUID): PlayerLogoutResult? { + val current = client ?: return null + return try { + current.logout(playerId) + } catch (e: RuntimeException) { + logger.warn("player presence logout failed: {}", playerId, e) + null + } + } + + override fun close() { + val current = client + client = null + try { + current?.close() + } catch (e: RuntimeException) { + logger.warn("player presence client close failed", e) + } + } +} diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml new file mode 100644 index 0000000..2e2fbd8 --- /dev/null +++ b/velocity/src/main/resources/config.yml @@ -0,0 +1,11 @@ +playerPresence: + target: "localhost:9000" + plaintext: true + timeoutMillis: 2000 + +messages: + serviceUnavailable: "Login service unavailable" + alreadyOnline: "You are already online." + invalidRequest: "Invalid login request." + genericError: "Unable to create player session." + From 1a2969722f18d21c5d335b80da1920a5de60961d Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 18 Jan 2026 19:43:42 +0100 Subject: [PATCH 2/9] chore: update gradle plugin to conventions plugin --- build.gradle.kts | 1 - common/build.gradle.kts | 6 ++++-- velocity/build.gradle.kts | 7 ++++++- .../META-INF/services/io.grpc.NameResolverProvider | 2 ++ velocity/src/main/resources/config.yml | 3 +-- 5 files changed, 13 insertions(+), 6 deletions(-) delete mode 100644 build.gradle.kts create mode 100644 velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 7bfb20d..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -plugins { id("gg.grounds.root") version "0.1.1" } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index cef9d21..18cf14e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,4 +1,7 @@ -plugins { id("com.google.protobuf") version "0.9.6" } +plugins { + id("gg.grounds.kotlin-conventions") version "0.2.0" + id("com.google.protobuf") version "0.9.6" +} val grpcVersion = "1.78.0" val protobufVersion = "4.33.4" @@ -18,7 +21,6 @@ dependencies { implementation("com.google.protobuf:protobuf-java:$protobufVersion") implementation("io.grpc:grpc-protobuf:$grpcVersion") implementation("io.grpc:grpc-stub:$grpcVersion") - runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") compileOnly("javax.annotation:javax.annotation-api:1.3.2") protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 355ee6b..889082e 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,4 +1,6 @@ -plugins { id("gg.grounds.velocity") version "0.1.1" } +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { id("gg.grounds.velocity-conventions") version "0.2.0" } repositories { mavenCentral() } @@ -6,4 +8,7 @@ dependencies { implementation(project(":common")) implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.0.3") implementation("tools.jackson.module:jackson-module-kotlin:3.0.3") + runtimeOnly("io.grpc:grpc-netty-shaded:1.78.0") } + +tasks.withType().configureEach { mergeServiceFiles() } diff --git a/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider new file mode 100644 index 0000000..40e4c47 --- /dev/null +++ b/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider @@ -0,0 +1,2 @@ +io.grpc.internal.DnsNameResolverProvider +io.grpc.netty.shaded.io.grpc.netty.UdsNameResolverProvider diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index 2e2fbd8..50c5612 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -1,5 +1,5 @@ playerPresence: - target: "localhost:9000" + target: "dns:///service-player.api:9000" plaintext: true timeoutMillis: 2000 @@ -8,4 +8,3 @@ messages: alreadyOnline: "You are already online." invalidRequest: "Invalid login request." genericError: "Unable to create player session." - From 91ede5f9c69325fd1f59cafa83f442c1cd65fda5 Mon Sep 17 00:00:00 2001 From: ItsKev Date: Sun, 18 Jan 2026 21:46:48 +0100 Subject: [PATCH 3/9] refactor: remove lots of duplicated code which is already present from player.proto --- build.gradle.kts | 1 + common/build.gradle.kts | 5 +- .../presence/GrpcPlayerPresenceClient.kt | 82 +++++++++++++++++ .../player/presence/PlayerLoginResult.kt | 3 - .../player/presence/PlayerLoginStatus.kt | 9 -- .../player/presence/PlayerLogoutResult.kt | 3 - .../player/presence/PlayerPresenceClient.kt | 11 --- .../presence/grpc/GrpcPlayerPresenceClient.kt | 92 ------------------- velocity/build.gradle.kts | 4 +- velocity/devspace.yaml | 26 +++--- .../listener/PlayerConnectionListener.kt | 13 +-- .../grounds/presence/PlayerPresenceService.kt | 29 +++--- .../services/io.grpc.LoadBalancerProvider | 4 + 13 files changed, 120 insertions(+), 162 deletions(-) create mode 100644 build.gradle.kts create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt create mode 100644 velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..fa7030f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1 @@ +plugins { id("gg.grounds.base-conventions") version "0.2.0" } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 18cf14e..58210c7 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("gg.grounds.kotlin-conventions") version "0.2.0" + id("gg.grounds.kotlin-conventions") id("com.google.protobuf") version "0.9.6" } @@ -14,11 +14,10 @@ repositories { password = providers.gradleProperty("github.token").get() } } - mavenCentral() } dependencies { - implementation("com.google.protobuf:protobuf-java:$protobufVersion") + api("com.google.protobuf:protobuf-java:$protobufVersion") implementation("io.grpc:grpc-protobuf:$grpcVersion") implementation("io.grpc:grpc-stub:$grpcVersion") compileOnly("javax.annotation:javax.annotation-api:1.3.2") diff --git a/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt new file mode 100644 index 0000000..9ff0cfd --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt @@ -0,0 +1,82 @@ +package gg.grounds.player.presence + +import gg.grounds.grpc.player.LoginStatus +import gg.grounds.grpc.player.PlayerLoginReply +import gg.grounds.grpc.player.PlayerLoginRequest +import gg.grounds.grpc.player.PlayerLogoutReply +import gg.grounds.grpc.player.PlayerLogoutRequest +import gg.grounds.grpc.player.PlayerPresenceServiceGrpc +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.StatusRuntimeException +import java.util.UUID +import java.util.concurrent.TimeUnit + +class GrpcPlayerPresenceClient +private constructor( + private val channel: ManagedChannel, + private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, + private val config: PlayerPresenceClientConfig, +) : AutoCloseable { + fun tryLogin(playerId: UUID): PlayerLoginReply { + return try { + stub + .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .tryPlayerLogin( + PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() + ) + } catch (e: StatusRuntimeException) { + errorReply(e.status.toString()) + } catch (e: RuntimeException) { + errorReply(e.message ?: e::class.java.name) + } + } + + fun logout(playerId: UUID): PlayerLogoutReply { + return try { + stub + .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .playerLogout( + PlayerLogoutRequest.newBuilder().setPlayerId(playerId.toString()).build() + ) + } catch (e: StatusRuntimeException) { + errorLogoutReply(e.status.toString()) + } catch (e: RuntimeException) { + errorLogoutReply(e.message ?: e::class.java.name) + } + } + + override fun close() { + channel.shutdown() + try { + if (!channel.awaitTermination(3, TimeUnit.SECONDS)) { + channel.shutdownNow() + channel.awaitTermination(3, TimeUnit.SECONDS) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + channel.shutdownNow() + } + } + + companion object { + fun create(config: PlayerPresenceClientConfig): GrpcPlayerPresenceClient { + val channelBuilder = ManagedChannelBuilder.forTarget(config.target) + if (config.plaintext) { + channelBuilder.usePlaintext() + } + val channel = channelBuilder.build() + val stub = PlayerPresenceServiceGrpc.newBlockingStub(channel) + return GrpcPlayerPresenceClient(channel, stub, config) + } + + private fun errorReply(message: String): PlayerLoginReply = + PlayerLoginReply.newBuilder() + .setStatus(LoginStatus.LOGIN_STATUS_ERROR) + .setMessage(message) + .build() + + private fun errorLogoutReply(message: String): PlayerLogoutReply = + PlayerLogoutReply.newBuilder().setRemoved(false).setMessage(message).build() + } +} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt deleted file mode 100644 index 6a14fb1..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt +++ /dev/null @@ -1,3 +0,0 @@ -package gg.grounds.player.presence - -data class PlayerLoginResult(val status: PlayerLoginStatus, val message: String) diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt deleted file mode 100644 index 0044547..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginStatus.kt +++ /dev/null @@ -1,9 +0,0 @@ -package gg.grounds.player.presence - -enum class PlayerLoginStatus { - UNSPECIFIED, - ACCEPTED, - ALREADY_ONLINE, - INVALID_REQUEST, - ERROR, -} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt deleted file mode 100644 index 20d8f17..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLogoutResult.kt +++ /dev/null @@ -1,3 +0,0 @@ -package gg.grounds.player.presence - -data class PlayerLogoutResult(val removed: Boolean, val message: String) diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt deleted file mode 100644 index 93d899e..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClient.kt +++ /dev/null @@ -1,11 +0,0 @@ -package gg.grounds.player.presence - -import java.util.UUID - -interface PlayerPresenceClient : AutoCloseable { - fun tryLogin(playerId: UUID): PlayerLoginResult - - fun logout(playerId: UUID): PlayerLogoutResult - - override fun close() -} diff --git a/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt b/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt deleted file mode 100644 index 773d4b2..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/grpc/GrpcPlayerPresenceClient.kt +++ /dev/null @@ -1,92 +0,0 @@ -package gg.grounds.player.presence.grpc - -import gg.grounds.grpc.player.LoginStatus -import gg.grounds.grpc.player.PlayerLoginRequest -import gg.grounds.grpc.player.PlayerLogoutRequest -import gg.grounds.grpc.player.PlayerPresenceServiceGrpc -import gg.grounds.player.presence.PlayerLoginResult -import gg.grounds.player.presence.PlayerLoginStatus -import gg.grounds.player.presence.PlayerLogoutResult -import gg.grounds.player.presence.PlayerPresenceClient -import gg.grounds.player.presence.PlayerPresenceClientConfig -import io.grpc.ManagedChannel -import io.grpc.ManagedChannelBuilder -import io.grpc.StatusRuntimeException -import java.util.UUID -import java.util.concurrent.TimeUnit - -class GrpcPlayerPresenceClient -private constructor( - private val channel: ManagedChannel, - private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, - private val config: PlayerPresenceClientConfig, -) : PlayerPresenceClient { - override fun tryLogin(playerId: UUID): PlayerLoginResult { - return try { - val reply = - stub - .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) - .tryPlayerLogin( - PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() - ) - - PlayerLoginResult(status = map(reply.status), message = reply.message) - } catch (e: StatusRuntimeException) { - PlayerLoginResult(PlayerLoginStatus.ERROR, e.status.toString()) - } catch (e: RuntimeException) { - PlayerLoginResult(PlayerLoginStatus.ERROR, e.message ?: e::class.java.name) - } - } - - override fun logout(playerId: UUID): PlayerLogoutResult { - return try { - val reply = - stub - .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) - .playerLogout( - PlayerLogoutRequest.newBuilder().setPlayerId(playerId.toString()).build() - ) - PlayerLogoutResult(removed = reply.removed, message = reply.message) - } catch (e: StatusRuntimeException) { - PlayerLogoutResult(false, e.status.toString()) - } catch (e: RuntimeException) { - PlayerLogoutResult(false, e.message ?: e::class.java.name) - } - } - - override fun close() { - channel.shutdown() - try { - if (!channel.awaitTermination(3, TimeUnit.SECONDS)) { - channel.shutdownNow() - channel.awaitTermination(3, TimeUnit.SECONDS) - } - } catch (e: InterruptedException) { - Thread.currentThread().interrupt() - channel.shutdownNow() - } - } - - companion object { - fun create(config: PlayerPresenceClientConfig): GrpcPlayerPresenceClient { - val channelBuilder = ManagedChannelBuilder.forTarget(config.target) - if (config.plaintext) { - channelBuilder.usePlaintext() - } - val channel = channelBuilder.build() - val stub = PlayerPresenceServiceGrpc.newBlockingStub(channel) - return GrpcPlayerPresenceClient(channel, stub, config) - } - - private fun map(status: LoginStatus): PlayerLoginStatus { - return when (status) { - LoginStatus.LOGIN_STATUS_ACCEPTED -> PlayerLoginStatus.ACCEPTED - LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> PlayerLoginStatus.ALREADY_ONLINE - LoginStatus.LOGIN_STATUS_INVALID_REQUEST -> PlayerLoginStatus.INVALID_REQUEST - LoginStatus.LOGIN_STATUS_ERROR -> PlayerLoginStatus.ERROR - LoginStatus.LOGIN_STATUS_UNSPECIFIED, - LoginStatus.UNRECOGNIZED -> PlayerLoginStatus.UNSPECIFIED - } - } - } -} diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 889082e..21cbbee 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,8 +1,6 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -plugins { id("gg.grounds.velocity-conventions") version "0.2.0" } - -repositories { mavenCentral() } +plugins { id("gg.grounds.velocity-conventions") } dependencies { implementation(project(":common")) diff --git a/velocity/devspace.yaml b/velocity/devspace.yaml index 716f0fa..4c4386b 100644 --- a/velocity/devspace.yaml +++ b/velocity/devspace.yaml @@ -1,35 +1,33 @@ version: v2beta1 -name: velocity-player +name: plugin-agones-velocity deployments: app: namespace: games helm: - releaseName: velocity-player + releaseName: plugin-agones-velocity chart: - name: oci://ghcr.io/groundsgg/charts/grounds-service + name: oci://ghcr.io/groundsgg/charts/agones-fleet version: 0.1.0 values: - deployment: - image: - repository: velocity - containerPort: 25565 - service: - enabled: true - port: 25565 - targetPort: 25565 + image: + repository: ghcr.io/groundsgg/velocity + tag: 0.6.0 + serverType: velocity + command: ["/bin/sh", "-c", "sleep infinity"] dev: app: # Search for the container that runs this image - imageSelector: velocity - # Replace the container image with this image - devImage: ghcr.io/groundsgg/velocity:v0.2.0 + imageSelector: ghcr.io/groundsgg/velocity + # Synchronize the plugin jar file to the plugins directory sync: - path: ./build/libs/*.jar:/app/plugins/*.jar + initialSync: preferLocal file: true # Open a terminal and use the following command to start it terminal: + disableReplace: true command: | echo "Use ./start.sh to start the server" /bin/sh diff --git a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt index f14876c..8dafca9 100644 --- a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt +++ b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt @@ -6,7 +6,7 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.connection.DisconnectEvent import com.velocitypowered.api.event.connection.LoginEvent import gg.grounds.config.PluginConfig -import gg.grounds.player.presence.PlayerLoginStatus +import gg.grounds.grpc.player.LoginStatus import gg.grounds.presence.PlayerPresenceService import net.kyori.adventure.text.Component import org.slf4j.Logger @@ -30,14 +30,15 @@ class PlayerConnectionListener( val kickMessage = when (result.status) { - PlayerLoginStatus.ACCEPTED -> { + LoginStatus.LOGIN_STATUS_ACCEPTED -> { logger.info("player session created: {} ({})", name, playerId) return@async } - PlayerLoginStatus.ALREADY_ONLINE -> messages.alreadyOnline - PlayerLoginStatus.INVALID_REQUEST -> messages.invalidRequest - PlayerLoginStatus.UNSPECIFIED, - PlayerLoginStatus.ERROR -> messages.genericError + LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> messages.alreadyOnline + LoginStatus.LOGIN_STATUS_INVALID_REQUEST -> messages.invalidRequest + LoginStatus.LOGIN_STATUS_UNSPECIFIED, + LoginStatus.LOGIN_STATUS_ERROR, + LoginStatus.UNRECOGNIZED -> messages.genericError } logger.warn( diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt index 6d1c6be..c055b0c 100644 --- a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt @@ -1,35 +1,32 @@ package gg.grounds.presence -import gg.grounds.player.presence.PlayerLoginResult -import gg.grounds.player.presence.PlayerLogoutResult -import gg.grounds.player.presence.PlayerPresenceClient +import gg.grounds.grpc.player.PlayerLoginReply +import gg.grounds.grpc.player.PlayerLogoutReply +import gg.grounds.player.presence.GrpcPlayerPresenceClient import gg.grounds.player.presence.PlayerPresenceClientConfig -import gg.grounds.player.presence.grpc.GrpcPlayerPresenceClient -import java.util.UUID +import java.util.* import org.slf4j.Logger class PlayerPresenceService(private val logger: Logger) : AutoCloseable { - @Volatile private var client: PlayerPresenceClient? = null + private lateinit var client: GrpcPlayerPresenceClient fun configure(config: PlayerPresenceClientConfig) { close() client = GrpcPlayerPresenceClient.create(config) } - fun tryLogin(playerId: UUID): PlayerLoginResult? { - val current = client ?: return null + fun tryLogin(playerId: UUID): PlayerLoginReply? { return try { - current.tryLogin(playerId) + client.tryLogin(playerId) } catch (e: RuntimeException) { logger.warn("player presence tryLogin failed: {}", playerId, e) null } } - fun logout(playerId: UUID): PlayerLogoutResult? { - val current = client ?: return null + fun logout(playerId: UUID): PlayerLogoutReply? { return try { - current.logout(playerId) + client.logout(playerId) } catch (e: RuntimeException) { logger.warn("player presence logout failed: {}", playerId, e) null @@ -37,12 +34,8 @@ class PlayerPresenceService(private val logger: Logger) : AutoCloseable { } override fun close() { - val current = client - client = null - try { - current?.close() - } catch (e: RuntimeException) { - logger.warn("player presence client close failed", e) + if (this::client.isInitialized) { + client.close() } } } diff --git a/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider new file mode 100644 index 0000000..0970cdd --- /dev/null +++ b/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -0,0 +1,4 @@ +io.grpc.internal.PickFirstLoadBalancerProvider +io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider +io.grpc.util.OutlierDetectionLoadBalancerProvider +io.grpc.util.RandomSubsettingLoadBalancerProvider From c87927154747e0eb6555d96dfe5906e0efbe2c19 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Sun, 18 Jan 2026 22:04:24 +0100 Subject: [PATCH 4/9] fix: align plugin name to naming conventions --- velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt index a29c304..391f4d3 100644 --- a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt +++ b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt @@ -13,7 +13,7 @@ import gg.grounds.presence.PlayerPresenceService import java.nio.file.Path import org.slf4j.Logger -@Plugin(id = "grounds-plugin-player", name = "Grounds Player Plugin") +@Plugin(id = "plugin-player", name = "Grounds Player Plugin") class GroundsPluginPlayer @Inject constructor( From 9e7a0dfde3e3e7cd763a8ef0fb8dd009ede9cbdd Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Mon, 19 Jan 2026 10:28:27 +0100 Subject: [PATCH 5/9] fix: Register missing providers manually --- velocity/build.gradle.kts | 6 +--- .../kotlin/gg/grounds/GroundsPluginPlayer.kt | 30 +++++++++++++++++-- .../services/io.grpc.LoadBalancerProvider | 4 --- .../services/io.grpc.NameResolverProvider | 2 -- 4 files changed, 28 insertions(+), 14 deletions(-) delete mode 100644 velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider delete mode 100644 velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 21cbbee..e835e17 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,12 +1,8 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - plugins { id("gg.grounds.velocity-conventions") } dependencies { implementation(project(":common")) implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.0.3") implementation("tools.jackson.module:jackson-module-kotlin:3.0.3") - runtimeOnly("io.grpc:grpc-netty-shaded:1.78.0") + implementation("io.grpc:grpc-netty-shaded:1.78.0") } - -tasks.withType().configureEach { mergeServiceFiles() } diff --git a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt index 391f4d3..58bd632 100644 --- a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt +++ b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt @@ -10,10 +10,21 @@ import com.velocitypowered.api.proxy.ProxyServer import gg.grounds.config.PluginConfigLoader import gg.grounds.listener.PlayerConnectionListener import gg.grounds.presence.PlayerPresenceService +import io.grpc.LoadBalancerRegistry +import io.grpc.NameResolverRegistry +import io.grpc.internal.DnsNameResolverProvider +import io.grpc.internal.PickFirstLoadBalancerProvider import java.nio.file.Path import org.slf4j.Logger -@Plugin(id = "plugin-player", name = "Grounds Player Plugin") +@Plugin( + id = "plugin-player", + name = "Grounds Player Plugin", + version = BuildInfo.VERSION, + description = "", + authors = ["Grounds Development Team and contributors"], + url = "https://github.com/groundsgg/plugin-player", +) class GroundsPluginPlayer @Inject constructor( @@ -28,7 +39,9 @@ constructor( } @Subscribe - fun onInitialize(@Suppress("UNUSED_PARAMETER") event: ProxyInitializeEvent) { + fun onInitialize(event: ProxyInitializeEvent) { + registerProviders() + val config = PluginConfigLoader(logger, dataDirectory).loadOrCreate() val clientConfig = config.playerPresence.toClientConfig() playerPresenceService.configure(clientConfig) @@ -51,7 +64,18 @@ constructor( } @Subscribe - fun onShutdown(@Suppress("UNUSED_PARAMETER") event: ProxyShutdownEvent) { + fun onShutdown(event: ProxyShutdownEvent) { playerPresenceService.close() } + + /** + * Registers gRPC name resolver and load balancer providers so client channels can resolve DNS + * targets and select endpoints when running inside Velocity's shaded environment. This manual + * step avoids startup IllegalArgumentExceptions caused by shaded classes not being discoverable + * via the default provider lookup. + */ + private fun registerProviders() { + NameResolverRegistry.getDefaultRegistry().register(DnsNameResolverProvider()) + LoadBalancerRegistry.getDefaultRegistry().register(PickFirstLoadBalancerProvider()) + } } diff --git a/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider deleted file mode 100644 index 0970cdd..0000000 --- a/velocity/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ /dev/null @@ -1,4 +0,0 @@ -io.grpc.internal.PickFirstLoadBalancerProvider -io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider -io.grpc.util.OutlierDetectionLoadBalancerProvider -io.grpc.util.RandomSubsettingLoadBalancerProvider diff --git a/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider deleted file mode 100644 index 40e4c47..0000000 --- a/velocity/src/main/resources/META-INF/services/io.grpc.NameResolverProvider +++ /dev/null @@ -1,2 +0,0 @@ -io.grpc.internal.DnsNameResolverProvider -io.grpc.netty.shaded.io.grpc.netty.UdsNameResolverProvider From e7841e2a5cf44acef88549c37ec58855eef4e921 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Mon, 19 Jan 2026 11:45:37 +0100 Subject: [PATCH 6/9] fix: use full kubernetes url --- velocity/src/main/resources/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index 50c5612..be77e84 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -1,5 +1,5 @@ playerPresence: - target: "dns:///service-player.api:9000" + target: "dns:///service-player.api.svc.cluster.local:9000" plaintext: true timeoutMillis: 2000 From 5ff5a71746c48599bc410dea6da67a3d76740dcf Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Mon, 19 Jan 2026 13:34:43 +0100 Subject: [PATCH 7/9] fix: improve error handling --- common/build.gradle.kts | 1 - .../presence/GrpcPlayerPresenceClient.kt | 33 +++---- .../player/presence/PlayerLoginResult.kt | 11 +++ .../listener/PlayerConnectionListener.kt | 96 +++++++++++++------ .../grounds/presence/PlayerPresenceService.kt | 6 +- 5 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 58210c7..c6bdc70 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { api("com.google.protobuf:protobuf-java:$protobufVersion") implementation("io.grpc:grpc-protobuf:$grpcVersion") implementation("io.grpc:grpc-stub:$grpcVersion") - compileOnly("javax.annotation:javax.annotation-api:1.3.2") protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") } 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 9ff0cfd..5e17234 100644 --- a/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt +++ b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt @@ -1,13 +1,12 @@ package gg.grounds.player.presence -import gg.grounds.grpc.player.LoginStatus -import gg.grounds.grpc.player.PlayerLoginReply import gg.grounds.grpc.player.PlayerLoginRequest import gg.grounds.grpc.player.PlayerLogoutReply import gg.grounds.grpc.player.PlayerLogoutRequest import gg.grounds.grpc.player.PlayerPresenceServiceGrpc import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder +import io.grpc.Status import io.grpc.StatusRuntimeException import java.util.UUID import java.util.concurrent.TimeUnit @@ -18,17 +17,22 @@ private constructor( private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, private val config: PlayerPresenceClientConfig, ) : AutoCloseable { - fun tryLogin(playerId: UUID): PlayerLoginReply { + fun tryLogin(playerId: UUID): PlayerLoginResult { return try { - stub - .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) - .tryPlayerLogin( - PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() - ) + val reply = + stub + .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .tryPlayerLogin( + PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() + ) + PlayerLoginResult.Success(reply) } catch (e: StatusRuntimeException) { - errorReply(e.status.toString()) + if (isServiceUnavailable(e.status.code)) { + return PlayerLoginResult.Unavailable(e.status.toString()) + } + PlayerLoginResult.Error(e.status.toString()) } catch (e: RuntimeException) { - errorReply(e.message ?: e::class.java.name) + PlayerLoginResult.Error(e.message ?: e::class.java.name) } } @@ -70,13 +74,10 @@ private constructor( return GrpcPlayerPresenceClient(channel, stub, config) } - private fun errorReply(message: String): PlayerLoginReply = - PlayerLoginReply.newBuilder() - .setStatus(LoginStatus.LOGIN_STATUS_ERROR) - .setMessage(message) - .build() - private fun errorLogoutReply(message: String): PlayerLogoutReply = PlayerLogoutReply.newBuilder().setRemoved(false).setMessage(message).build() + + private fun isServiceUnavailable(status: Status.Code): Boolean = + status == Status.Code.UNAVAILABLE || status == Status.Code.DEADLINE_EXCEEDED } } diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt new file mode 100644 index 0000000..e08adc3 --- /dev/null +++ b/common/src/main/kotlin/gg/grounds/player/presence/PlayerLoginResult.kt @@ -0,0 +1,11 @@ +package gg.grounds.player.presence + +import gg.grounds.grpc.player.PlayerLoginReply + +sealed class PlayerLoginResult { + data class Success(val reply: PlayerLoginReply) : PlayerLoginResult() + + data class Unavailable(val message: String) : PlayerLoginResult() + + data class Error(val message: String) : PlayerLoginResult() +} diff --git a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt index 8dafca9..dd29ab9 100644 --- a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt +++ b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt @@ -1,13 +1,15 @@ package gg.grounds.listener import com.velocitypowered.api.event.EventTask -import com.velocitypowered.api.event.ResultedEvent.ComponentResult import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.connection.DisconnectEvent -import com.velocitypowered.api.event.connection.LoginEvent +import com.velocitypowered.api.event.connection.PreLoginEvent import gg.grounds.config.PluginConfig import gg.grounds.grpc.player.LoginStatus +import gg.grounds.grpc.player.PlayerLoginReply +import gg.grounds.player.presence.PlayerLoginResult import gg.grounds.presence.PlayerPresenceService +import java.util.UUID import net.kyori.adventure.text.Component import org.slf4j.Logger @@ -17,39 +19,38 @@ class PlayerConnectionListener( private val messages: PluginConfig.Messages, ) { @Subscribe - fun onLogin(event: LoginEvent): EventTask { - val playerId = event.player.uniqueId - val name = event.player.username + fun onPreLogin(event: PreLoginEvent): EventTask { + val name = event.username + val playerId = + event.uniqueId + ?: UUID.nameUUIDFromBytes("OfflinePlayer:$name".toByteArray(Charsets.UTF_8)) return EventTask.async { - val result = playerPresenceService.tryLogin(playerId) - if (result == null) { - event.result = ComponentResult.denied(Component.text(messages.serviceUnavailable)) - return@async - } - - val kickMessage = - when (result.status) { - LoginStatus.LOGIN_STATUS_ACCEPTED -> { - logger.info("player session created: {} ({})", name, playerId) + when (val result = playerPresenceService.tryLogin(playerId)) { + is PlayerLoginResult.Success -> { + if (handleSuccess(event, name, playerId, result.reply)) { return@async } - LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> messages.alreadyOnline - LoginStatus.LOGIN_STATUS_INVALID_REQUEST -> messages.invalidRequest - LoginStatus.LOGIN_STATUS_UNSPECIFIED, - LoginStatus.LOGIN_STATUS_ERROR, - LoginStatus.UNRECOGNIZED -> messages.genericError } - - logger.warn( - "player session rejected: {} ({}) status={} message={}", - name, - playerId, - result.status, - result.message, - ) - - event.result = ComponentResult.denied(Component.text(kickMessage)) + is PlayerLoginResult.Unavailable -> { + logger.warn( + "player presence unavailable: {} ({}) reason={}", + name, + playerId, + result.message, + ) + deny(event, messages.serviceUnavailable) + } + is PlayerLoginResult.Error -> { + logger.warn( + "player presence error: {} ({}) reason={}", + name, + playerId, + result.message, + ) + deny(event, messages.genericError) + } + } } } @@ -69,4 +70,39 @@ class PlayerConnectionListener( ) } } + + private fun handleSuccess( + event: PreLoginEvent, + name: String, + playerId: UUID, + reply: PlayerLoginReply, + ): Boolean { + val kickMessage = + when (reply.status) { + LoginStatus.LOGIN_STATUS_ACCEPTED -> { + logger.info("player session created: {} ({})", name, playerId) + return true + } + LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> messages.alreadyOnline + LoginStatus.LOGIN_STATUS_INVALID_REQUEST -> messages.invalidRequest + LoginStatus.LOGIN_STATUS_UNSPECIFIED, + LoginStatus.LOGIN_STATUS_ERROR, + LoginStatus.UNRECOGNIZED -> messages.genericError + } + + logger.warn( + "player session rejected: {} ({}) status={} message={}", + name, + playerId, + reply.status, + reply.message, + ) + + deny(event, kickMessage) + return false + } + + private fun deny(event: PreLoginEvent, message: String) { + event.result = PreLoginEvent.PreLoginComponentResult.denied(Component.text(message)) + } } diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt index c055b0c..67c72f3 100644 --- a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt @@ -1,8 +1,8 @@ package gg.grounds.presence -import gg.grounds.grpc.player.PlayerLoginReply import gg.grounds.grpc.player.PlayerLogoutReply import gg.grounds.player.presence.GrpcPlayerPresenceClient +import gg.grounds.player.presence.PlayerLoginResult import gg.grounds.player.presence.PlayerPresenceClientConfig import java.util.* import org.slf4j.Logger @@ -15,12 +15,12 @@ class PlayerPresenceService(private val logger: Logger) : AutoCloseable { client = GrpcPlayerPresenceClient.create(config) } - fun tryLogin(playerId: UUID): PlayerLoginReply? { + fun tryLogin(playerId: UUID): PlayerLoginResult { return try { client.tryLogin(playerId) } catch (e: RuntimeException) { logger.warn("player presence tryLogin failed: {}", playerId, e) - null + PlayerLoginResult.Error(e.message ?: e::class.java.name) } } From cd0b24e5c6add8ada27c8d962ad989d8d07a901b Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Mon, 19 Jan 2026 14:48:50 +0100 Subject: [PATCH 8/9] feat: use envs for configuring plugin --- README.md | 11 ++++ .../presence/GrpcPlayerPresenceClient.kt | 17 +++---- .../presence/PlayerPresenceClientConfig.kt | 23 --------- velocity/devspace.yaml | 2 + .../kotlin/gg/grounds/GroundsPluginPlayer.kt | 28 +++++------ .../gg/grounds/config/MessagesConfig.kt | 8 +++ ...onfigLoader.kt => MessagesConfigLoader.kt} | 27 +++++----- .../kotlin/gg/grounds/config/PluginConfig.kt | 31 ------------ .../listener/PlayerConnectionListener.kt | 50 ++++++++++++------- .../grounds/presence/PlayerPresenceService.kt | 12 ++--- velocity/src/main/resources/config.yml | 10 ---- velocity/src/main/resources/messages.yml | 4 ++ 12 files changed, 96 insertions(+), 127 deletions(-) delete mode 100644 common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt create mode 100644 velocity/src/main/kotlin/gg/grounds/config/MessagesConfig.kt rename velocity/src/main/kotlin/gg/grounds/config/{PluginConfigLoader.kt => MessagesConfigLoader.kt} (64%) delete mode 100644 velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt delete mode 100644 velocity/src/main/resources/config.yml create mode 100644 velocity/src/main/resources/messages.yml diff --git a/README.md b/README.md index 13e06cf..64cbdb2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ ./gradlew build ``` +## Configuration + +The plugin requires the gRPC target to be provided via environment variable: + +```bash +export PLAYER_PRESENCE_GRPC_TARGET="dns:///service-player.api.svc.cluster.local:9000" +``` + +Messages are configured in `velocity/src/main/resources/messages.yml` (copied to the plugin data +directory on first run). + ## Development Run in dev mode with live reload using DevSpace in a Kubernetes cluster: 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 5e17234..5a34d1e 100644 --- a/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt +++ b/common/src/main/kotlin/gg/grounds/player/presence/GrpcPlayerPresenceClient.kt @@ -15,13 +15,12 @@ class GrpcPlayerPresenceClient private constructor( private val channel: ManagedChannel, private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub, - private val config: PlayerPresenceClientConfig, ) : AutoCloseable { fun tryLogin(playerId: UUID): PlayerLoginResult { return try { val reply = stub - .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) .tryPlayerLogin( PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build() ) @@ -39,7 +38,7 @@ private constructor( fun logout(playerId: UUID): PlayerLogoutReply { return try { stub - .withDeadlineAfter(config.timeout.toMillis(), TimeUnit.MILLISECONDS) + .withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS) .playerLogout( PlayerLogoutRequest.newBuilder().setPlayerId(playerId.toString()).build() ) @@ -64,14 +63,12 @@ private constructor( } companion object { - fun create(config: PlayerPresenceClientConfig): GrpcPlayerPresenceClient { - val channelBuilder = ManagedChannelBuilder.forTarget(config.target) - if (config.plaintext) { - channelBuilder.usePlaintext() - } + fun create(target: String): GrpcPlayerPresenceClient { + val channelBuilder = ManagedChannelBuilder.forTarget(target) + channelBuilder.usePlaintext() val channel = channelBuilder.build() val stub = PlayerPresenceServiceGrpc.newBlockingStub(channel) - return GrpcPlayerPresenceClient(channel, stub, config) + return GrpcPlayerPresenceClient(channel, stub) } private fun errorLogoutReply(message: String): PlayerLogoutReply = @@ -79,5 +76,7 @@ private constructor( private fun isServiceUnavailable(status: Status.Code): Boolean = status == Status.Code.UNAVAILABLE || status == Status.Code.DEADLINE_EXCEEDED + + private const val DEFAULT_TIMEOUT_MS = 2000L } } diff --git a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt b/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt deleted file mode 100644 index 19a8fa6..0000000 --- a/common/src/main/kotlin/gg/grounds/player/presence/PlayerPresenceClientConfig.kt +++ /dev/null @@ -1,23 +0,0 @@ -package gg.grounds.player.presence - -import java.time.Duration - -data class PlayerPresenceClientConfig( - val target: String, - val plaintext: Boolean, - val timeout: Duration, -) { - init { - require(target.isNotBlank()) { "target must not be blank" } - require(!timeout.isZero && !timeout.isNegative) { "timeout must be > 0" } - } - - companion object { - fun defaults(): PlayerPresenceClientConfig = - PlayerPresenceClientConfig( - target = "localhost:9000", - plaintext = true, - timeout = Duration.ofSeconds(2), - ) - } -} diff --git a/velocity/devspace.yaml b/velocity/devspace.yaml index 4c4386b..a20f2f1 100644 --- a/velocity/devspace.yaml +++ b/velocity/devspace.yaml @@ -29,6 +29,8 @@ dev: terminal: disableReplace: true command: | + # ENV needs to be exported in start command because we are not letting devspace replace the pod + export PLAYER_PRESENCE_GRPC_TARGET=dns:///service-player.api.svc.cluster.local:9000 echo "Use ./start.sh to start the server" /bin/sh # Forward the following ports to be able to access your application via localhost diff --git a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt index 58bd632..e5f3bab 100644 --- a/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt +++ b/velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt @@ -7,7 +7,7 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent import com.velocitypowered.api.plugin.Plugin import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.ProxyServer -import gg.grounds.config.PluginConfigLoader +import gg.grounds.config.MessagesConfigLoader import gg.grounds.listener.PlayerConnectionListener import gg.grounds.presence.PlayerPresenceService import io.grpc.LoadBalancerRegistry @@ -21,7 +21,7 @@ import org.slf4j.Logger id = "plugin-player", name = "Grounds Player Plugin", version = BuildInfo.VERSION, - description = "", + description = "A plugin which manages player related actions and data transactions", authors = ["Grounds Development Team and contributors"], url = "https://github.com/groundsgg/plugin-player", ) @@ -32,35 +32,30 @@ constructor( private val logger: Logger, @param:DataDirectory private val dataDirectory: Path, ) { - private val playerPresenceService = PlayerPresenceService(logger) + private val playerPresenceService = PlayerPresenceService() init { - logger.info("VelocityPlayerPlugin initialized") + logger.info("Initialized plugin (plugin=plugin-player, version={})", BuildInfo.VERSION) } @Subscribe fun onInitialize(event: ProxyInitializeEvent) { registerProviders() - val config = PluginConfigLoader(logger, dataDirectory).loadOrCreate() - val clientConfig = config.playerPresence.toClientConfig() - playerPresenceService.configure(clientConfig) + val messages = MessagesConfigLoader(logger, dataDirectory).loadOrCreate() + val target = resolveTarget() + playerPresenceService.configure(target) proxy.eventManager.register( this, PlayerConnectionListener( logger = logger, playerPresenceService = playerPresenceService, - messages = config.messages, + messages = messages, ), ) - logger.info( - "PlayerPresence gRPC configured (target={}, plaintext={}, timeoutMs={})", - clientConfig.target, - clientConfig.plaintext, - clientConfig.timeout.toMillis(), - ) + logger.info("Configured player presence gRPC client (target={})", target) } @Subscribe @@ -78,4 +73,9 @@ constructor( NameResolverRegistry.getDefaultRegistry().register(DnsNameResolverProvider()) LoadBalancerRegistry.getDefaultRegistry().register(PickFirstLoadBalancerProvider()) } + + private fun resolveTarget(): String { + return System.getenv("PLAYER_PRESENCE_GRPC_TARGET")?.takeIf { it.isNotBlank() } + ?: error("Missing required environment variable PLAYER_PRESENCE_GRPC_TARGET") + } } diff --git a/velocity/src/main/kotlin/gg/grounds/config/MessagesConfig.kt b/velocity/src/main/kotlin/gg/grounds/config/MessagesConfig.kt new file mode 100644 index 0000000..946a6d0 --- /dev/null +++ b/velocity/src/main/kotlin/gg/grounds/config/MessagesConfig.kt @@ -0,0 +1,8 @@ +package gg.grounds.config + +data class MessagesConfig( + val serviceUnavailable: String = "Login service unavailable", + val alreadyOnline: String = "You are already online.", + val invalidRequest: String = "Invalid login request.", + val genericError: String = "Unable to create player session.", +) diff --git a/velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt b/velocity/src/main/kotlin/gg/grounds/config/MessagesConfigLoader.kt similarity index 64% rename from velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt rename to velocity/src/main/kotlin/gg/grounds/config/MessagesConfigLoader.kt index d53eda3..881a8fb 100644 --- a/velocity/src/main/kotlin/gg/grounds/config/PluginConfigLoader.kt +++ b/velocity/src/main/kotlin/gg/grounds/config/MessagesConfigLoader.kt @@ -9,15 +9,15 @@ import tools.jackson.dataformat.yaml.YAMLMapper import tools.jackson.module.kotlin.KotlinModule import tools.jackson.module.kotlin.readValue -class PluginConfigLoader(private val logger: Logger, private val dataDirectory: Path) { +class MessagesConfigLoader(private val logger: Logger, private val dataDirectory: Path) { private val mapper: YAMLMapper = YAMLMapper.builder() .addModule(KotlinModule.Builder().build()) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .build() - fun loadOrCreate(): PluginConfig { - val yamlFile = dataDirectory.resolve("config.yml") + fun loadOrCreate(): MessagesConfig { + val yamlFile = dataDirectory.resolve("messages.yml") return try { Files.createDirectories(dataDirectory) @@ -28,29 +28,30 @@ class PluginConfigLoader(private val logger: Logger, private val dataDirectory: Files.newInputStream(yamlFile).use { input -> parseYaml(input) } } catch (e: Exception) { - logger.warn("Unable to load config from {}", yamlFile, e) - PluginConfig() + logger.warn("Failed to load messages config (path={})", yamlFile, e) + MessagesConfig() } } - private fun parseYaml(input: InputStream): PluginConfig { + private fun parseYaml(input: InputStream): MessagesConfig { return try { - mapper.readValue(input) + mapper.readValue(input) } catch (e: Exception) { - logger.warn("Unable to parse config.yml; using defaults", e) - PluginConfig() + logger.warn("Failed to parse messages config; using defaults (path=messages.yml)", e) + MessagesConfig() } } private fun copyDefaultConfig(target: Path) { - val resourceName = "config.yml" + val resourceName = "messages.yml" val inputStream = javaClass.classLoader.getResourceAsStream(resourceName) if (inputStream == null) { logger.warn( - "Default config resource {} not found; writing generated defaults", + "Default messages resource missing; writing defaults (resource={}, path={})", resourceName, + target, ) - writeYaml(target, PluginConfig()) + writeYaml(target, MessagesConfig()) return } @@ -59,7 +60,7 @@ class PluginConfigLoader(private val logger: Logger, private val dataDirectory: } } - private fun writeYaml(target: Path, config: PluginConfig) { + private fun writeYaml(target: Path, config: MessagesConfig) { Files.newOutputStream(target).use { output -> output.writer(Charsets.UTF_8).use { writer -> mapper.writeValue(writer, config) } } diff --git a/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt b/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt deleted file mode 100644 index 22dc6bc..0000000 --- a/velocity/src/main/kotlin/gg/grounds/config/PluginConfig.kt +++ /dev/null @@ -1,31 +0,0 @@ -package gg.grounds.config - -import gg.grounds.player.presence.PlayerPresenceClientConfig -import java.time.Duration - -data class PluginConfig( - val playerPresence: PlayerPresence = PlayerPresence(), - val messages: Messages = Messages(), -) { - data class PlayerPresence( - val target: String = PlayerPresenceClientConfig.defaults().target, - val plaintext: Boolean = PlayerPresenceClientConfig.defaults().plaintext, - val timeoutMillis: Long = PlayerPresenceClientConfig.defaults().timeout.toMillis(), - ) { - fun toClientConfig(): PlayerPresenceClientConfig { - val millis = maxOf(1L, timeoutMillis) - return PlayerPresenceClientConfig( - target = target, - plaintext = plaintext, - timeout = Duration.ofMillis(millis), - ) - } - } - - data class Messages( - val serviceUnavailable: String = "Login service unavailable", - val alreadyOnline: String = "You are already online.", - val invalidRequest: String = "Invalid login request.", - val genericError: String = "Unable to create player session.", - ) -} diff --git a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt index dd29ab9..be56f94 100644 --- a/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt +++ b/velocity/src/main/kotlin/gg/grounds/listener/PlayerConnectionListener.kt @@ -4,7 +4,8 @@ import com.velocitypowered.api.event.EventTask import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.connection.DisconnectEvent import com.velocitypowered.api.event.connection.PreLoginEvent -import gg.grounds.config.PluginConfig +import com.velocitypowered.api.util.UuidUtils +import gg.grounds.config.MessagesConfig import gg.grounds.grpc.player.LoginStatus import gg.grounds.grpc.player.PlayerLoginReply import gg.grounds.player.presence.PlayerLoginResult @@ -16,14 +17,12 @@ import org.slf4j.Logger class PlayerConnectionListener( private val logger: Logger, private val playerPresenceService: PlayerPresenceService, - private val messages: PluginConfig.Messages, + private val messages: MessagesConfig, ) { @Subscribe fun onPreLogin(event: PreLoginEvent): EventTask { val name = event.username - val playerId = - event.uniqueId - ?: UUID.nameUUIDFromBytes("OfflinePlayer:$name".toByteArray(Charsets.UTF_8)) + val playerId = event.uniqueId ?: UuidUtils.generateOfflinePlayerUuid(name) return EventTask.async { when (val result = playerPresenceService.tryLogin(playerId)) { @@ -34,18 +33,18 @@ class PlayerConnectionListener( } is PlayerLoginResult.Unavailable -> { logger.warn( - "player presence unavailable: {} ({}) reason={}", - name, + "Player presence login unavailable (playerId={}, username={}, reason={})", playerId, + name, result.message, ) deny(event, messages.serviceUnavailable) } is PlayerLoginResult.Error -> { logger.warn( - "player presence error: {} ({}) reason={}", - name, + "Player presence login failed (playerId={}, username={}, reason={})", playerId, + name, result.message, ) deny(event, messages.genericError) @@ -61,13 +60,21 @@ class PlayerConnectionListener( return EventTask.async { val result = playerPresenceService.logout(playerId) ?: return@async - logger.info( - "player session logout: {} ({}) removed={} message={}", - name, - playerId, - result.removed, - result.message, - ) + if (result.removed) { + logger.info( + "Player session logout completed (playerId={}, username={}, message={})", + playerId, + name, + result.message, + ) + } else { + logger.warn( + "Player session logout failed (playerId={}, username={}, message={})", + playerId, + name, + result.message, + ) + } } } @@ -80,7 +87,12 @@ class PlayerConnectionListener( val kickMessage = when (reply.status) { LoginStatus.LOGIN_STATUS_ACCEPTED -> { - logger.info("player session created: {} ({})", name, playerId) + logger.info( + "Player session created (playerId={}, username={}, status={})", + playerId, + name, + reply.status, + ) return true } LoginStatus.LOGIN_STATUS_ALREADY_ONLINE -> messages.alreadyOnline @@ -91,9 +103,9 @@ class PlayerConnectionListener( } logger.warn( - "player session rejected: {} ({}) status={} message={}", - name, + "Player session rejected (playerId={}, username={}, status={}, message={})", playerId, + name, reply.status, reply.message, ) diff --git a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt index 67c72f3..ba6fa37 100644 --- a/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt +++ b/velocity/src/main/kotlin/gg/grounds/presence/PlayerPresenceService.kt @@ -3,23 +3,20 @@ package gg.grounds.presence import gg.grounds.grpc.player.PlayerLogoutReply import gg.grounds.player.presence.GrpcPlayerPresenceClient import gg.grounds.player.presence.PlayerLoginResult -import gg.grounds.player.presence.PlayerPresenceClientConfig -import java.util.* -import org.slf4j.Logger +import java.util.UUID -class PlayerPresenceService(private val logger: Logger) : AutoCloseable { +class PlayerPresenceService : AutoCloseable { private lateinit var client: GrpcPlayerPresenceClient - fun configure(config: PlayerPresenceClientConfig) { + fun configure(target: String) { close() - client = GrpcPlayerPresenceClient.create(config) + client = GrpcPlayerPresenceClient.create(target) } fun tryLogin(playerId: UUID): PlayerLoginResult { return try { client.tryLogin(playerId) } catch (e: RuntimeException) { - logger.warn("player presence tryLogin failed: {}", playerId, e) PlayerLoginResult.Error(e.message ?: e::class.java.name) } } @@ -28,7 +25,6 @@ class PlayerPresenceService(private val logger: Logger) : AutoCloseable { return try { client.logout(playerId) } catch (e: RuntimeException) { - logger.warn("player presence logout failed: {}", playerId, e) null } } diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml deleted file mode 100644 index be77e84..0000000 --- a/velocity/src/main/resources/config.yml +++ /dev/null @@ -1,10 +0,0 @@ -playerPresence: - target: "dns:///service-player.api.svc.cluster.local:9000" - plaintext: true - timeoutMillis: 2000 - -messages: - serviceUnavailable: "Login service unavailable" - alreadyOnline: "You are already online." - invalidRequest: "Invalid login request." - genericError: "Unable to create player session." diff --git a/velocity/src/main/resources/messages.yml b/velocity/src/main/resources/messages.yml new file mode 100644 index 0000000..ce7ef24 --- /dev/null +++ b/velocity/src/main/resources/messages.yml @@ -0,0 +1,4 @@ +serviceUnavailable: "Login service unavailable" +alreadyOnline: "You are already online." +invalidRequest: "Invalid login request." +genericError: "Unable to create player session." From 5c18588541b09756cc3d438443a1beb8af610baf Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Mon, 19 Jan 2026 20:47:35 +0100 Subject: [PATCH 9/9] feat: use grpc-conventions plugin --- build.gradle.kts | 2 +- common/build.gradle.kts | 24 ++---------------------- velocity/devspace.yaml | 4 ++-- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fa7030f..9ca9bb1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1 +1 @@ -plugins { id("gg.grounds.base-conventions") version "0.2.0" } +plugins { id("gg.grounds.base-conventions") version "0.3.0" } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c6bdc70..cce2133 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,10 +1,4 @@ -plugins { - id("gg.grounds.kotlin-conventions") - id("com.google.protobuf") version "0.9.6" -} - -val grpcVersion = "1.78.0" -val protobufVersion = "4.33.4" +plugins { id("gg.grounds.grpc-conventions") } repositories { maven { @@ -16,18 +10,4 @@ repositories { } } -dependencies { - api("com.google.protobuf:protobuf-java:$protobufVersion") - implementation("io.grpc:grpc-protobuf:$grpcVersion") - implementation("io.grpc:grpc-stub:$grpcVersion") - - protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") -} - -protobuf { - protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" } - - plugins { create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" } } - - generateProtoTasks { all().forEach { task -> task.plugins { create("grpc") } } } -} +dependencies { protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") } diff --git a/velocity/devspace.yaml b/velocity/devspace.yaml index a20f2f1..f4dd501 100644 --- a/velocity/devspace.yaml +++ b/velocity/devspace.yaml @@ -1,11 +1,11 @@ version: v2beta1 -name: plugin-agones-velocity +name: plugin-player-velocity deployments: app: namespace: games helm: - releaseName: plugin-agones-velocity + releaseName: plugin-player-velocity chart: name: oci://ghcr.io/groundsgg/charts/agones-fleet version: 0.1.0