Skip to content
Open
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
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ARG VELOCITY_VERSION="3.2.0-SNAPSHOT"
ARG VELOCITY_BUILD_NUMBER=260
ARG REALIP_VERSION="2.6.0"
ARG VIA_VERSION="4.5.1"
ARG PROTOCOLIZE_BUILD=727

LABEL com.bluedragonmc.image=komodo
LABEL com.bluedragonmc.environment=development
Expand All @@ -29,9 +30,12 @@ ADD "https://api.papermc.io/v2/projects/velocity/versions/$VELOCITY_VERSION/buil
# Add TCPShield's RealIP plugin
ADD "https://github.com/TCPShield/RealIP/releases/download/$REALIP_VERSION/TCPShield-$REALIP_VERSION.jar" /proxy/plugins/disabled/TCPShield-$REALIP_VERSION.jar
# Add LuckPerms for permissions
ADD "https://download.luckperms.net/1512/velocity/LuckPerms-Velocity-5.4.98.jar" /proxy/plugins/LuckPerms-$LP_VERSION.jar
ADD "https://download.luckperms.net/1526/velocity/LuckPerms-Velocity-5.4.113.jar" /proxy/plugins/LuckPerms-$LP_VERSION.jar
# Add the Jukebox plugin (and Protocolize, its dependency)
ADD "https://ci.exceptionflug.de/job/Protocolize2/$PROTOCOLIZE_BUILD/artifact/protocolize-velocity/target/protocolize-velocity.jar" /proxy/plugins/protocolize-$PROTOCOLIZE_BUILD.jar
ADD "https://github.com/BlueDragonMC/Jukebox/releases/download/latest/Jukebox-1.0-SNAPSHOT-all.jar" /proxy/plugins/Jukebox.jar
# Add ViaVersion to allow newer clients to connect
#ADD "https://github.com/ViaVersion/ViaVersion/releases/download/$VIA_VERSION/ViaVersion-${VIA_VERSION}.jar" /proxy/plugins/ViaVersion-$VIA_VERSION.jar
COPY --from=build /work/build/libs/Komodo-*-all.jar /proxy/plugins/Komodo.jar
COPY build/libs/Komodo-*-all.jar /proxy/plugins/Komodo.jar
COPY /assets /proxy
CMD ["sh", "/proxy/entrypoint.sh"]
7 changes: 4 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
kotlin("jvm") version "1.7.10"
kotlin("kapt") version "1.7.10"
kotlin("jvm") version "1.9.0"
kotlin("kapt") version "1.9.0"
id("com.github.johnrengelman.shadow") version "7.0.0"
}

Expand Down Expand Up @@ -30,7 +30,8 @@ dependencies {
implementation("io.grpc:grpc-protobuf:$grpcVersion")
implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
implementation("com.google.protobuf:protobuf-kotlin:$protoVersion")
implementation("com.github.bluedragonmc:rpc:c2785493d9")
implementation("com.github.bluedragonmc:rpc:b7071251fb")
implementation("com.github.BlueDragonMC:Jukebox:a0d80dfc74")
}

tasks.shadowJar {
Expand Down
139 changes: 28 additions & 111 deletions src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
package com.bluedragonmc.komodo

import com.bluedragonmc.api.grpc.*
import com.bluedragonmc.api.grpc.GetPlayersResponseKt.connectedPlayer
import com.bluedragonmc.api.grpc.findLobbyRequest
import com.bluedragonmc.api.grpc.playerLogoutRequest
import com.bluedragonmc.jukebox.JukeboxPlugin
import com.bluedragonmc.komodo.command.AddServerCommand
import com.bluedragonmc.komodo.command.RemoveServerCommand
import com.bluedragonmc.komodo.handler.FailoverHandler
import com.bluedragonmc.komodo.handler.InstanceRoutingHandler
import com.bluedragonmc.komodo.handler.ServerListPingHandler
import com.bluedragonmc.komodo.rpc.JukeboxService
import com.bluedragonmc.komodo.rpc.PlayerHolderService
import com.google.inject.Inject
import com.google.protobuf.Empty
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.connection.DisconnectEvent
import com.velocitypowered.api.event.player.KickedFromServerEvent
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent
import com.velocitypowered.api.plugin.Dependency
import com.velocitypowered.api.plugin.Plugin
import com.velocitypowered.api.proxy.Player
import com.velocitypowered.api.proxy.ProxyServer
import com.velocitypowered.api.proxy.server.RegisteredServer
import com.velocitypowered.api.proxy.server.ServerInfo
import io.grpc.Server
import io.grpc.ServerBuilder
import kotlinx.coroutines.runBlocking
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import java.net.InetSocketAddress
import java.time.Duration
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Logger
import kotlin.jvm.optionals.getOrElse
import kotlin.jvm.optionals.getOrNull
import kotlin.system.exitProcess

@OptIn(ExperimentalStdlibApi::class)
@Plugin(
id = "komodo",
name = "Komodo",
version = "0.0.3",
version = "0.1.0",
description = "BlueDragon's Velocity plugin that handles coordination with our service",
url = "https://bluedragonmc.com",
authors = ["FluxCapacitor2"]
authors = ["FluxCapacitor2"],
dependencies = [Dependency(id = "bluedragon-jukebox", optional = false)]
)
class Komodo {

Expand All @@ -47,9 +47,10 @@ class Komodo {
@Inject
lateinit var proxyServer: ProxyServer

private val lastFailover = mutableMapOf<Player, Long>()
@Inject
lateinit var jukeboxPlugin: JukeboxPlugin

private val instanceRoutingHandler = InstanceRoutingHandler()
internal val instanceRoutingHandler = InstanceRoutingHandler()

private lateinit var server: Server

Expand All @@ -61,7 +62,17 @@ class Komodo {
fun onInit(event: ProxyInitializeEvent) {
try {
INSTANCE = this
server = ServerBuilder.forPort(50051).addService(PlayerHolderService()).build()
val jukeboxService =
JukeboxService(
proxyServer,
jukeboxPlugin.getSongPlayer(proxyServer, this),
jukeboxPlugin.getSongLoader()
)

server = ServerBuilder.forPort(50051)
.addService(PlayerHolderService(proxyServer, logger, instanceRoutingHandler))
.addService(jukeboxService)
.build()
server.start()

// Initialize gRPC channel to Puffin
Expand All @@ -72,6 +83,8 @@ class Komodo {
// Subscribe to events
proxyServer.eventManager.register(this, ServerListPingHandler())
proxyServer.eventManager.register(this, instanceRoutingHandler)
proxyServer.eventManager.register(this, FailoverHandler())
proxyServer.eventManager.register(this, jukeboxService.jukeboxHandler)

// Register commands
proxyServer.commandManager.register(AddServerCommand.create(proxyServer))
Expand All @@ -91,63 +104,8 @@ class Komodo {
}
}

inner class PlayerHolderService : PlayerHolderGrpcKt.PlayerHolderCoroutineImplBase() {
@OptIn(ExperimentalStdlibApi::class)
override suspend fun sendPlayer(request: PlayerHolderOuterClass.SendPlayerRequest): PlayerHolderOuterClass.SendPlayerResponse {

val uuid = UUID.fromString(request.playerUuid)
val player = proxyServer.getPlayer(uuid).getOrNull()
val registeredServer = proxyServer.getServer(request.serverName).getOrNull() ?: run {
logger.info("Registering server ${request.serverName} at ${request.gameServerIp}:${request.gameServerPort} to send player $player to it.")
proxyServer.registerServer(
ServerInfo(request.serverName, InetSocketAddress(request.gameServerIp, request.gameServerPort))
)
}
// Don't try to send a player to their current server
if (player == null || registeredServer == null) {
return sendPlayerResponse {
playerFound = player != null
}
}
if (player.currentServer.getOrNull()?.serverInfo?.name == registeredServer.serverInfo.name) {
return sendPlayerResponse {
playerFound = true
successes += PlayerHolderOuterClass.SendPlayerResponse.SuccessFlags.SET_SERVER
}
}
try {
instanceRoutingHandler.route(player, request.instanceId.toString())
player.createConnectionRequest(registeredServer).fireAndForget()
} catch (e: Throwable) {
logger.warning("Error sending player ${player.username} to server $registeredServer!")
e.printStackTrace()
}
logger.info("Sending player $player to server $registeredServer and instance ${request.instanceId}")
return sendPlayerResponse {
playerFound = true
successes += PlayerHolderOuterClass.SendPlayerResponse.SuccessFlags.SET_SERVER
successes += PlayerHolderOuterClass.SendPlayerResponse.SuccessFlags.SET_INSTANCE
}
}

override suspend fun getPlayers(request: Empty): PlayerHolderOuterClass.GetPlayersResponse {
return getPlayersResponse {
proxyServer.allPlayers.forEach { player ->
this.players += connectedPlayer {
this.uuid = player.uniqueId.toString()
this.username = player.username
if (player.currentServer.isPresent) {
this.serverName = player.currentServer.get().serverInfo.name
}
}
}
}
}
}

@Subscribe
fun onPlayerLeave(event: DisconnectEvent) {
lastFailover.remove(event.player)
runBlocking {
Stubs.playerTracking.playerLogout(playerLogoutRequest {
username = event.player.username
Expand All @@ -156,47 +114,6 @@ class Komodo {
}
}

@Subscribe
fun onPlayerKick(event: KickedFromServerEvent) {
if (event.kickedDuringServerConnect() || ((lastFailover[event.player]
?: 0L) + 10000 > System.currentTimeMillis())
) {
return
}

// Kick messages with a non-breaking space (U+00A0) should not trigger failover
// This is a way of differentiating intentional vs. accidental kicks that remains invisible to the end user
val kickWasIntentional = event.serverKickReason.getOrNull()?.toPlainText()?.contains("\u00A0")
if (kickWasIntentional == true) {
val extraInfo = if (event.kickedDuringServerConnect()) {
Component.text(
"You were kicked while trying to join " + event.server.serverInfo.name + ".",
NamedTextColor.DARK_GRAY
)
} else {
Component.text(
"You were kicked from " + event.server.serverInfo.name + ".",
NamedTextColor.DARK_GRAY
)
}
event.result =
KickedFromServerEvent.DisconnectPlayer.create(extraInfo + Component.newline() + event.serverKickReason.get())
return
}

lastFailover[event.player] = System.currentTimeMillis()

val (registeredServer, lobbyInstance) = getLobby(excluding = event.server.serverInfo.name)
val msg = Component.text("You were kicked from ${event.server.serverInfo.name}: ", NamedTextColor.RED)
.append(event.serverKickReason.orElse(Component.text("No reason specified", NamedTextColor.DARK_GRAY)))
if (registeredServer != null) {
event.result = KickedFromServerEvent.RedirectPlayer.create(registeredServer, msg)
instanceRoutingHandler.route(event.player, lobbyInstance)
} else {
event.result = KickedFromServerEvent.DisconnectPlayer.create(msg)
}
}

fun getLobby(serverName: String? = null, excluding: String? = null): Pair<RegisteredServer?, String?> =
runBlocking {
val response = Stubs.discovery.findLobby(findLobbyRequest {
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/bluedragonmc/komodo/RPCUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.bluedragonmc.komodo

import org.slf4j.LoggerFactory

object RPCUtils {
inline fun <R : Any> handleRPC(handler: () -> R): R {
try {
return handler()
} catch (e: Throwable) {
LoggerFactory.getLogger(this::class.java).error("An error occurred in an RPC handler:")
e.printStackTrace()
throw e
}
}
}
63 changes: 63 additions & 0 deletions src/main/kotlin/com/bluedragonmc/komodo/handler/FailoverHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.bluedragonmc.komodo.handler

import com.bluedragonmc.komodo.Komodo
import com.bluedragonmc.komodo.plus
import com.bluedragonmc.komodo.toPlainText
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.connection.DisconnectEvent
import com.velocitypowered.api.event.player.KickedFromServerEvent
import com.velocitypowered.api.proxy.Player
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import kotlin.jvm.optionals.getOrNull

class FailoverHandler {

private val lastFailover = mutableMapOf<Player, Long>()

@Subscribe
fun onPlayerKick(event: KickedFromServerEvent) {
if (event.kickedDuringServerConnect() || ((lastFailover[event.player]
?: 0L) + 10000 > System.currentTimeMillis())
) {
return
}

// Kick messages with a non-breaking space (U+00A0) should not trigger failover
// This is a way of differentiating intentional vs. accidental kicks that remains invisible to the end user
val kickWasIntentional = event.serverKickReason.getOrNull()?.toPlainText()?.contains("\u00A0")
if (kickWasIntentional == true) {
val extraInfo = if (event.kickedDuringServerConnect()) {
Component.text(
"You were kicked while trying to join " + event.server.serverInfo.name + ".",
NamedTextColor.DARK_GRAY
)
} else {
Component.text(
"You were kicked from " + event.server.serverInfo.name + ".",
NamedTextColor.DARK_GRAY
)
}
event.result =
KickedFromServerEvent.DisconnectPlayer.create(extraInfo + Component.newline() + event.serverKickReason.get())
return
}

lastFailover[event.player] = System.currentTimeMillis()

val (registeredServer, lobbyInstance) = Komodo.INSTANCE.getLobby(excluding = event.server.serverInfo.name)
val msg = Component.text("You were kicked from ${event.server.serverInfo.name}: ", NamedTextColor.RED)
.append(event.serverKickReason.orElse(Component.text("No reason specified", NamedTextColor.DARK_GRAY)))
if (registeredServer != null) {
event.result = KickedFromServerEvent.RedirectPlayer.create(registeredServer, msg)
Komodo.INSTANCE.instanceRoutingHandler.route(event.player, lobbyInstance)
} else {
event.result = KickedFromServerEvent.DisconnectPlayer.create(msg)
}
}

@Subscribe
fun onPlayerLeave(event: DisconnectEvent) {
lastFailover.remove(event.player)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.bluedragonmc.komodo
package com.bluedragonmc.komodo.handler

import com.bluedragonmc.api.grpc.playerLoginRequest
import com.bluedragonmc.komodo.Komodo
import com.bluedragonmc.komodo.Stubs
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.velocitypowered.api.event.Subscribe
Expand Down
Loading