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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 1 addition & 25 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.base-conventions") version "0.3.0" }
13 changes: 13 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins { id("gg.grounds.grpc-conventions") }

repositories {
maven {
url = uri("https://maven.pkg.github.com/groundsgg/*")
credentials {
username = providers.gradleProperty("github.user").get()
password = providers.gradleProperty("github.token").get()
}
}
}

dependencies { protobuf("gg.grounds:library-grpc-contracts-player:0.1.0") }
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package gg.grounds.player.presence

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

class GrpcPlayerPresenceClient
private constructor(
private val channel: ManagedChannel,
private val stub: PlayerPresenceServiceGrpc.PlayerPresenceServiceBlockingStub,
) : AutoCloseable {
fun tryLogin(playerId: UUID): PlayerLoginResult {
return try {
val reply =
stub
.withDeadlineAfter(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.tryPlayerLogin(
PlayerLoginRequest.newBuilder().setPlayerId(playerId.toString()).build()
)
PlayerLoginResult.Success(reply)
} catch (e: StatusRuntimeException) {
if (isServiceUnavailable(e.status.code)) {
return PlayerLoginResult.Unavailable(e.status.toString())
}
PlayerLoginResult.Error(e.status.toString())
} catch (e: RuntimeException) {
PlayerLoginResult.Error(e.message ?: e::class.java.name)
}
}

fun logout(playerId: UUID): PlayerLogoutReply {
return try {
stub
.withDeadlineAfter(DEFAULT_TIMEOUT_MS, 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(target: String): GrpcPlayerPresenceClient {
val channelBuilder = ManagedChannelBuilder.forTarget(target)
channelBuilder.usePlaintext()
val channel = channelBuilder.build()
val stub = PlayerPresenceServiceGrpc.newBlockingStub(channel)
return GrpcPlayerPresenceClient(channel, stub)
}

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

private const val DEFAULT_TIMEOUT_MS = 2000L
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
15 changes: 14 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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()
}
}
23 changes: 4 additions & 19 deletions velocity/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
plugins {
id("com.gradleup.shadow") version "9.3.0"
}
plugins { id("gg.grounds.velocity-conventions") }

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")
implementation("io.grpc:grpc-netty-shaded:1.78.0")
}
28 changes: 14 additions & 14 deletions velocity/devspace.yaml
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
version: v2beta1
name: velocity-player
name: plugin-player-velocity

deployments:
app:
namespace: games
helm:
releaseName: velocity-player
releaseName: plugin-player-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: |
# 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
Expand Down
32 changes: 0 additions & 32 deletions velocity/src/main/java/gg/grounds/GroundsPluginPlayer.java

This file was deleted.

81 changes: 81 additions & 0 deletions velocity/src/main/kotlin/gg/grounds/GroundsPluginPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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.MessagesConfigLoader
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",
version = BuildInfo.VERSION,
description = "A plugin which manages player related actions and data transactions",
authors = ["Grounds Development Team and contributors"],
url = "https://github.com/groundsgg/plugin-player",
)
class GroundsPluginPlayer
@Inject
constructor(
private val proxy: ProxyServer,
private val logger: Logger,
@param:DataDirectory private val dataDirectory: Path,
) {
private val playerPresenceService = PlayerPresenceService()

init {
logger.info("Initialized plugin (plugin=plugin-player, version={})", BuildInfo.VERSION)
}

@Subscribe
fun onInitialize(event: ProxyInitializeEvent) {
registerProviders()

val messages = MessagesConfigLoader(logger, dataDirectory).loadOrCreate()
val target = resolveTarget()
playerPresenceService.configure(target)

proxy.eventManager.register(
this,
PlayerConnectionListener(
logger = logger,
playerPresenceService = playerPresenceService,
messages = messages,
),
)

logger.info("Configured player presence gRPC client (target={})", target)
}

@Subscribe
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())
}

private fun resolveTarget(): String {
return System.getenv("PLAYER_PRESENCE_GRPC_TARGET")?.takeIf { it.isNotBlank() }
?: error("Missing required environment variable PLAYER_PRESENCE_GRPC_TARGET")
}
}
8 changes: 8 additions & 0 deletions velocity/src/main/kotlin/gg/grounds/config/MessagesConfig.kt
Original file line number Diff line number Diff line change
@@ -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.",
)
Loading