From 359b8eb99652184393825fb25d317c3c17e0ffbe Mon Sep 17 00:00:00 2001 From: FluxCapacitor2 <31071265+FluxCapacitor2@users.noreply.github.com> Date: Fri, 22 Dec 2023 00:20:15 -0500 Subject: [PATCH 1/2] Add Jukebox RPC integration --- build.gradle.kts | 7 +- .../kotlin/com/bluedragonmc/komodo/Komodo.kt | 131 +++----------- .../komodo/handler/FailoverHandler.kt | 63 +++++++ .../{ => handler}/InstanceRoutingHandler.kt | 4 +- .../{ => handler}/ServerListPingHandler.kt | 25 ++- .../komodo/jukebox/JukeboxState.kt | 161 ++++++++++++++++++ .../bluedragonmc/komodo/rpc/JukeboxService.kt | 84 +++++++++ .../komodo/rpc/PlayerHolderService.kt | 73 ++++++++ 8 files changed, 423 insertions(+), 125 deletions(-) create mode 100644 src/main/kotlin/com/bluedragonmc/komodo/handler/FailoverHandler.kt rename src/main/kotlin/com/bluedragonmc/komodo/{ => handler}/InstanceRoutingHandler.kt (97%) rename src/main/kotlin/com/bluedragonmc/komodo/{ => handler}/ServerListPingHandler.kt (82%) create mode 100644 src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt create mode 100644 src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt create mode 100644 src/main/kotlin/com/bluedragonmc/komodo/rpc/PlayerHolderService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7eac286..be5a942 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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" } @@ -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:3b2e9f051a") } tasks.shadowJar { diff --git a/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt b/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt index 8451510..7128733 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt @@ -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.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.jukebox.JukeboxState +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 { @@ -47,9 +47,7 @@ class Komodo { @Inject lateinit var proxyServer: ProxyServer - private val lastFailover = mutableMapOf() - - private val instanceRoutingHandler = InstanceRoutingHandler() + internal val instanceRoutingHandler = InstanceRoutingHandler() private lateinit var server: Server @@ -61,7 +59,10 @@ class Komodo { fun onInit(event: ProxyInitializeEvent) { try { INSTANCE = this - server = ServerBuilder.forPort(50051).addService(PlayerHolderService()).build() + server = ServerBuilder.forPort(50051) + .addService(PlayerHolderService(proxyServer, logger, instanceRoutingHandler)) + .addService(JukeboxService(proxyServer, logger)) + .build() server.start() // Initialize gRPC channel to Puffin @@ -72,6 +73,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, JukeboxState) // Register commands proxyServer.commandManager.register(AddServerCommand.create(proxyServer)) @@ -91,63 +94,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 @@ -156,47 +104,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 = runBlocking { val response = Stubs.discovery.findLobby(findLobbyRequest { diff --git a/src/main/kotlin/com/bluedragonmc/komodo/handler/FailoverHandler.kt b/src/main/kotlin/com/bluedragonmc/komodo/handler/FailoverHandler.kt new file mode 100644 index 0000000..6f3c26a --- /dev/null +++ b/src/main/kotlin/com/bluedragonmc/komodo/handler/FailoverHandler.kt @@ -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() + + @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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bluedragonmc/komodo/InstanceRoutingHandler.kt b/src/main/kotlin/com/bluedragonmc/komodo/handler/InstanceRoutingHandler.kt similarity index 97% rename from src/main/kotlin/com/bluedragonmc/komodo/InstanceRoutingHandler.kt rename to src/main/kotlin/com/bluedragonmc/komodo/handler/InstanceRoutingHandler.kt index a4d8964..8768054 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/InstanceRoutingHandler.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/handler/InstanceRoutingHandler.kt @@ -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 diff --git a/src/main/kotlin/com/bluedragonmc/komodo/ServerListPingHandler.kt b/src/main/kotlin/com/bluedragonmc/komodo/handler/ServerListPingHandler.kt similarity index 82% rename from src/main/kotlin/com/bluedragonmc/komodo/ServerListPingHandler.kt rename to src/main/kotlin/com/bluedragonmc/komodo/handler/ServerListPingHandler.kt index 4712a9b..17ca705 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/ServerListPingHandler.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/handler/ServerListPingHandler.kt @@ -1,10 +1,10 @@ -package com.bluedragonmc.komodo +package com.bluedragonmc.komodo.handler import com.bluedragonmc.api.grpc.ServerTracking +import com.bluedragonmc.komodo.* import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyPingEvent -import com.velocitypowered.api.proxy.server.ServerPing.SamplePlayer -import com.velocitypowered.api.proxy.server.ServerPing.Version +import com.velocitypowered.api.proxy.server.ServerPing import com.velocitypowered.api.util.Favicon import kotlinx.coroutines.* import net.kyori.adventure.text.Component @@ -13,19 +13,26 @@ import java.io.File import java.nio.charset.Charset import java.nio.file.FileSystems import java.nio.file.Paths -import java.nio.file.StandardWatchEventKinds.* +import java.nio.file.StandardWatchEventKinds import java.time.Duration import java.util.* import kotlin.coroutines.CoroutineContext import kotlin.io.path.inputStream +private const val PLAYER_COUNT_FETCH_INTERVAL = 5L + class ServerListPingHandler { private val configDir = Paths.get("/proxy/config/") private fun watchConfig() { val watcher = FileSystems.getDefault().newWatchService() - configDir.register(watcher, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE) + configDir.register( + watcher, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE + ) while (true) { val key = watcher.take() for (event in key.pollEvents()) { @@ -67,13 +74,13 @@ class ServerListPingHandler { val samplePlayers = Komodo.INSTANCE.proxyServer.allPlayers .shuffled() .take(5) - .map { SamplePlayer(it.username, it.uniqueId) } + .map { ServerPing.SamplePlayer(it.username, it.uniqueId) } event.ping = event.ping.asBuilder() .favicon(favicon) .description(motd) .onlinePlayers(lastOnlinePlayerCount) - .version(Version(763, "1.20")) + .version(ServerPing.Version(763, "1.20")) .samplePlayers(*samplePlayers.toTypedArray()) .build() } @@ -94,6 +101,6 @@ class ServerListPingHandler { lastOnlinePlayerCount = runBlocking { Stubs.instanceSvc.getTotalPlayerCount(ServerTracking.PlayerCountRequest.getDefaultInstance()).totalPlayers } - }.repeat(Duration.ofSeconds(30)).schedule() + }.repeat(Duration.ofSeconds(PLAYER_COUNT_FETCH_INTERVAL)).schedule() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt b/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt new file mode 100644 index 0000000..52b4be0 --- /dev/null +++ b/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt @@ -0,0 +1,161 @@ +package com.bluedragonmc.komodo.jukebox + +import com.bluedragonmc.jukebox.Song +import com.bluedragonmc.jukebox.event.SongEndEvent +import com.bluedragonmc.jukebox.event.SongStartEvent +import com.github.benmanes.caffeine.cache.Caffeine +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.proxy.Player +import java.time.Duration +import kotlin.io.path.Path + +data class PlayerSongState( + val isPlaying: Boolean, + val queue: List, +) + +data class SongState( + val songName: String, + val fileName: String, + val timeInTicks: Int, + val lengthInTicks: Int, + val tags: List, +) + +object JukeboxState { + + private val songStates = mutableMapOf() + + private val defaultSongState = PlayerSongState(false, emptyList()) + + internal fun getSongState(player: Player) = songStates[player] ?: defaultSongState + + private fun updateQueue(player: Player, updater: (MutableList) -> List): List { + val oldState = getSongState(player) + val queue = updater(oldState.queue.toMutableList()) + val newState = oldState.copy(queue = queue) + songStates[player] = newState + + if (oldState.isPlaying) { + if (newState.queue.isEmpty()) { + // All songs were removed from the queue; stop the music + Song.stop(player) + } else if (oldState.queue.isEmpty() || newState.queue.first() != oldState.queue.first()) { + // The currently-playing song was changed; play the new one + val entry = newState.queue.first() + val song = getSong(entry.fileName) + Song.play(song, player, entry.timeInTicks) + } + } + + return newState.queue + } + + private fun addToQueue(player: Player, index: Int, state: SongState) { + val current = getSongState(player) + val newQueue = current.queue.toMutableList() + val clampedIndex = index.coerceIn(0..newQueue.size) + + newQueue.add(clampedIndex, state) + + if (clampedIndex == 0) { + // Stop the current song and record its current time + val currentTime = Song.getCurrentSong(player)?.currentTimeInTicks + if (currentTime != null) { + newQueue[1] = newQueue[1].copy(timeInTicks = currentTime) + } + Song.stop(player) + // Play the new song + val song = getSong(state.fileName) + Song.play(song, player, state.timeInTicks) + } + + updateQueue(player) { newQueue } + } + + fun addToQueue( + player: Player, + fileName: String, + startTime: Int, + queuePos: Int, + tags: List, + ) { + val current = getSongState(player) + val newIndex = if (queuePos == -1) current.queue.size else queuePos + val song = getSong(fileName) + + addToQueue(player, newIndex, SongState(song.songName, fileName, startTime, song.durationInTicks, tags)) + } + + fun removeByName(player: Player, name: String) = updateQueue(player) { queue -> + queue.filter { item -> item.fileName != name } + } + + fun removeByTags(player: Player, matchTags: List) = updateQueue(player) { queue -> + queue.filter { item -> item.tags.none { itemTag -> matchTags.contains(itemTag) } } + } + + fun clearQueue(player: Player) = updateQueue(player) { emptyList() } + + private val songCache = Caffeine + .newBuilder() + .expireAfterAccess(Duration.ofMinutes(10)) + .build() + + private fun getSong(fileName: String): Song = songCache.get(fileName) { _ -> + Song.loadRelative(Path(fileName)) + } + + @Subscribe + fun onSongEnd(event: SongEndEvent) { + val player = event.player + val state = getSongState(player) + + if (event.song.fileName != state.queue.firstOrNull()?.fileName) { + // A song *did* end, but it wasn't the first in the queue. There is no need to play the next song in this case. + return + } + + val newQueue = updateQueue(player) { queue -> + if (queue.isNotEmpty()) { + queue.removeFirst() + } + return@updateQueue queue + } + + if (newQueue.isNotEmpty() && state.isPlaying) { + // Play the next song + val nextSong = newQueue.first() + val song = getSong(nextSong.fileName) + Song.play(song, player, nextSong.timeInTicks) + } + } + + @Subscribe + fun onSongStart(event: SongStartEvent) { + // If a song is started that isn't part of the queue (for example, from another plugin), + // this method places it in the front of the queue to keep our state in sync. + + val player = event.player + val state = getSongState(player) + + val firstItem = state.queue.firstOrNull() + + if (firstItem == null || firstItem.songName != event.song.songName) { + updateQueue(player) { queue -> + queue.add( + 0, + SongState( + event.song.songName, + event.song.fileName, + event.startTimeInTicks, + event.song.durationInTicks, + emptyList() + ) + ) + return@updateQueue queue + } + songStates[player] = songStates[player]!!.copy(isPlaying = true) + } + } +} diff --git a/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt b/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt new file mode 100644 index 0000000..758992f --- /dev/null +++ b/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt @@ -0,0 +1,84 @@ +package com.bluedragonmc.komodo.rpc + +import com.bluedragonmc.api.grpc.* +import com.bluedragonmc.jukebox.Song +import com.bluedragonmc.komodo.jukebox.JukeboxState +import com.google.protobuf.Empty +import com.velocitypowered.api.proxy.ProxyServer +import java.util.* +import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull + +class JukeboxService(private val proxyServer: ProxyServer, private val logger: Logger) : + JukeboxGrpcKt.JukeboxCoroutineImplBase() { + override suspend fun getSongInfo(request: JukeboxOuterClass.SongInfoRequest): JukeboxOuterClass.PlayerSongQueue { + + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return playerSongQueue { isPlaying = false } + val state = JukeboxState.getSongState(player) + + return playerSongQueue { + isPlaying = state.isPlaying + state.queue.forEach { song -> + songs.add(playerSongInfo { + this.songName = song.songName + this.playerUuid = request.playerUuid + this.songLengthTicks = song.lengthInTicks + this.songProgressTicks = song.timeInTicks + song.tags.forEach { tag -> this.tags.add(tag) } + }) + } + } + } + + override suspend fun playSong(request: JukeboxOuterClass.PlaySongRequest): JukeboxOuterClass.PlaySongResponse { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return playSongResponse { + playerUuid = request.playerUuid + songName = request.songName + startedPlaying = false + } + + JukeboxState.addToQueue( + player, + request.songName, + request.startTimeTicks, + request.queuePosition, + request.tagsList + ) + + return playSongResponse { + playerUuid = request.playerUuid + songName = request.songName + startedPlaying = request.queuePosition == 0 + } + } + + override suspend fun removeSong(request: JukeboxOuterClass.SongRemoveRequest): Empty { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return Empty.getDefaultInstance() + + JukeboxState.removeByName(player, request.songName) + + return Empty.getDefaultInstance() + } + + override suspend fun removeSongs(request: JukeboxOuterClass.BatchSongRemoveRequest): Empty { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return Empty.getDefaultInstance() + + JukeboxState.removeByTags(player, request.matchTagsList) + + return Empty.getDefaultInstance() + } + + override suspend fun stopSong(request: JukeboxOuterClass.StopSongRequest): Empty { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return Empty.getDefaultInstance() + + JukeboxState.clearQueue(player) + Song.stop(player) + + return Empty.getDefaultInstance() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bluedragonmc/komodo/rpc/PlayerHolderService.kt b/src/main/kotlin/com/bluedragonmc/komodo/rpc/PlayerHolderService.kt new file mode 100644 index 0000000..bd2fb7e --- /dev/null +++ b/src/main/kotlin/com/bluedragonmc/komodo/rpc/PlayerHolderService.kt @@ -0,0 +1,73 @@ +package com.bluedragonmc.komodo.rpc + +import com.bluedragonmc.api.grpc.GetPlayersResponseKt.connectedPlayer +import com.bluedragonmc.api.grpc.PlayerHolderGrpcKt +import com.bluedragonmc.api.grpc.PlayerHolderOuterClass +import com.bluedragonmc.api.grpc.getPlayersResponse +import com.bluedragonmc.api.grpc.sendPlayerResponse +import com.bluedragonmc.komodo.handler.InstanceRoutingHandler +import com.google.protobuf.Empty +import com.velocitypowered.api.proxy.ProxyServer +import com.velocitypowered.api.proxy.server.ServerInfo +import java.net.InetSocketAddress +import java.util.* +import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull + +class PlayerHolderService( + private val proxyServer: ProxyServer, + private val logger: Logger, + private val instanceRoutingHandler: InstanceRoutingHandler, +) : PlayerHolderGrpcKt.PlayerHolderCoroutineImplBase() { + + 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 + } + } + } + } + } +} \ No newline at end of file From 2ec9d0708d73a865a58539b698beabf4d839d813 Mon Sep 17 00:00:00 2001 From: FluxCapacitor2 <31071265+FluxCapacitor2@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:10:31 -0500 Subject: [PATCH 2/2] Rework Jukebox integration according to the new API changes --- Dockerfile | 8 +- build.gradle.kts | 2 +- .../kotlin/com/bluedragonmc/komodo/Komodo.kt | 16 ++- .../com/bluedragonmc/komodo/RPCUtils.kt | 15 +++ .../komodo/jukebox/JukeboxState.kt | 25 ++--- .../bluedragonmc/komodo/rpc/JukeboxService.kt | 97 ++++++++++--------- 6 files changed, 102 insertions(+), 61 deletions(-) create mode 100644 src/main/kotlin/com/bluedragonmc/komodo/RPCUtils.kt diff --git a/Dockerfile b/Dockerfile index 02d4c10..10a0712 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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"] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index be5a942..a6cd28e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion") implementation("com.google.protobuf:protobuf-kotlin:$protoVersion") implementation("com.github.bluedragonmc:rpc:b7071251fb") - implementation("com.github.BlueDragonMC:Jukebox:3b2e9f051a") + implementation("com.github.BlueDragonMC:Jukebox:a0d80dfc74") } tasks.shadowJar { diff --git a/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt b/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt index 7128733..b50114e 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/Komodo.kt @@ -2,12 +2,12 @@ package com.bluedragonmc.komodo 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.jukebox.JukeboxState import com.bluedragonmc.komodo.rpc.JukeboxService import com.bluedragonmc.komodo.rpc.PlayerHolderService import com.google.inject.Inject @@ -47,6 +47,9 @@ class Komodo { @Inject lateinit var proxyServer: ProxyServer + @Inject + lateinit var jukeboxPlugin: JukeboxPlugin + internal val instanceRoutingHandler = InstanceRoutingHandler() private lateinit var server: Server @@ -59,9 +62,16 @@ class Komodo { fun onInit(event: ProxyInitializeEvent) { try { INSTANCE = this + val jukeboxService = + JukeboxService( + proxyServer, + jukeboxPlugin.getSongPlayer(proxyServer, this), + jukeboxPlugin.getSongLoader() + ) + server = ServerBuilder.forPort(50051) .addService(PlayerHolderService(proxyServer, logger, instanceRoutingHandler)) - .addService(JukeboxService(proxyServer, logger)) + .addService(jukeboxService) .build() server.start() @@ -74,7 +84,7 @@ class Komodo { proxyServer.eventManager.register(this, ServerListPingHandler()) proxyServer.eventManager.register(this, instanceRoutingHandler) proxyServer.eventManager.register(this, FailoverHandler()) - proxyServer.eventManager.register(this, JukeboxState) + proxyServer.eventManager.register(this, jukeboxService.jukeboxHandler) // Register commands proxyServer.commandManager.register(AddServerCommand.create(proxyServer)) diff --git a/src/main/kotlin/com/bluedragonmc/komodo/RPCUtils.kt b/src/main/kotlin/com/bluedragonmc/komodo/RPCUtils.kt new file mode 100644 index 0000000..1277477 --- /dev/null +++ b/src/main/kotlin/com/bluedragonmc/komodo/RPCUtils.kt @@ -0,0 +1,15 @@ +package com.bluedragonmc.komodo + +import org.slf4j.LoggerFactory + +object RPCUtils { + inline fun 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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt b/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt index 52b4be0..239b3c6 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/jukebox/JukeboxState.kt @@ -1,6 +1,8 @@ package com.bluedragonmc.komodo.jukebox -import com.bluedragonmc.jukebox.Song +import com.bluedragonmc.jukebox.api.Song +import com.bluedragonmc.jukebox.api.SongLoader +import com.bluedragonmc.jukebox.api.SongPlayer import com.bluedragonmc.jukebox.event.SongEndEvent import com.bluedragonmc.jukebox.event.SongStartEvent import com.github.benmanes.caffeine.cache.Caffeine @@ -8,6 +10,7 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.proxy.Player import java.time.Duration import kotlin.io.path.Path +import kotlin.io.path.readBytes data class PlayerSongState( val isPlaying: Boolean, @@ -22,7 +25,7 @@ data class SongState( val tags: List, ) -object JukeboxState { +class JukeboxState(private val songPlayer: SongPlayer, private val songLoader: SongLoader) { private val songStates = mutableMapOf() @@ -39,12 +42,12 @@ object JukeboxState { if (oldState.isPlaying) { if (newState.queue.isEmpty()) { // All songs were removed from the queue; stop the music - Song.stop(player) + songPlayer.stop(player) } else if (oldState.queue.isEmpty() || newState.queue.first() != oldState.queue.first()) { // The currently-playing song was changed; play the new one val entry = newState.queue.first() val song = getSong(entry.fileName) - Song.play(song, player, entry.timeInTicks) + songPlayer.play(song, player, entry.timeInTicks) } } @@ -60,14 +63,14 @@ object JukeboxState { if (clampedIndex == 0) { // Stop the current song and record its current time - val currentTime = Song.getCurrentSong(player)?.currentTimeInTicks + val currentTime = songPlayer.getCurrentSong(player)?.currentTick if (currentTime != null) { newQueue[1] = newQueue[1].copy(timeInTicks = currentTime) } - Song.stop(player) + songPlayer.stop(player) // Play the new song val song = getSong(state.fileName) - Song.play(song, player, state.timeInTicks) + songPlayer.play(song, player, state.timeInTicks) } updateQueue(player) { newQueue } @@ -103,7 +106,7 @@ object JukeboxState { .build() private fun getSong(fileName: String): Song = songCache.get(fileName) { _ -> - Song.loadRelative(Path(fileName)) + songLoader.load(fileName, Path(fileName).readBytes()) } @Subscribe @@ -111,7 +114,7 @@ object JukeboxState { val player = event.player val state = getSongState(player) - if (event.song.fileName != state.queue.firstOrNull()?.fileName) { + if (event.song.source != state.queue.firstOrNull()?.fileName) { // A song *did* end, but it wasn't the first in the queue. There is no need to play the next song in this case. return } @@ -127,7 +130,7 @@ object JukeboxState { // Play the next song val nextSong = newQueue.first() val song = getSong(nextSong.fileName) - Song.play(song, player, nextSong.timeInTicks) + songPlayer.play(song, player, nextSong.timeInTicks) } } @@ -147,7 +150,7 @@ object JukeboxState { 0, SongState( event.song.songName, - event.song.fileName, + event.song.source, event.startTimeInTicks, event.song.durationInTicks, emptyList() diff --git a/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt b/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt index 758992f..edcec6a 100644 --- a/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt +++ b/src/main/kotlin/com/bluedragonmc/komodo/rpc/JukeboxService.kt @@ -1,83 +1,92 @@ package com.bluedragonmc.komodo.rpc import com.bluedragonmc.api.grpc.* -import com.bluedragonmc.jukebox.Song +import com.bluedragonmc.jukebox.api.SongLoader +import com.bluedragonmc.jukebox.api.SongPlayer +import com.bluedragonmc.komodo.RPCUtils.handleRPC import com.bluedragonmc.komodo.jukebox.JukeboxState import com.google.protobuf.Empty import com.velocitypowered.api.proxy.ProxyServer import java.util.* -import java.util.logging.Logger import kotlin.jvm.optionals.getOrNull -class JukeboxService(private val proxyServer: ProxyServer, private val logger: Logger) : +class JukeboxService( + private val proxyServer: ProxyServer, + private val songPlayer: SongPlayer, + songLoader: SongLoader +) : JukeboxGrpcKt.JukeboxCoroutineImplBase() { - override suspend fun getSongInfo(request: JukeboxOuterClass.SongInfoRequest): JukeboxOuterClass.PlayerSongQueue { - val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() - ?: return playerSongQueue { isPlaying = false } - val state = JukeboxState.getSongState(player) - - return playerSongQueue { - isPlaying = state.isPlaying - state.queue.forEach { song -> - songs.add(playerSongInfo { - this.songName = song.songName - this.playerUuid = request.playerUuid - this.songLengthTicks = song.lengthInTicks - this.songProgressTicks = song.timeInTicks - song.tags.forEach { tag -> this.tags.add(tag) } - }) + internal val jukeboxHandler = JukeboxState(songPlayer, songLoader) + + override suspend fun getSongInfo(request: JukeboxOuterClass.SongInfoRequest): JukeboxOuterClass.PlayerSongQueue = + handleRPC { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return playerSongQueue { isPlaying = false } + val state = jukeboxHandler.getSongState(player) + + return playerSongQueue { + isPlaying = state.isPlaying + state.queue.forEach { song -> + songs.add(playerSongInfo { + this.songName = song.songName + this.playerUuid = request.playerUuid + this.songLengthTicks = song.lengthInTicks + this.songProgressTicks = song.timeInTicks + song.tags.forEach { tag -> this.tags.add(tag) } + }) + } } } - } - override suspend fun playSong(request: JukeboxOuterClass.PlaySongRequest): JukeboxOuterClass.PlaySongResponse { - val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() - ?: return playSongResponse { + override suspend fun playSong(request: JukeboxOuterClass.PlaySongRequest): JukeboxOuterClass.PlaySongResponse = + handleRPC { + val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() + ?: return playSongResponse { + playerUuid = request.playerUuid + songName = request.songName + startedPlaying = false + } + + jukeboxHandler.addToQueue( + player, + request.songName, + request.startTimeTicks, + request.queuePosition, + request.tagsList + ) + + return playSongResponse { playerUuid = request.playerUuid songName = request.songName - startedPlaying = false + startedPlaying = request.queuePosition == 0 } - - JukeboxState.addToQueue( - player, - request.songName, - request.startTimeTicks, - request.queuePosition, - request.tagsList - ) - - return playSongResponse { - playerUuid = request.playerUuid - songName = request.songName - startedPlaying = request.queuePosition == 0 } - } - override suspend fun removeSong(request: JukeboxOuterClass.SongRemoveRequest): Empty { + override suspend fun removeSong(request: JukeboxOuterClass.SongRemoveRequest): Empty = handleRPC { val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() ?: return Empty.getDefaultInstance() - JukeboxState.removeByName(player, request.songName) + jukeboxHandler.removeByName(player, request.songName) return Empty.getDefaultInstance() } - override suspend fun removeSongs(request: JukeboxOuterClass.BatchSongRemoveRequest): Empty { + override suspend fun removeSongs(request: JukeboxOuterClass.BatchSongRemoveRequest): Empty = handleRPC { val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() ?: return Empty.getDefaultInstance() - JukeboxState.removeByTags(player, request.matchTagsList) + jukeboxHandler.removeByTags(player, request.matchTagsList) return Empty.getDefaultInstance() } - override suspend fun stopSong(request: JukeboxOuterClass.StopSongRequest): Empty { + override suspend fun stopSong(request: JukeboxOuterClass.StopSongRequest): Empty = handleRPC { val player = proxyServer.getPlayer(UUID.fromString(request.playerUuid)).getOrNull() ?: return Empty.getDefaultInstance() - JukeboxState.clearQueue(player) - Song.stop(player) + jukeboxHandler.clearQueue(player) + songPlayer.stop(player) return Empty.getDefaultInstance() }