From 560a06ef8563e4678621e31dc1eb958abf812df1 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 12 Apr 2025 13:40:22 +0200 Subject: [PATCH 1/7] feat: implement player group management and enhance server commands --- .../client/server/CloudClientServerManager.kt | 13 + .../paper/command/args/CloudServerArgument.kt | 73 ++++++ .../command/args/CloudServerGroupArgument.kt | 73 ++++++ .../args/OfflineCloudPlayerArgument.kt | 79 +++++++ .../command/args/OnlineCloudPlayerArgument.kt | 62 +++++ .../cloud/api/common/player/CloudPlayer.kt | 39 +-- .../api/common/player/CloudPlayerManager.kt | 2 + .../cloud/api/common/server/CloudServer.kt | 7 + .../api/common/server/CloudServerManager.kt | 16 ++ .../api/common/server/CommonCloudServer.kt | 2 + .../slne/surf/cloud/api/common/util/util.kt | 12 +- .../api/server/server/ServerCloudServer.kt | 4 +- .../surf/cloud/bukkit/CloudBukkitInstance.kt | 2 + .../bukkit/command/PaperCommandManager.kt | 17 ++ .../command/lastseen/LastSeenCommand.kt | 87 +++++++ .../bukkit/command/network/FindCommand.kt | 87 +++++++ .../bukkit/command/network/SendCommand.kt | 223 ++++++++++++++++++ .../bukkit/command/network/ServerCommand.kt | 101 ++++++++ .../command/playtime/PlaytimeCommand.kt | 66 ++++++ .../permission/CloudPermissionRegistry.kt | 16 ++ .../client/player/ClientCloudPlayerImpl.kt | 13 + .../server/ClientCloudServerManagerImpl.kt | 35 ++- .../cloud/core/common/coroutines/scopes.kt | 5 + .../protocol/running/RunningProtocols.kt | 2 + .../ServerboundPullPlayersToGroupPacket.kt | 22 ++ .../common/player/CloudPlayerManagerImpl.kt | 25 ++ .../common/player/CommonCloudPlayerImpl.kt | 2 +- .../common/server/CommonCloudServerImpl.kt | 19 ++ .../server/CommonCloudServerManagerImpl.kt | 20 +- .../player/StandaloneCloudPlayerImpl.kt | 59 ++--- .../server/StandaloneCloudServerImpl.kt | 83 +++++++ .../StandaloneCloudServerManagerImpl.kt | 68 ++++++ 32 files changed, 1282 insertions(+), 52 deletions(-) create mode 100644 surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-common/src/main/kotlin/dev/slne/surf/cloud/api/client/server/CloudClientServerManager.kt create mode 100644 surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerArgument.kt create mode 100644 surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerGroupArgument.kt create mode 100644 surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OfflineCloudPlayerArgument.kt create mode 100644 surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OnlineCloudPlayerArgument.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/FindCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/SendCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/ServerCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/playtime/PlaytimeCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt create mode 100644 surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPullPlayersToGroupPacket.kt diff --git a/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-common/src/main/kotlin/dev/slne/surf/cloud/api/client/server/CloudClientServerManager.kt b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-common/src/main/kotlin/dev/slne/surf/cloud/api/client/server/CloudClientServerManager.kt new file mode 100644 index 00000000..c3bc5ece --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-common/src/main/kotlin/dev/slne/surf/cloud/api/client/server/CloudClientServerManager.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.cloud.api.client.server + +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.api.common.util.annotation.InternalApi + +interface CloudClientServerManager : CloudServerManager { + fun currentServer(): CloudServer + + @OptIn(InternalApi::class) + companion object : + CloudClientServerManager by CloudServerManager.instance as CloudClientServerManager +} \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerArgument.kt b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerArgument.kt new file mode 100644 index 00000000..c296c33f --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerArgument.kt @@ -0,0 +1,73 @@ +package dev.slne.surf.cloud.api.client.paper.command.args + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.arguments.Argument +import dev.jorel.commandapi.arguments.ArgumentSuggestions +import dev.jorel.commandapi.arguments.CustomArgument +import dev.jorel.commandapi.arguments.StringArgument +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.api.common.util.annotation.InternalApi +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.future + + +@OptIn(InternalApi::class) +class CloudServerArgument(nodeName: String) : CustomArgument( + StringArgument(nodeName), + { info -> + val serverName = info.input + val server = CloudServerManager.getServerByNameUnsafe(serverName) ?: run { + throw CustomArgumentException.fromMessageBuilder( + MessageBuilder() + .append("Server '") + .appendArgInput() + .append("' not found") + ) + } + server + } +) { + init { + replaceSuggestions(ArgumentSuggestions.stringCollectionAsync { + scope.future { + CloudServerManager.retrieveAllServers() + .filterIsInstance() + .map { it.name } + } + }) + } + + private companion object { + private val log = logger() + private val scope = + CoroutineScope(Dispatchers.Default + CoroutineName("CloudServerSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable -> + log.atWarning() + .withCause(throwable) + .log("Failed to suggest cloud servers") + }) + } +} + +inline fun CommandTree.cloudServerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandTree = then(CloudServerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument<*>.cloudServerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): Argument<*> = then(CloudServerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.cloudServerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandAPICommand = withArguments(CloudServerArgument(nodeName).setOptional(optional).apply(block)) \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerGroupArgument.kt b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerGroupArgument.kt new file mode 100644 index 00000000..97180fb8 --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/CloudServerGroupArgument.kt @@ -0,0 +1,73 @@ +package dev.slne.surf.cloud.api.client.paper.command.args + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.arguments.Argument +import dev.jorel.commandapi.arguments.ArgumentSuggestions +import dev.jorel.commandapi.arguments.CustomArgument +import dev.jorel.commandapi.arguments.TextArgument +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.api.common.util.annotation.InternalApi +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.future + +@OptIn(InternalApi::class) +class CloudServerGroupArgument(nodeName: String) : CustomArgument( + TextArgument(nodeName), + { info -> + val groupName = info.currentInput + val exists = CloudServerManager.existsServerGroup(groupName) + if (!exists) { + throw CustomArgumentException.fromMessageBuilder( + MessageBuilder() + .append("Server group '") + .appendArgInput() + .append("' not found or no servers in this group are online!") + ) + } + groupName + } +) { + init { + replaceSuggestions(ArgumentSuggestions.stringCollectionAsync { + scope.future { + CloudServerManager.retrieveAllServers() + .filter { it.group.isNotEmpty() } + .map { it.group } + .distinct() + } + }) + } + + companion object { + private val log = logger() + private val scope = + CoroutineScope(Dispatchers.Default + CoroutineName("CloudServerGroupSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable -> + log.atWarning() + .withCause(throwable) + .log("Failed to suggest cloud server groups") + }) + } +} + +inline fun CommandTree.cloudServerGroupArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandTree = then(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument<*>.cloudServerGroupArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): Argument<*> = then(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.cloudServerGroupArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandAPICommand = withArguments(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block)) \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OfflineCloudPlayerArgument.kt b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OfflineCloudPlayerArgument.kt new file mode 100644 index 00000000..7eea811f --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OfflineCloudPlayerArgument.kt @@ -0,0 +1,79 @@ +package dev.slne.surf.cloud.api.client.paper.command.args + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.arguments.Argument +import dev.jorel.commandapi.arguments.ArgumentSuggestions +import dev.jorel.commandapi.arguments.AsyncOfflinePlayerArgument +import dev.jorel.commandapi.arguments.CustomArgument +import dev.slne.surf.cloud.api.client.paper.player.toCloudOfflinePlayer +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.cloud.api.common.player.OfflineCloudPlayer +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import org.bukkit.OfflinePlayer +import java.util.concurrent.CompletableFuture + +class OfflineCloudPlayerArgument(nodeName: String) : + CustomArgument, CompletableFuture>( + AsyncOfflinePlayerArgument(nodeName), + { info -> + scope.async { + try { + val player = info.currentInput.await() + player.toCloudOfflinePlayer() + } catch (e: RuntimeException) { + val cause = e.cause + val rootCause = if (cause is RuntimeException) cause.cause else cause + + info.sender.sendText { + error( + rootCause?.message + ?: "Unknown error occurred while fetching offline player" + ) + } + null + } + } + } + ) { + init { + includeSuggestions(ArgumentSuggestions.stringCollectionAsync { + scope.future { + CloudPlayerManager.getOnlinePlayers().map { it.name } + } + }) + } + + companion object { + private val log = logger() + private val scope = + CoroutineScope(Dispatchers.IO + CoroutineName("OfflineCloudPlayerArgument") + CoroutineExceptionHandler { _, throwable -> + log.atWarning() + .withCause(throwable) + .log("An error occurred in OfflineCloudPlayerArgument") + }) + } +} + +inline fun CommandTree.offlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandTree = then(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument<*>.offlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): Argument<*> = then(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.offlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandAPICommand = + withArguments(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OnlineCloudPlayerArgument.kt b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OnlineCloudPlayerArgument.kt new file mode 100644 index 00000000..5fb72571 --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-client/surf-cloud-api-client-paper/src/main/kotlin/dev/slne/surf/cloud/api/client/paper/command/args/OnlineCloudPlayerArgument.kt @@ -0,0 +1,62 @@ +package dev.slne.surf.cloud.api.client.paper.command.args + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.arguments.* +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.surfapi.core.api.util.logger +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.future + +private val log = logger() +private val scope = + CoroutineScope(Dispatchers.Default + CoroutineName("OnlineCloudPlayerSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable -> + log.atWarning() + .withCause(throwable) + .log("Failed to suggest online players") + }) + +class OnlineCloudPlayerArgument(nodeName: String) : CustomArgument( + StringArgument(nodeName), + { info -> + val playerName = info.input + CloudPlayerManager.getPlayer(playerName) ?: run { + throw CustomArgumentException.fromMessageBuilder( + MessageBuilder() + .append("Player '") + .appendArgInput() + .append("' not found") + ) + } + } +) { + init { + replaceSuggestions(ArgumentSuggestions.stringCollectionAsync { + scope.future { + CloudPlayerManager.getOnlinePlayers().map { it.name } + } + }) + } +} + +inline fun CommandTree.onlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandTree = then(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun Argument<*>.onlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): Argument<*> = then(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) + +inline fun CommandAPICommand.onlineCloudPlayerArgument( + nodeName: String, + optional: Boolean = false, + block: Argument<*>.() -> Unit = {} +): CommandAPICommand = withArguments(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block)) \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt index 703a8327..cd7c3a19 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt @@ -5,6 +5,7 @@ import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag import dev.slne.surf.cloud.api.common.player.teleport.TeleportLocation import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import java.net.Inet4Address @@ -19,6 +20,8 @@ import kotlin.time.Duration * it enables sending messages or components to the player. */ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but done correctly? + val name: String + override suspend fun latestIpAddress(): Inet4Address override suspend fun lastServerRaw(): String @@ -97,6 +100,9 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but */ suspend fun connectToServerOrQueue(group: String): ConnectionResult + fun isOnServer(server: CloudServer): Boolean + fun isInGroup(group: String): Boolean + /** * Disconnects the player from the network with a specified reason. * @@ -145,6 +151,8 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but vararg flags: TeleportFlag, ) = teleport(TeleportLocation(world, x, y, z, yaw, pitch), teleportCause, *flags) + suspend fun teleport(target: CloudPlayer): Boolean + override suspend fun displayName(): Component override suspend fun name(): String } @@ -152,20 +160,23 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but /** * Enum representing the result of a player's connection attempt to a server. */ -enum class ConnectionResultEnum { - SUCCESS, - SERVER_NOT_FOUND, - SERVER_FULL, - CATEGORY_FULL, - SERVER_OFFLINE, - ALREADY_CONNECTED, - CANNOT_SWITCH_PROXY, - OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET, - CANNOT_COMMUNICATE_WITH_PROXY, - CONNECTION_IN_PROGRESS, - CONNECTION_CANCELLED, - SERVER_DISCONNECTED, - CANNOT_CONNECT_TO_PROXY, +enum class ConnectionResultEnum( + val message: Component, + val isSuccess: Boolean = false +) { + SUCCESS(buildText { success("Du hast dich erfolgreich Verbunden.") }, isSuccess = true), + SERVER_NOT_FOUND(buildText { error("Der Server wurde nicht gefunden.") }), + SERVER_FULL(buildText { error("Der Server ist voll.") }), + CATEGORY_FULL(buildText { error("Die Kategorie ist voll.") }), + SERVER_OFFLINE(buildText { error("Der Server ist offline.") }), + ALREADY_CONNECTED(buildText { error("Du bist bereits mit diesem Server verbunden.") }), + CANNOT_SWITCH_PROXY(buildText { error("Du kannst nicht zu diesem Server wechseln, da dieser unter einem anderen Proxy läuft.") }), + OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET(buildText { error("Der Server kann das Transfer-Paket nicht akzeptieren.") }), + CANNOT_COMMUNICATE_WITH_PROXY(buildText { error("Der Proxy kann nicht erreicht werden.") }), + CONNECTION_IN_PROGRESS(buildText { error("Du versucht bereits eine Verbindung zu einem Server herzustellen.") }), + CONNECTION_CANCELLED(buildText { error("Die Verbindung wurde abgebrochen.") }), + SERVER_DISCONNECTED(buildText { error("Der Server hat die Verbindung getrennt.") }), + CANNOT_CONNECT_TO_PROXY(buildText { error("Der Proxy kann nicht erreicht werden.") }), } /** diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayerManager.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayerManager.kt index 6cf8826b..540e449d 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayerManager.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayerManager.kt @@ -27,6 +27,8 @@ interface CloudPlayerManager { */ fun getPlayer(uuid: UUID?): CloudPlayer? + fun getPlayer(name: String): CloudPlayer? + fun getOfflinePlayer(uuid: UUID): OfflineCloudPlayer fun getOnlinePlayers(): UserList diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServer.kt index 3ae90d3b..b0198a74 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServer.kt @@ -1,6 +1,11 @@ package dev.slne.surf.cloud.api.common.server +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import it.unimi.dsi.fastutil.objects.ObjectList +import net.kyori.adventure.text.Component import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Unmodifiable /** * Represents a backend server within the cloud infrastructure. @@ -19,4 +24,6 @@ interface CloudServer : CommonCloudServer { * When enabled, only players on the allowlist (whitelist) can join the server. */ val allowlist: Boolean + + suspend fun pullPlayers(players: Collection): @Unmodifiable ObjectList> } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt index 9dde137a..067dbbbc 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt @@ -1,10 +1,14 @@ package dev.slne.surf.cloud.api.common.server +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.util.annotation.InternalApi import dev.slne.surf.surfapi.core.api.util.requiredService import it.unimi.dsi.fastutil.objects.ObjectCollection import it.unimi.dsi.fastutil.objects.ObjectList +import net.kyori.adventure.text.Component import org.jetbrains.annotations.ApiStatus.NonExtendable +import org.jetbrains.annotations.Unmodifiable /** @@ -48,6 +52,12 @@ interface CloudServerManager { */ suspend fun retrieveServerByName(name: String): CommonCloudServer? + @InternalApi + fun getServerByNameUnsafe(name: String): CloudServer? + + @InternalApi + fun existsServerGroup(name: String): Boolean + /** * Retrieves all servers in a specified category. * @@ -58,6 +68,12 @@ interface CloudServerManager { suspend fun retrieveAllServers(): ObjectCollection + suspend fun pullPlayersToGroup( + group: String, + players: Collection + ): @Unmodifiable ObjectList> + + companion object : CloudServerManager by INSTANCE { @InternalApi val instance = INSTANCE diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt index 1419afd5..a26c5f92 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt @@ -102,6 +102,8 @@ interface CommonCloudServer : ForwardingAudience { suspend fun sendAll(category: String): BatchTransferResult + fun isInGroup(group: String): Boolean + /** * Shuts down the server. * diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt index d80e1b21..441ce012 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt @@ -1,5 +1,8 @@ package dev.slne.surf.cloud.api.common.util +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.lang.reflect.Method import java.nio.file.Path @@ -7,10 +10,8 @@ import java.util.* import java.util.function.ToIntFunction import kotlin.io.path.deleteIfExists import kotlin.io.path.exists -import kotlin.io.path.fileVisitor import kotlin.io.path.isRegularFile import kotlin.io.path.moveTo -import kotlin.io.path.visitFileTree import kotlin.reflect.jvm.kotlinFunction const val LINEAR_LOOKUP_THRESHOLD = 8 @@ -143,4 +144,9 @@ private fun createFileDeletedCheck(path: Path): () -> Boolean = { !path.exists() } -fun Method.isSuspending() = kotlinFunction?.isSuspend == true \ No newline at end of file +fun Method.isSuspending() = kotlinFunction?.isSuspend == true + +suspend inline fun Iterable.mapAsync(crossinline transform: suspend (T) -> R): List> = + coroutineScope { + map { async { transform(it) } } + } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-server/src/main/kotlin/dev/slne/surf/cloud/api/server/server/ServerCloudServer.kt b/surf-cloud-api/surf-cloud-api-server/src/main/kotlin/dev/slne/surf/cloud/api/server/server/ServerCloudServer.kt index 5e7cd264..f46535df 100644 --- a/surf-cloud-api/surf-cloud-api-server/src/main/kotlin/dev/slne/surf/cloud/api/server/server/ServerCloudServer.kt +++ b/surf-cloud-api/surf-cloud-api-server/src/main/kotlin/dev/slne/surf/cloud/api/server/server/ServerCloudServer.kt @@ -4,4 +4,6 @@ import dev.slne.surf.cloud.api.common.server.CloudServer import org.jetbrains.annotations.ApiStatus @ApiStatus.NonExtendable -interface ServerCloudServer : ServerCommonCloudServer, CloudServer \ No newline at end of file +interface ServerCloudServer : ServerCommonCloudServer, CloudServer { + val expectedPlayers: Int +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/CloudBukkitInstance.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/CloudBukkitInstance.kt index e48965b2..bed90405 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/CloudBukkitInstance.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/CloudBukkitInstance.kt @@ -2,6 +2,7 @@ package dev.slne.surf.cloud.bukkit import com.google.auto.service.AutoService import dev.slne.surf.cloud.api.common.CloudInstance +import dev.slne.surf.cloud.bukkit.command.PaperCommandManager import dev.slne.surf.cloud.bukkit.listener.ListenerManager import dev.slne.surf.cloud.bukkit.netty.BukkitNettyManager import dev.slne.surf.cloud.bukkit.processor.BukkitListenerProcessor @@ -19,6 +20,7 @@ class CloudBukkitInstance : ClientCommonCloudInstance(BukkitNettyManager) { override suspend fun onEnable() { super.onEnable() + PaperCommandManager.registerCommands() bean().registerListeners() ListenerManager.registerListeners() } diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt new file mode 100644 index 00000000..492d96b6 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt @@ -0,0 +1,17 @@ +package dev.slne.surf.cloud.bukkit.command + +import dev.slne.surf.cloud.bukkit.command.lastseen.lastSeenCommand +import dev.slne.surf.cloud.bukkit.command.network.findCommand +import dev.slne.surf.cloud.bukkit.command.network.sendCommand +import dev.slne.surf.cloud.bukkit.command.network.serverCommand +import dev.slne.surf.cloud.bukkit.command.playtime.playtimeCommand + +object PaperCommandManager { + fun registerCommands() { + findCommand() + serverCommand() + sendCommand() + playtimeCommand() + lastSeenCommand() + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt new file mode 100644 index 00000000..c5aafd7b --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt @@ -0,0 +1,87 @@ +package dev.slne.surf.cloud.bukkit.command.lastseen + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.slne.surf.cloud.api.client.paper.command.args.offlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.OfflineCloudPlayer +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder +import org.bukkit.command.CommandSender +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder + +private val dateTimeFormatter = DateTimeFormatterBuilder() + .append(DateTimeFormatter.ofPattern("dd.MM.yyyy")) + .appendLiteral(" um ") + .append(DateTimeFormatter.ofPattern("HH:mm:ss")) + .appendLiteral(" Uhr") + .toFormatter() + .withZone(ZoneId.systemDefault()) + +fun lastSeenCommand() = commandTree("lastseen") { + withPermission(CloudPermissionRegistry.LAST_SEEN_COMMAND) + offlineCloudPlayerArgument("player") { + anyExecutor { sender, args -> + val player: OfflineCloudPlayer? by args + player?.let { sendLastSeen(sender, it) } + } + } +} + +private fun sendLastSeen(sender: CommandSender, player: OfflineCloudPlayer) = plugin.launch { + val lastSeen = player.lastSeen() + val onlinePlayer = player.player + + when { + lastSeen == null -> sender.sendNeverSeenMessage(player) + onlinePlayer?.connected == true -> sender.sendOnlineMessage(player, onlinePlayer) + else -> sender.sendLastSeenMessage(player, lastSeen) + } +} + +private suspend fun CommandSender.sendNeverSeenMessage(player: OfflineCloudPlayer) = sendText { + appendPrefix() + error("Der Spielende ") + appendPlayerInfo(player) + error(" wurde noch nie gesehen.") +} + +private suspend fun CommandSender.sendOnlineMessage( + player: OfflineCloudPlayer, + onlinePlayer: CloudPlayer +) = sendText { + appendPrefix() + info("Der Spielende ") + appendPlayerInfo(player) + info(" ist seit ") + variableValue(onlinePlayer.currentSessionDuration().toString()) + info(" auf dem Server ") + variableValue(onlinePlayer.lastServerRaw()) + info(" online.") +} + +private suspend fun CommandSender.sendLastSeenMessage( + player: OfflineCloudPlayer, + lastSeen: ZonedDateTime +) = sendText { + appendPrefix() + info("Der Spielende ") + appendPlayerInfo(player) + info(" wurde zuletzt am ") + variableValue(dateTimeFormatter.format(lastSeen)) + info(" gesehen.") +} + +private suspend fun SurfComponentBuilder.appendPlayerInfo(player: OfflineCloudPlayer) = + appendAsync { + variableValue(player.name() ?: player.uuid.toString()) + hoverEvent(buildText { variableValue(player.uuid.toString()) }) + } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/FindCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/FindCommand.kt new file mode 100644 index 00000000..5b7b0694 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/FindCommand.kt @@ -0,0 +1,87 @@ +package dev.slne.surf.cloud.bukkit.command.network + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.toCloudPlayer +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import kotlinx.coroutines.async +import net.kyori.adventure.text.event.ClickEvent + +fun findCommand() = commandTree("find") { + withPermission(CloudPermissionRegistry.FIND_COMMAND) + onlineCloudPlayerArgument("player") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + + plugin.launch { + val serverDeferred = async { player.lastServer() } + val displayNameDeferred = async { player.displayName() } + + val server = serverDeferred.await() ?: run { + sender.sendText { + error("Der Spielende ") + append(displayNameDeferred.await()) + error(" ist nicht auf einem Server.") + } + return@launch + } + + val displayName = displayNameDeferred.await() + + sender.sendText { + appendPrefix() + info("Der Spielende ") + append(displayName) + info(" befindet sich auf dem Server ") + append { + variableValue("${server.name} (${server.group})") + } + + if (sender.hasPermission(CloudPermissionRegistry.FIND_COMMAND_TELEPORT)) { + hoverEvent(buildText { + info("Klicke, um dich zu ") + append(displayName) + info(" zu teleportieren.") + }) + + clickEvent(ClickEvent.callback { clicker -> + clicker.sendText { + appendPrefix() + info("Du wirst zu ") + append(displayName) + info(" teleportiert...") + } + + plugin.launch { + val teleported = clicker.toCloudPlayer()?.teleport(player) == true + + if (!teleported) { + clicker.sendText { + appendPrefix() + error("Teleportation zu ") + append(displayName) + error(" fehlgeschlagen.") + } + } else { + clicker.sendText { + appendPrefix() + success("Du wurdest erfolgreich zu ") + append(displayName) + success(" teleportiert.") + } + } + } + }) + } + } + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/SendCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/SendCommand.kt new file mode 100644 index 00000000..837527ec --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/SendCommand.kt @@ -0,0 +1,223 @@ +package dev.slne.surf.cloud.bukkit.command.network + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.CommandAPI +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.literalArgument +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument +import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument +import dev.slne.surf.cloud.api.client.server.CloudClientServerManager +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import it.unimi.dsi.fastutil.objects.ObjectList +import org.bukkit.command.CommandSender + +fun sendCommand() = commandTree("send") { + literalArgument("player") { + onlineCloudPlayerArgument("player") { + literalArgument("toServer") { + cloudServerArgument("server") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + val server: CloudServer by args + sendPlayersToServer(sender, server, listOf(player)) + } + } + } + + literalArgument("toGroup") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + val group: String by args + sendPlayersToGroup(sender, group, listOf(player)) + } + } + } + } + } + + literalArgument("all") { + literalArgument("toServer") { + cloudServerArgument("server") { + anyExecutor { sender, args -> + val server: CloudServer by args + val players = CloudPlayerManager.getOnlinePlayers() + .filterNot { it.isOnServer(server) } + sendPlayersToServer(sender, server, players) + } + } + } + literalArgument("toGroup") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val group: String by args + val players = CloudPlayerManager.getOnlinePlayers() + .filterNot { it.isInGroup(group) } + sendPlayersToGroup(sender, group, players) + } + } + } + } + + literalArgument("current") { + literalArgument("toServer") { + cloudServerArgument("server") { + anyExecutor { sender, args -> + val server: CloudServer by args + val current = CloudClientServerManager.currentServer() + + if (current == server) { + throw CommandAPI.failWithString("Cannot send players to the same server.") + } + + sendPlayersToServer(sender, server, current.users) + } + } + } + literalArgument("toGroup") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val group: String by args + val current = CloudClientServerManager.currentServer() + + if (current.isInGroup(group)) { + throw CommandAPI.failWithString("Cannot send players to the same group.") + } + + sendPlayersToGroup(sender, group, current.users) + } + } + } + } + + literalArgument("server") { + cloudServerArgument("from") { + literalArgument("toServer") { + cloudServerArgument("to") { + anyExecutor { sender, args -> + val from: CloudServer by args + val to: CloudServer by args + + sendPlayersToServer(sender, to, from.users) + } + } + } + literalArgument("toGroup") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val server: CloudServer by args + val group: String by args + + sendPlayersToGroup(sender, group, server.users) + } + } + } + } + } + + literalArgument("group") { + cloudServerGroupArgument("from") { + literalArgument("toServer") { + cloudServerArgument("to") { + anyExecutor { sender, args -> + val from: String by args + val to: CloudServer by args + + sendPlayersToServer( + sender, + to, + CloudPlayerManager.getOnlinePlayers().filter { it.isInGroup(from) }) + } + } + } + literalArgument("toGroup") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val from: String by args + val group: String by args + + if (from.equals(group, ignoreCase = true)) { + throw CommandAPI.failWithString("Cannot send players to the same group.") + } + + sendPlayersToGroup( + sender, + group, + CloudPlayerManager.getOnlinePlayers().filter { it.isInGroup(from) }) + } + } + } + } + } +} + +private fun sendPlayersToServer( + sender: CommandSender, + server: CloudServer, + players: Collection +) = plugin.launch { + sender.sendText { + appendPrefix() + variableValue(players.size) + info(" Spielende werden verschickt...") + } + + val results = server.pullPlayers(players) + handleSendResults(sender, players, results) +} + +private fun sendPlayersToGroup( + sender: CommandSender, + group: String, + players: Collection +) = plugin.launch { + sender.sendText { + appendPrefix() + variableValue(players.size) + info(" Spielende werden verschickt...") + } + + val results = CloudServerManager.pullPlayersToGroup(group, players) + handleSendResults(sender, players, results) +} + +private fun handleSendResults( + sender: CommandSender, + players: Collection, + results: ObjectList> +) { + var count = 0 + results.forEach { (player, result) -> + if (result.isSuccess) { + count++ + } else { + player.sendMessage(result.message) + } + } + + if (count == 0) { + sender.sendText { + appendPrefix() + error("Niemand wurde verschickt.") + } + } else if (count < players.size) { + sender.sendText { + appendPrefix() + error("Nur $count/${players.size} Spielende wurden verschickt.") + } + } else { + sender.sendText { + appendPrefix() + success("Alle ${players.size} Spielenden wurden verschickt.") + } + } +} diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/ServerCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/ServerCommand.kt new file mode 100644 index 00000000..c4d2d925 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/ServerCommand.kt @@ -0,0 +1,101 @@ +package dev.slne.surf.cloud.bukkit.command.network + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.arguments.CustomArgument.CustomArgumentException +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.playerExecutor +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum.* +import dev.slne.surf.cloud.api.common.player.toCloudPlayer +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun serverCommand() = commandTree("server") { + withPermission(CloudPermissionRegistry.SERVER_COMMAND) + cloudServerArgument("server") { + playerExecutor { sender, args -> + val server: CloudServer by args + val cloudPlayer = sender.toCloudPlayer() ?: return@playerExecutor + + if (cloudPlayer.isOnServer(server)) { + throw CustomArgumentException.fromString("Du befindest dich bereits auf dem Server!") + } + + if (!sender.hasPermission(CloudPermissionRegistry.ALL_SERVER_PERMISSION) + && !sender.hasPermission(CloudPermissionRegistry.SPECIFIC_SERVER_PERMISSION_PREFIX + server.name) + ) { + throw CustomArgumentException.fromString("Du hast keine Berechtigung, um auf diesen Server zu wechseln!") + } + + sender.sendText { + appendPrefix() + info("Du wirst mit dem Server ") + append { + variableValue("${server.name} (${server.group})") + } + info(" verbunden...") + } + + plugin.launch { + val (result, _) = cloudPlayer.connectToServer(server) + + sender.sendText { + appendPrefix() + when (result) { + SUCCESS -> append { + success("Du wurdest erfolgreich mit dem Server ") + variableValue("${server.name} (${server.group})") + success(" verbunden.") + } + + SERVER_FULL, CATEGORY_FULL -> append { + error("Der Server ") + variableValue("${server.name} (${server.group})") + error(" ist voll.") + } + + SERVER_OFFLINE -> append { + error("Der Server ") + variableValue("${server.name} (${server.group})") + error(" ist offline.") + } + + CANNOT_SWITCH_PROXY -> append { + error("Der Server ") + variableValue("${server.name} (${server.group})") + error(" befindet sich unter einem anderen Proxy.") + } + + CANNOT_COMMUNICATE_WITH_PROXY -> append { + error("Der Server ") + variableValue("${server.name} (${server.group})") + error(" kann nicht mit dem Proxy kommunizieren.") + } + + CONNECTION_IN_PROGRESS -> append { + error("Du wechselst bereits einen Server.") + } + + CONNECTION_CANCELLED -> append { + error("Die Verbindung wurde abgebrochen.") + } + + SERVER_DISCONNECTED -> append { + error("Die Verbindung zum Server ") + variableValue("${server.name} (${server.group})") + error(" wurde getrennt.") + } + + SERVER_NOT_FOUND -> throw AssertionError("Server not found") + ALREADY_CONNECTED -> throw AssertionError("Already connected") + OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET -> throw AssertionError() + CANNOT_CONNECT_TO_PROXY -> throw AssertionError("Cannot connect to proxy") + } + } + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/playtime/PlaytimeCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/playtime/PlaytimeCommand.kt new file mode 100644 index 00000000..b4eb15e9 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/playtime/PlaytimeCommand.kt @@ -0,0 +1,66 @@ +package dev.slne.surf.cloud.bukkit.command.playtime + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.playerExecutor +import dev.slne.surf.cloud.api.client.paper.command.args.offlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.OfflineCloudPlayer +import dev.slne.surf.cloud.api.common.player.toCloudPlayer +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import org.bukkit.command.CommandSender + +fun playtimeCommand() = commandTree("playtime") { + withPermission(CloudPermissionRegistry.PLAYTIME_COMMAND) + + playerExecutor { sender, args -> + sendPlaytime(sender, sender.toCloudPlayer() ?: throw AssertionError("Player is null")) + } + offlineCloudPlayerArgument("player") { + withPermission(CloudPermissionRegistry.PLAYTIME_COMMAND_OTHER) + anyExecutor { sender, args -> + val player: OfflineCloudPlayer? by args + player?.let { sendPlaytime(sender, it) } + } + } +} + +private fun sendPlaytime(sender: CommandSender, player: OfflineCloudPlayer) = plugin.launch { + val playtime = player.playtime() + val complete = playtime.sumPlaytimes() + val playtimeMap = playtime.playtimePerCategoryPerServer() + + sender.sendText { + appendPrefix() + info("Spielzeit für ") + variableValue("${player.name()} (${player.uuid})") + appendNewPrefixedLine() + appendNewPrefixedLine { + variableKey("Total") + spacer(": ") + variableValue(complete.toString()) + } + appendNewPrefixedLine() + for ((group, groupServer) in playtimeMap) { + appendNewPrefixedLine { + spacer("- ") + variableKey(group) + spacer(": ") + variableValue(playtime.sumByCategory(group).toString()) + + for ((serverName, playtime) in groupServer) { + appendNewPrefixedLine { + text(" ") + variableKey(serverName) + spacer(": ") + variableValue(playtime.toString()) + } + } + appendNewPrefixedLine() + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt new file mode 100644 index 00000000..1ae7ed4b --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.cloud.bukkit.permission + +import dev.slne.surf.surfapi.bukkit.api.permission.PermissionRegistry + +object CloudPermissionRegistry: PermissionRegistry() { + private const val COMMAND_PREFIX = "surfcloud.command" + + val FIND_COMMAND = create("$COMMAND_PREFIX.find") + val FIND_COMMAND_TELEPORT = create("$COMMAND_PREFIX.find.teleport") + val SERVER_COMMAND = create("$COMMAND_PREFIX.server") + const val SPECIFIC_SERVER_PERMISSION_PREFIX = "$COMMAND_PREFIX.server." + const val ALL_SERVER_PERMISSION = "$COMMAND_PREFIX.server.*" + val PLAYTIME_COMMAND = create("$COMMAND_PREFIX.playtime") + val PLAYTIME_COMMAND_OTHER = create("$COMMAND_PREFIX.playtime.other") + val LAST_SEEN_COMMAND = create("$COMMAND_PREFIX.lastseen") +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt index 990cffb8..9d3c1e53 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt @@ -12,6 +12,7 @@ import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag import dev.slne.surf.cloud.api.common.player.teleport.TeleportLocation import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.core.client.server.serverManagerImpl import dev.slne.surf.cloud.core.client.util.luckperms import dev.slne.surf.cloud.core.common.netty.network.protocol.running.* import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataPacket.DataRequestType @@ -81,6 +82,18 @@ abstract class ClientCloudPlayerImpl(uuid: UUID) : return request(DataRequestType.PLAYTIME_SESSION).playtime } + override fun isOnServer(server: CloudServer): Boolean { + return server.uid == serverUid + } + + override fun isInGroup(group: String): Boolean { + val currentServer = serverUid + return currentServer != null && serverManagerImpl.getServerByIdUnsafe(currentServer)?.group?.equals( + group, + ignoreCase = true + ) == true + } + override suspend fun withPersistentData(block: PersistentPlayerDataContainer.() -> R): R { val response = ServerboundRequestPlayerPersistentDataContainer(uuid).fireAndAwaitOrThrow() diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/server/ClientCloudServerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/server/ClientCloudServerManagerImpl.kt index ffa02cb1..732f4916 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/server/ClientCloudServerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/server/ClientCloudServerManagerImpl.kt @@ -1,17 +1,50 @@ package dev.slne.surf.cloud.core.client.server import com.google.auto.service.AutoService +import dev.slne.surf.cloud.api.client.netty.packet.fireAndAwaitOrThrow +import dev.slne.surf.cloud.api.client.server.CloudClientServerManager +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager import dev.slne.surf.cloud.api.common.server.CommonCloudServer +import dev.slne.surf.cloud.core.common.data.CloudPersistentData import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ClientInformation +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundPullPlayersToGroupPacket import dev.slne.surf.cloud.core.common.server.CommonCloudServerImpl import dev.slne.surf.cloud.core.common.server.CommonCloudServerManagerImpl +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.objects.ObjectList +import org.jetbrains.annotations.Unmodifiable +import kotlin.time.Duration @AutoService(CloudServerManager::class) -class ClientCloudServerManagerImpl : CommonCloudServerManagerImpl() { +class ClientCloudServerManagerImpl : CommonCloudServerManagerImpl(), + CloudClientServerManager { + fun updateServerInformationNow(uid: Long, information: ClientInformation) { (servers[uid] as? CommonCloudServerImpl)?.information = information } + + override fun currentServer(): CloudServer { + return getServerByIdUnsafe(CloudPersistentData.SERVER_ID) as? CloudServer + ?: throw AssertionError("Current server not found") + } + + override suspend fun pullPlayersToGroup( + group: String, + players: Collection + ): @Unmodifiable ObjectList> { + return ServerboundPullPlayersToGroupPacket( + group, + players.map { it.uuid } + ).fireAndAwaitOrThrow(Duration.INFINITE) + .results + .mapNotNullTo(mutableObjectListOf()) { (uuid, result) -> + CloudPlayerManager.getPlayer(uuid)?.let { it to result } + } + } } val serverManagerImpl get() = CloudServerManager.instance as ClientCloudServerManagerImpl \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/coroutines/scopes.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/coroutines/scopes.kt index 47a86904..e674505c 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/coroutines/scopes.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/coroutines/scopes.kt @@ -125,4 +125,9 @@ object PlayerDatabaseScope : BaseScope( object PlayerPlaytimeScope : BaseScope( dispatcher = Dispatchers.IO, name = "player-playtime" +) + +object CloudServerCleanupScope : BaseScope( + dispatcher = Dispatchers.Default, + name = "cloud-server-cleanup" ) \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt index 19fc2cad..19ad109d 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt @@ -53,6 +53,7 @@ object RunningProtocols { .addPacket(RequestOfflineDisplayNamePacket.STREAM_CODEC) .addPacket(ClientboundBatchUpdateServer.STREAM_CODEC) .addPacket(ServerboundRequestPlayerDataResponse.STREAM_CODEC) + .addPacket(PullPlayersToGroupResponsePacket::class.createCodec()) } val CLIENTBOUND by lazy { CLIENTBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } @@ -98,6 +99,7 @@ object RunningProtocols { .addPacket(RequestOfflineDisplayNamePacket.STREAM_CODEC) .addPacket(ServerboundRequestPlayerDataPacket.STREAM_CODEC) .addPacket(ServerboundUpdateAFKState::class.createCodec()) + .addPacket(ServerboundPullPlayersToGroupPacket::class.createCodec()) } val SERVERBOUND by lazy { SERVERBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPullPlayersToGroupPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPullPlayersToGroupPacket.kt new file mode 100644 index 00000000..a5fe588f --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPullPlayersToGroupPacket.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.cloud.core.common.netty.network.protocol.running + +import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow +import dev.slne.surf.cloud.api.common.netty.packet.RespondingNettyPacket +import dev.slne.surf.cloud.api.common.netty.packet.ResponseNettyPacket +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.util.* + +@SurfNettyPacket("cloud:serverbound:pull_players_to_group", PacketFlow.SERVERBOUND) +@Serializable +class ServerboundPullPlayersToGroupPacket( + val group: String, + val players: Collection<@Contextual UUID> +) : RespondingNettyPacket() + +@SurfNettyPacket("cloud:response:pull_players_to_group_response", PacketFlow.CLIENTBOUND) +@Serializable +class PullPlayersToGroupResponsePacket(val results: List>) : + ResponseNettyPacket() \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt index 01cf98e3..b6eba0be 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt @@ -2,7 +2,9 @@ package dev.slne.surf.cloud.core.common.player import dev.slne.surf.cloud.api.common.event.player.connection.CloudPlayerConnectToNetworkEvent import dev.slne.surf.cloud.api.common.event.player.connection.CloudPlayerDisconnectFromNetworkEvent +import dev.slne.surf.cloud.api.common.player.CloudPlayer import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.cloud.api.common.server.CloudServerManager import dev.slne.surf.cloud.api.common.server.UserList import dev.slne.surf.cloud.api.common.server.UserListImpl import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf @@ -10,6 +12,8 @@ import dev.slne.surf.cloud.api.common.util.synchronize import dev.slne.surf.cloud.core.common.util.publish import dev.slne.surf.surfapi.core.api.util.logger import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import org.jetbrains.annotations.MustBeInvokedByOverriders import java.net.Inet4Address import java.util.* @@ -22,6 +26,9 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa return players[uuid] } + override fun getPlayer(name: String): CloudPlayer? = + players.values.find { it.name.equals(name, ignoreCase = true) } + abstract suspend fun createPlayer( uuid: UUID, name: String, @@ -113,10 +120,28 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa } open suspend fun onServerConnect(uuid: UUID, player: P, serverUid: Long) { + coroutineScope { + launch { + val userList = CloudServerManager.retrieveServerById(serverUid)?.users ?: return@launch + if (userList !is UserListImpl || !userList.add(uuid)) { + log.atWarning() + .log("Failed to add player to server user list: $userList") + } + } + } } @MustBeInvokedByOverriders open suspend fun onServerDisconnect(uuid: UUID, player: P, serverUid: Long) { + coroutineScope { + launch { + val userList = CloudServerManager.retrieveServerById(serverUid)?.users ?: return@launch + if (userList !is UserListImpl || !userList.remove(uuid)) { + log.atWarning() + .log("Failed to remove player from server user list: $userList") + } + } + } } @MustBeInvokedByOverriders diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt index b53f250b..f08ae76e 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt @@ -8,7 +8,7 @@ import dev.slne.surf.cloud.api.common.server.CloudServerManager import java.time.ZonedDateTime import java.util.* -abstract class CommonCloudPlayerImpl(uuid: UUID) : CommonOfflineCloudPlayerImpl(uuid), CloudPlayer { +abstract class CommonCloudPlayerImpl(uuid: UUID, override val name: String) : CommonOfflineCloudPlayerImpl(uuid), CloudPlayer { override suspend fun connectToServer( group: String, diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt index 80a9868f..7b5c226d 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt @@ -48,6 +48,10 @@ abstract class CommonCloudServerImpl( override suspend fun sendAll(category: String): BatchTransferResult = executeBatchTransfer { PlayerBatchTransferScope.async { it.connectToServer(category) } } + override fun isInGroup(group: String): Boolean { + return group.equals(group, ignoreCase = true) + } + override val maxPlayerCount get() = information.maxPlayerCount override val currentPlayerCount get() = users.size override val state get() = information.state @@ -58,7 +62,22 @@ abstract class CommonCloudServerImpl( coreCloudInstance.shutdownServer(this) } + + override fun toString(): String { return "CloudServerImpl(group='$group', uid=$uid, name='$name, users=$users, information=$information, maxPlayerCount=$maxPlayerCount, currentPlayerCount=$currentPlayerCount, state=$state)" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CommonCloudServerImpl) return false + + if (uid != other.uid) return false + + return true + } + + override fun hashCode(): Int { + return uid.hashCode() + } } diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt index d0363727..f4630218 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt @@ -1,7 +1,9 @@ package dev.slne.surf.cloud.core.common.server +import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager import dev.slne.surf.cloud.api.common.server.CommonCloudServer +import dev.slne.surf.cloud.api.common.util.annotation.InternalApi import dev.slne.surf.cloud.api.common.util.mutableLong2ObjectMapOf import dev.slne.surf.cloud.api.common.util.mutableObjectListOf import dev.slne.surf.cloud.api.common.util.synchronize @@ -20,6 +22,8 @@ abstract class CommonCloudServerManagerImpl : CloudServer open suspend fun unregisterServer(uid: Long) = serversMutex.withLock { servers.remove(uid) } + fun getServerByIdUnsafe(uid: Long): S? = servers[uid] + override suspend fun retrieveServerById(id: Long): S? = serversMutex.withLock { servers[id] } override suspend fun retrieveServerByCategoryAndName( @@ -27,18 +31,28 @@ abstract class CommonCloudServerManagerImpl : CloudServer name: String ) = serversMutex.withLock { servers.values.asSequence() - .filter { it.group == category && it.name == name } + .filter { it.group.equals(category, true) && it.name.equals(name, true) } .minByOrNull { it.currentPlayerCount } } override suspend fun retrieveServerByName(name: String) = serversMutex.withLock { servers.values.asSequence() - .filter { it.name == name } + .filter { it.name.equals(name, true) } .minByOrNull { it.currentPlayerCount } } + override fun getServerByNameUnsafe(name: String): CloudServer? = + servers.values.asSequence() + .filter { it.name.equals(name, ignoreCase = true) } + .minByOrNull { it.currentPlayerCount } as? CloudServer + + + @InternalApi + override fun existsServerGroup(name: String): Boolean = + servers.values.any { it.group.equals(name, ignoreCase = true) } + override suspend fun retrieveServersByCategory(category: String) = serversMutex.withLock { - servers.values.filterTo(mutableObjectListOf()) { it.group == category } + servers.values.filterTo(mutableObjectListOf()) { it.group.equals(category, true) } } suspend fun batchUpdateServer(update: List) { diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt index 183970f5..d749d0e1 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt @@ -71,7 +71,7 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre get() = server ?: proxyServer ?: error("Player is not connected to a server") @Volatile - private var connecting = false + var connecting = false @Volatile var connectionQueueCallback: CompletableDeferred? = null @@ -79,7 +79,6 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre @Volatile var connectingToServer: StandaloneCloudServerImpl? = null - private set private val ppdc = PersistentPlayerDataContainerImpl() @@ -196,33 +195,35 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre override suspend fun connectToServer(server: CloudServer): ConnectionResult { check(server is StandaloneCloudServerImpl) { "Server must be a StandaloneCloudServerImpl" } - if (connecting) { - return ConnectionResultEnum.CONNECTION_IN_PROGRESS to null - } - - connecting = true - - // is user connected through proxy? - // yes - // -> Is new server managed by the same proxy? - // yes - // -> Send connect packet - // no - // -> Return ConnectionResult.CANNOT_SWITCH_PROXY - // no - // -> try send transfer packet - // -> if failed, return ConnectionResult.OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET - // -> if succeeded, return ConnectionResult.SUCCESS - - val proxy = proxyServer - if (proxy != null) { - connectingToServer = server - return switchServerUnderSameProxy(proxy, server).also { - connecting = false; connectingToServer = null - } - } - - error("NOT SUPPORTED") + return server.pullPlayer(this) to null + +// if (connecting) { +// return ConnectionResultEnum.CONNECTION_IN_PROGRESS to null +// } +// +// connecting = true +// +// // is user connected through proxy? +// // yes +// // -> Is new server managed by the same proxy? +// // yes +// // -> Send connect packet +// // no +// // -> Return ConnectionResult.CANNOT_SWITCH_PROXY +// // no +// // -> try send transfer packet +// // -> if failed, return ConnectionResult.OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET +// // -> if succeeded, return ConnectionResult.SUCCESS +// +// val proxy = proxyServer +// if (proxy != null) { +// connectingToServer = server +// return switchServerUnderSameProxy(proxy, server).also { +// connecting = false; connectingToServer = null +// } +// } +// +// error("NOT SUPPORTED") // return switchServerUnderNoProxy(server).also { connecting = false } } diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt index df7a6e21..42e5b2f3 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt @@ -1,10 +1,28 @@ package dev.slne.surf.cloud.standalone.server +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import dev.slne.surf.cloud.api.common.util.emptyObjectList +import dev.slne.surf.cloud.api.common.util.mapAsync +import dev.slne.surf.cloud.api.common.util.mutableObjectSetOf +import dev.slne.surf.cloud.api.common.util.objectListOf import dev.slne.surf.cloud.api.server.server.ServerCloudServer import dev.slne.surf.cloud.api.server.server.ServerCommonCloudServer +import dev.slne.surf.cloud.core.common.coroutines.CloudServerCleanupScope import dev.slne.surf.cloud.core.common.netty.network.ConnectionImpl +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ClientboundTransferPlayerPacket +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundTransferPlayerPacketResponse.Status import dev.slne.surf.cloud.core.common.server.CloudServerImpl +import dev.slne.surf.cloud.standalone.player.StandaloneCloudPlayerImpl import dev.slne.surf.cloud.standalone.server.queue.SingleServerQueue +import it.unimi.dsi.fastutil.objects.ObjectList +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import net.kyori.adventure.text.Component +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds class StandaloneCloudServerImpl( uid: Long, @@ -16,9 +34,74 @@ class StandaloneCloudServerImpl( init { wrapper = this + startCleanupTask() } + val connectingPlayers = mutableObjectSetOf() val queue = SingleServerQueue(this) + + override val expectedPlayers: Int + get() = currentPlayerCount + connectingPlayers.size + + private fun startCleanupTask() = CloudServerCleanupScope.launch { + while (isActive) { + connectingPlayers.removeIf { !it.connecting || it.connectingToServer != this } + delay(5.seconds) + } + } + + suspend fun pullPlayer(player: CloudPlayer): ConnectionResultEnum { + require(player is StandaloneCloudPlayerImpl) { "Player must be StandaloneCloudPlayerImpl" } + if (player.connecting) return ConnectionResultEnum.CONNECTION_IN_PROGRESS + player.connecting = true + player.connectingToServer = this + connectingPlayers.add(player) + + val proxy = player.proxyServer + if (proxy != null) { + return pullPlayerThroughProxy(player, proxy).also { + connectingPlayers.remove(player) + player.connecting = false + player.connectingToServer = null + } + } + + TODO("Use transfer packet") + } + + private suspend fun pullPlayerThroughProxy( + player: StandaloneCloudPlayerImpl, + proxy: StandaloneProxyCloudServerImpl + ): ConnectionResultEnum { + val result = ClientboundTransferPlayerPacket(player.uuid, proxy.connection.virtualHost) + .fireAndAwait(proxy.connection, Duration.INFINITE) + ?: error("Failed to pull player through proxy") + + return when (result.status) { + Status.SUCCESS -> ConnectionResultEnum.SUCCESS + Status.ALREADY_CONNECTED -> ConnectionResultEnum.ALREADY_CONNECTED + Status.CONNECTION_IN_PROGRESS -> ConnectionResultEnum.CONNECTION_IN_PROGRESS + Status.CONNECTION_CANCELLED -> ConnectionResultEnum.CONNECTION_CANCELLED + Status.SERVER_DISCONNECTED -> ConnectionResultEnum.SERVER_DISCONNECTED + } + } + + override suspend fun pullPlayers(players: Collection): ObjectList> { + if (players.isEmpty()) return emptyObjectList() + if (players.size == 1) { + val player = players.first() + val (result, _) = player.connectToServer(this) + return objectListOf(player to result) + } + + val results = players.mapAsync { it to it.connectToServer(this) }.awaitAll() + .map { (player, rawResult) -> + val (result, _) = rawResult + player to result + } + + return objectListOf(*results.toTypedArray()) + } } fun ServerCommonCloudServer?.asStandaloneServer() = this as? StandaloneCloudServerImpl \ No newline at end of file diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerManagerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerManagerImpl.kt index 377b79a1..07429f70 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerManagerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerManagerImpl.kt @@ -3,7 +3,12 @@ package dev.slne.surf.cloud.standalone.server import com.google.auto.service.AutoService import dev.slne.surf.cloud.api.common.netty.network.protocol.awaitOrThrowUrgent import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.api.common.util.emptyObjectList +import dev.slne.surf.cloud.api.common.util.freeze +import dev.slne.surf.cloud.api.common.util.mutableObjectListOf import dev.slne.surf.cloud.api.server.server.ServerCloudServerManager import dev.slne.surf.cloud.api.server.server.ServerCommonCloudServer import dev.slne.surf.cloud.api.server.server.ServerProxyCloudServer @@ -16,8 +21,11 @@ import dev.slne.surf.cloud.core.common.util.bean import dev.slne.surf.cloud.standalone.config.standaloneConfig import dev.slne.surf.cloud.standalone.netty.server.NettyServerImpl import dev.slne.surf.cloud.standalone.netty.server.ProxyServerAutoregistration +import dev.slne.surf.cloud.standalone.player.StandaloneCloudPlayerImpl +import it.unimi.dsi.fastutil.objects.ObjectList import kotlinx.coroutines.sync.withLock import net.kyori.adventure.text.Component +import org.jetbrains.annotations.Unmodifiable import java.util.* @AutoService(CloudServerManager::class) @@ -99,6 +107,66 @@ class StandaloneCloudServerManagerImpl : CommonCloudServerManagerImpl + ): @Unmodifiable ObjectList> { + val toConnect = players + .filterNot { it.isInGroup(group) } + .filterIsInstance() + + if (toConnect.isEmpty()) { + return emptyObjectList() + } + + val results = mutableObjectListOf>() + val serversInGroup = retrieveServersByCategory(group) + .filterIsInstance() + .toMutableList() + + if (serversInGroup.isEmpty()) { + toConnect.forEach { player -> + results += player to ConnectionResultEnum.SERVER_NOT_FOUND + } + + return results.freeze() + } + + val queue = ArrayDeque(toConnect) + + while (queue.isNotEmpty() && serversInGroup.isNotEmpty()) { + serversInGroup.sortByDescending { server -> + val maxPlayers = server.maxPlayerCount + val expected = server.expectedPlayers + maxPlayers - expected + } + + val bestServer = serversInGroup.first() + val freeSlots = bestServer.maxPlayerCount - bestServer.expectedPlayers + + if (freeSlots <= 0) break + + val subset = mutableObjectListOf() + repeat(freeSlots) { + if (queue.isNotEmpty()) { + subset += queue.removeFirst() + } else { + return@repeat + } + } + + val pullResult = bestServer.pullPlayers(subset) + results += pullResult + } + + while (queue.isNotEmpty()) { + val leftoverPlayer = queue.removeFirst() + results += leftoverPlayer to ConnectionResultEnum.SERVER_FULL + } + + return results.freeze() + } + private suspend fun singleProxyServer() = serversMutex.withLock { servers.values.single { it is StandaloneProxyCloudServerImpl } } } From 61b223fe19f9c6bcedc2a8bf188b8b56771a2b58 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 12 Apr 2025 16:54:42 +0200 Subject: [PATCH 2/7] feat: add broadcast command for sending messages to servers and groups --- .../api/common/server/CloudServerManager.kt | 2 + .../api/common/server/CommonCloudServer.kt | 3 + .../bukkit/command/PaperCommandManager.kt | 2 + .../command/broadcast/BroadcastCommand.kt | 162 ++++++++++++++++++ .../common/server/CommonCloudServerImpl.kt | 13 ++ .../server/CommonCloudServerManagerImpl.kt | 11 ++ .../cloud/core/common/sound/CommonSounds.kt | 22 +++ 7 files changed, 215 insertions(+) create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt create mode 100644 surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/sound/CommonSounds.kt diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt index 067dbbbc..00553421 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt @@ -73,6 +73,8 @@ interface CloudServerManager { players: Collection ): @Unmodifiable ObjectList> + suspend fun broadcastToGroup(group: String, message: Component) + suspend fun broadcast(message: Component) companion object : CloudServerManager by INSTANCE { @InternalApi diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt index a26c5f92..81934bfa 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CommonCloudServer.kt @@ -5,6 +5,7 @@ import dev.slne.surf.cloud.api.common.player.ConnectionResult import dev.slne.surf.cloud.api.common.server.state.ServerState import it.unimi.dsi.fastutil.objects.Object2ObjectMap import net.kyori.adventure.audience.ForwardingAudience +import net.kyori.adventure.text.Component import org.jetbrains.annotations.ApiStatus /** @@ -104,6 +105,8 @@ interface CommonCloudServer : ForwardingAudience { fun isInGroup(group: String): Boolean + suspend fun broadcast(message: Component) + /** * Shuts down the server. * diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt index 492d96b6..69c69113 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt @@ -1,5 +1,6 @@ package dev.slne.surf.cloud.bukkit.command +import dev.slne.surf.cloud.bukkit.command.broadcast.broadcastCommand import dev.slne.surf.cloud.bukkit.command.lastseen.lastSeenCommand import dev.slne.surf.cloud.bukkit.command.network.findCommand import dev.slne.surf.cloud.bukkit.command.network.sendCommand @@ -13,5 +14,6 @@ object PaperCommandManager { sendCommand() playtimeCommand() lastSeenCommand() + broadcastCommand() } } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt new file mode 100644 index 00000000..4552cc63 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt @@ -0,0 +1,162 @@ +package dev.slne.surf.cloud.bukkit.command.broadcast + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.CommandTree +import dev.jorel.commandapi.arguments.Argument +import dev.jorel.commandapi.kotlindsl.* +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument +import dev.slne.surf.surfapi.core.api.messages.Colors +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import net.kyori.adventure.text.Component +import org.bukkit.command.CommandSender + +@Suppress("DuplicatedCode") +fun broadcastCommand() = commandTree("broadcast") { + literalArgument("--server") { + cloudServerArgument("server") { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val server: CloudServer by args + val message: Component by args + executeBroadcast(sender, message, server = server) + } + } + } + literalArgument("--prefix") { + argument(MiniMessageArgument("prefix")) { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val server: CloudServer by args + val prefix: Component by args + val message: Component by args + executeBroadcast(sender, message, prefix = prefix, server = server) + } + } + } + } + } + } + } + + literalArgument("--group") { + cloudServerGroupArgument("group") { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val group: String by args + val message: Component by args + executeBroadcast(sender, message, group = group) + } + } + } + literalArgument("--prefix") { + argument(MiniMessageArgument("prefix")) { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val group: String by args + val prefix: Component by args + val message: Component by args + executeBroadcast(sender, message, prefix = prefix, group = group) + } + } + } + } + } + } + } + + literalArgument("--prefix") { + argument(MiniMessageArgument("prefix")) { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val prefix: Component by args + val message: Component by args + executeBroadcast(sender, message, prefix = prefix) + } + } + } + literalArgument("--server") { + cloudServerArgument("server") { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val server: CloudServer by args + val prefix: Component by args + val message: Component by args + executeBroadcast(sender, message, prefix = prefix, server = server) + } + } + } + } + } + literalArgument("--group") { + cloudServerGroupArgument("group") { + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val group: String by args + val prefix: Component by args + val message: Component by args + executeBroadcast(sender, message, prefix = prefix, group = group) + } + } + } + } + } + } + } + + literalArgument("--message") { + argument(MiniMessageArgument("message")) { + anyExecutor { sender, args -> + val message: Component by args + executeBroadcast(sender, message) + } + } + } +} + +private fun executeBroadcast( + sender: CommandSender, + message: Component, + prefix: Component? = null, + server: CloudServer? = null, + group: String? = null +) = plugin.launch{ + val prefix = prefix ?: Colors.PREFIX + val message = message.replaceText { + it.match("(?m)^") + .replacement(prefix) + .replaceInsideHoverEvents(false) + } + + when { + server != null -> server.sendMessage(message) + group != null -> CloudServerManager.broadcastToGroup(group, message) + else -> CloudServerManager.broadcast(message) + } + + sender.sendText { + appendPrefix() + success("Die Nachricht wurde erfolgreich an ") + if (server != null) { + success("den Server ") + variableValue(server.name) + } else if (group != null) { + success("die Gruppe ") + variableValue(group) + } else { + success("alle") + } + success(" gesendet.") + } +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt index 7b5c226d..5781698b 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerImpl.kt @@ -11,8 +11,13 @@ import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf import dev.slne.surf.cloud.core.common.coreCloudInstance import dev.slne.surf.cloud.core.common.coroutines.PlayerBatchTransferScope import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ClientInformation +import dev.slne.surf.cloud.core.common.sound.CommonSounds import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import net.kyori.adventure.sound.Sound +import net.kyori.adventure.sound.Sound.Emitter +import net.kyori.adventure.text.Component abstract class CommonCloudServerImpl( override val uid: Long, @@ -52,6 +57,14 @@ abstract class CommonCloudServerImpl( return group.equals(group, ignoreCase = true) } + override suspend fun broadcast(message: Component) { + sendMessage(message) + for (sound in CommonSounds.broadcastSounds) { + playSound(sound, Emitter.self()) + delay(150) + } + } + override val maxPlayerCount get() = information.maxPlayerCount override val currentPlayerCount get() = users.size override val state get() = information.state diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt index f4630218..86fd1610 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt @@ -11,6 +11,7 @@ import dev.slne.surf.cloud.api.common.util.toObjectSet import it.unimi.dsi.fastutil.objects.ObjectCollection import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import net.kyori.adventure.text.Component abstract class CommonCloudServerManagerImpl : CloudServerManager { protected val servers = mutableLong2ObjectMapOf().synchronize() @@ -64,4 +65,14 @@ abstract class CommonCloudServerManagerImpl : CloudServer override suspend fun retrieveAllServers(): ObjectCollection { return serversMutex.withLock { servers.values.toObjectSet() } } + + override suspend fun broadcastToGroup(group: String, message: Component) = + serversMutex.withLock { + servers.values.filter { it.isInGroup(group) }.filterIsInstance() + }.forEach { it.broadcast(message) } + + + override suspend fun broadcast(message: Component) = serversMutex.withLock { + servers.values.filterIsInstance() + }.forEach { it.broadcast(message) } } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/sound/CommonSounds.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/sound/CommonSounds.kt new file mode 100644 index 00000000..c5b1ad94 --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/sound/CommonSounds.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.cloud.core.common.sound + +import dev.slne.surf.cloud.api.common.util.objectListOf +import dev.slne.surf.surfapi.core.api.generated.SoundKeys +import dev.slne.surf.surfapi.core.api.messages.adventure.Sound +import net.kyori.adventure.sound.Sound.Source + +object CommonSounds { + val BROADCAST_SOUND_1 = Sound { + type(SoundKeys.BLOCK_NOTE_BLOCK_CHIME) + pitch(0.9f) + volume(1f) + source(Source.MASTER) + } + val BROADCAST_SOUND_2 = Sound { + type(SoundKeys.BLOCK_NOTE_BLOCK_CHIME) + pitch(1.3f) + volume(1f) + source(Source.MASTER) + } + val broadcastSounds = objectListOf(BROADCAST_SOUND_1, BROADCAST_SOUND_2) +} \ No newline at end of file From a04f540c2554a8bc07740a728ccfd8f103c4de54 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 13 Apr 2025 16:27:43 +0200 Subject: [PATCH 3/7] feat: implement silent disconnect command and related functionality --- .../cloud/api/common/player/CloudPlayer.kt | 7 + .../api/common/server/CloudServerManager.kt | 2 + .../slne/surf/cloud/api/common/util/util.kt | 5 +- .../bukkit/command/PaperCommandManager.kt | 6 + .../command/broadcast/BroadcastCommand.kt | 3 + .../connection/DisconnectPlayerCommand.kt | 49 +++++++ .../SilentDisconnectPlayerCommand.kt | 26 ++++ .../bukkit/command/network/GListCommand.kt | 123 ++++++++++++++++++ .../cloud/bukkit/listener/ListenerManager.kt | 6 + .../player/SilentDisconnectListener.kt | 33 +++++ .../BukkitSpecificPacketListenerExtension.kt | 17 +++ .../permission/CloudPermissionRegistry.kt | 3 + .../player/BukkitClientCloudPlayerImpl.kt | 18 ++- .../player/BukkitCloudPlayerManagerImpl.kt | 2 +- .../ClientRunningPacketListenerImpl.kt | 8 ++ ...PlatformSpecificPacketListenerExtension.kt | 3 + .../client/player/ClientCloudPlayerImpl.kt | 10 +- .../common/netty/network/ConnectionImpl.kt | 5 +- .../running/RunningClientPacketListener.kt | 4 + .../protocol/running/RunningProtocols.kt | 4 + .../running/RunningServerPacketListener.kt | 4 + .../running/SilentDisconnectPlayerPacket.kt | 14 ++ .../running/TeleportPlayerToPlayerPacket.kt | 15 +++ .../common/player/CommonCloudPlayerImpl.kt | 1 + .../core/common/server/CloudServerImpl.kt | 26 ++++ .../server/CommonCloudServerManagerImpl.kt | 8 +- .../ServerRunningPacketListenerImpl.kt | 17 +++ .../player/StandaloneCloudPlayerImpl.kt | 46 ++++++- .../server/StandaloneCloudServerImpl.kt | 17 --- ...VelocitySpecificPacketListenerExtension.kt | 11 ++ .../player/VelocityClientCloudPlayerImpl.kt | 10 +- .../player/VelocityCloudPlayerManagerImpl.kt | 2 +- 32 files changed, 475 insertions(+), 30 deletions(-) create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/DisconnectPlayerCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/GListCommand.kt create mode 100644 surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt create mode 100644 surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/SilentDisconnectPlayerPacket.kt create mode 100644 surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/TeleportPlayerToPlayerPacket.kt diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt index cd7c3a19..5d39d66e 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt @@ -110,6 +110,13 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but */ fun disconnect(reason: Component) + /** + * Disconnects the player from the network silently. + * + * The player will think they are timed out. + */ + fun disconnectSilent() + /** * Teleports the player to a specified location. * diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt index 00553421..ec999df7 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt @@ -52,6 +52,8 @@ interface CloudServerManager { */ suspend fun retrieveServerByName(name: String): CommonCloudServer? + suspend fun retrieveServersInGroup(group: String): ObjectList + @InternalApi fun getServerByNameUnsafe(name: String): CloudServer? diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt index 441ce012..624ba861 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt @@ -1,5 +1,6 @@ package dev.slne.surf.cloud.api.common.util +import it.unimi.dsi.fastutil.objects.ObjectList import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -146,7 +147,7 @@ private fun createFileDeletedCheck(path: Path): () -> Boolean = { fun Method.isSuspending() = kotlinFunction?.isSuspend == true -suspend inline fun Iterable.mapAsync(crossinline transform: suspend (T) -> R): List> = +suspend inline fun Iterable.mapAsync(crossinline transform: suspend (T) -> R): ObjectList> = coroutineScope { - map { async { transform(it) } } + mapTo(mutableObjectListOf()) { async { transform(it) } } } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt index 69c69113..d41bc785 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt @@ -1,8 +1,11 @@ package dev.slne.surf.cloud.bukkit.command import dev.slne.surf.cloud.bukkit.command.broadcast.broadcastCommand +import dev.slne.surf.cloud.bukkit.command.connection.disconnectPlayerCommand +import dev.slne.surf.cloud.bukkit.command.connection.silentDisconnectPlayerCommand import dev.slne.surf.cloud.bukkit.command.lastseen.lastSeenCommand import dev.slne.surf.cloud.bukkit.command.network.findCommand +import dev.slne.surf.cloud.bukkit.command.network.glistCommand import dev.slne.surf.cloud.bukkit.command.network.sendCommand import dev.slne.surf.cloud.bukkit.command.network.serverCommand import dev.slne.surf.cloud.bukkit.command.playtime.playtimeCommand @@ -15,5 +18,8 @@ object PaperCommandManager { playtimeCommand() lastSeenCommand() broadcastCommand() + glistCommand() + silentDisconnectPlayerCommand() + disconnectPlayerCommand() } } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt index 4552cc63..ebb6f167 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt @@ -8,6 +8,7 @@ import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument import dev.slne.surf.surfapi.core.api.messages.Colors @@ -17,6 +18,8 @@ import org.bukkit.command.CommandSender @Suppress("DuplicatedCode") fun broadcastCommand() = commandTree("broadcast") { + withPermission(CloudPermissionRegistry.BROADCAST_COMMAND) + literalArgument("--server") { cloudServerArgument("server") { literalArgument("--message") { diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/DisconnectPlayerCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/DisconnectPlayerCommand.kt new file mode 100644 index 00000000..33d3e8b0 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/DisconnectPlayerCommand.kt @@ -0,0 +1,49 @@ +package dev.slne.surf.cloud.bukkit.command.connection + +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.argument +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument +import dev.slne.surf.surfapi.core.api.messages.CommonComponents +import dev.slne.surf.surfapi.core.api.messages.adventure.appendNewline +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import net.kyori.adventure.text.Component +import org.bukkit.command.CommandSender + +fun disconnectPlayerCommand() = commandTree("disconnect") { + onlineCloudPlayerArgument("player") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + disconnect(sender, player) + } + + argument(MiniMessageArgument("reason")) { + anyExecutor { sender, args -> + val player: CloudPlayer by args + val reason: Component by args + disconnect(sender, player, reason) + } + } + } +} + +private fun disconnect(sender: CommandSender, player: CloudPlayer, reason: Component? = null) { + val reason = reason ?: buildText { + appendDisconnectHeader() + error("DU WURDEST VOM NETZWERK GEWORFEN") + appendNewline(3) + append(CommonComponents.RETRY_LATER_FOOTER) + } + + player.disconnect(reason) + sender.sendText { + appendPrefix() + success("Der Spielende ") + variableValue(player.name) + success(" wurde erfolgreich vom Netzwerk getrennt.") + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt new file mode 100644 index 00000000..f7a61b7b --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.cloud.bukkit.command.connection + +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText + +fun silentDisconnectPlayerCommand() = commandTree("silentdisconnect") { // TODO: 13.04.2025 14:15 - better name + withPermission(CloudPermissionRegistry.SILENT_DISCONNECT_COMMAND) + + onlineCloudPlayerArgument("player") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + player.disconnectSilent() + sender.sendText { + appendPrefix() + success("Der Spielende ") + variableValue(player.name) + success(" wurde erfolgreich still getrennt.") + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/GListCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/GListCommand.kt new file mode 100644 index 00000000..ce0b5996 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/network/GListCommand.kt @@ -0,0 +1,123 @@ +package dev.slne.surf.cloud.bukkit.command.network + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.literalArgument +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument +import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager +import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.api.common.server.UserList +import dev.slne.surf.cloud.api.common.util.mapAsync +import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf +import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry +import dev.slne.surf.cloud.bukkit.plugin +import dev.slne.surf.surfapi.core.api.messages.Colors +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.messages.joinToComponent +import it.unimi.dsi.fastutil.objects.Object2ObjectMap +import kotlinx.coroutines.awaitAll +import net.kyori.adventure.text.Component +import org.bukkit.command.CommandSender +import dev.slne.surf.surfapi.core.api.messages.adventure.text as component + +fun glistCommand() = commandTree("glist") { + withPermission(CloudPermissionRegistry.GLIST_COMMAND) + + anyExecutor { sender, args -> + sender.sendText { + appendPrefix() + info("Es sind gerade ") + variableValue(CloudPlayerManager.getOnlinePlayers().size) + info(" Spielende auf dem Netzwerk online.") + } + } + + literalArgument("all") { + anyExecutor { sender, args -> + plugin.launch { + displayOnlinePlayers( + sender, + CloudServerManager.retrieveAllServers().filterIsInstance() + ) + } + } + } + + literalArgument("group") { + cloudServerGroupArgument("group") { + anyExecutor { sender, args -> + val group: String by args + plugin.launch { + displayOnlinePlayers( + sender, + CloudServerManager.retrieveServersInGroup(group) + .filterIsInstance() + ) + } + } + } + } + + literalArgument("server") { + cloudServerArgument("server") { + anyExecutor { sender, args -> + val server: CloudServer by args + plugin.launch { + displayOnlinePlayers(sender, listOf(server)) + } + } + } + } +} + +private suspend fun displayOnlinePlayers(sender: CommandSender, servers: Collection) { + sender.sendText { + appendNewPrefixedLine() + if (servers.size == 1) { + val server = servers.first() + + variableKey("${server.name} (") + variableKey(server.currentPlayerCount) + variableKey("): ") + append(server.users.displayNames()) + } else { + val groupedServers = servers.groupBy { it.group }.withDisplayNames() + for ((group, serversWithName) in groupedServers) { + if (serversWithName.isEmpty()) continue + variableKey("$group:") + appendNewPrefixedLine() + for ((server, names) in serversWithName) { + variableKey(" ${server.name} (") + variableValue(server.currentPlayerCount) + variableKey("): ") + append(names) + appendNewPrefixedLine() + } + } + } + + if (servers.size != 1) { + val totalUsers = servers.sumOf { it.currentPlayerCount } + appendNewPrefixedLine() + variableKey("Insgesamt: ") + variableValue(totalUsers) + } + } +} + +private suspend fun Map>.withDisplayNames(): Object2ObjectMap>> = + mapValuesTo(mutableObject2ObjectMapOf(size)) { (_, servers) -> + servers.mapAsync { server -> + server to server.users.displayNames() + }.awaitAll() + } + + +private suspend fun UserList.displayNames() = + mapAsync { it.displayName().hoverEvent(component(it.uuid, Colors.VARIABLE_VALUE)) } + .awaitAll() + .joinToComponent { it } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/ListenerManager.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/ListenerManager.kt index 41ecd61e..8a9743ed 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/ListenerManager.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/ListenerManager.kt @@ -2,18 +2,24 @@ package dev.slne.surf.cloud.bukkit.listener import dev.slne.surf.cloud.bukkit.listener.exception.SurfFatalErrorExceptionListener import dev.slne.surf.cloud.bukkit.listener.player.ConnectionListener +import dev.slne.surf.cloud.bukkit.listener.player.SilentDisconnectListener import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.surfapi.bukkit.api.event.register +import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution +import dev.slne.surf.surfapi.bukkit.api.nms.nmsBridge import org.bukkit.event.HandlerList +@OptIn(NmsUseWithCaution::class) object ListenerManager { fun registerListeners() { ConnectionListener.register() SurfFatalErrorExceptionListener.register() + nmsBridge.registerClientboundPacketListener(SilentDisconnectListener) } fun unregisterListeners() { HandlerList.unregisterAll(plugin) + nmsBridge.unregisterClientboundPacketListener(SilentDisconnectListener) } } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt new file mode 100644 index 00000000..7dc2d618 --- /dev/null +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt @@ -0,0 +1,33 @@ +package dev.slne.surf.cloud.bukkit.listener.player + +import dev.slne.surf.cloud.api.common.util.mutableObjectSetOf +import dev.slne.surf.cloud.bukkit.player.BukkitClientCloudPlayerImpl +import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution +import dev.slne.surf.surfapi.bukkit.api.nms.listener.NmsClientboundPacketListener +import dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.clientbound.DisconnectPacket +import dev.slne.surf.surfapi.bukkit.api.packet.listener.listener.PacketListenerResult +import org.bukkit.entity.Player +import java.util.* + +@OptIn(NmsUseWithCaution::class) +object SilentDisconnectListener : NmsClientboundPacketListener { + private val silentDisconnects = mutableObjectSetOf() + + override fun handleClientboundPacket( + packet: DisconnectPacket, + player: Player + ): PacketListenerResult { + if (!silentDisconnects.remove(player.uniqueId)) return PacketListenerResult.CONTINUE + return PacketListenerResult.CANCEL + } + + fun silentDisconnect(player: BukkitClientCloudPlayerImpl) { + val player = player.audience ?: return + silentDisconnect(player) + } + + fun silentDisconnect(player: Player) { + silentDisconnects.add(player.uniqueId) + player.kick() + } +} \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt index 9e5ec42f..6b54e54d 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt @@ -6,10 +6,12 @@ import dev.slne.surf.cloud.api.common.player.toCloudPlayer import dev.slne.surf.cloud.api.common.player.teleport.TeleportLocation import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag +import dev.slne.surf.cloud.bukkit.listener.player.SilentDisconnectListener import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.cloud.core.client.netty.network.PlatformSpecificPacketListenerExtension import dev.slne.surf.cloud.core.common.netty.network.protocol.running.RegistrationInfo import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundTransferPlayerPacketResponse +import kotlinx.coroutines.future.await import net.kyori.adventure.text.Component import org.bukkit.Bukkit import java.net.InetSocketAddress @@ -31,6 +33,11 @@ object BukkitSpecificPacketListenerExtension : PlatformSpecificPacketListenerExt error("Requested wrong server! This packet can only be acknowledged on a proxy!") } + override fun silentDisconnectPlayer(playerUuid: UUID) { + val player = Bukkit.getPlayer(playerUuid) ?: return + SilentDisconnectListener.silentDisconnect(player) + } + override suspend fun teleportPlayer( uuid: UUID, location: TeleportLocation, @@ -47,6 +54,16 @@ object BukkitSpecificPacketListenerExtension : PlatformSpecificPacketListenerExt error("Requested wrong server! This packet can only be acknowledged on a proxy!") } + override suspend fun teleportPlayerToPlayer( + uuid: UUID, + target: UUID + ): Boolean { + val player = Bukkit.getPlayer(uuid) ?: return false + val targetPlayer = Bukkit.getPlayer(target) ?: return false + + return player.teleportAsync(targetPlayer.location).await() + } + override fun triggerShutdown() { plugin.launch(plugin.globalRegionDispatcher) { Bukkit.shutdown() diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt index 1ae7ed4b..ba568861 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt @@ -13,4 +13,7 @@ object CloudPermissionRegistry: PermissionRegistry() { val PLAYTIME_COMMAND = create("$COMMAND_PREFIX.playtime") val PLAYTIME_COMMAND_OTHER = create("$COMMAND_PREFIX.playtime.other") val LAST_SEEN_COMMAND = create("$COMMAND_PREFIX.lastseen") + val BROADCAST_COMMAND = create("$COMMAND_PREFIX.broadcast") + val GLIST_COMMAND = create("$COMMAND_PREFIX.glist") + val SILENT_DISCONNECT_COMMAND = create("$COMMAND_PREFIX.silentdisconnect") } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt index 239c4f6a..39c680a9 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt @@ -7,6 +7,7 @@ import dev.slne.surf.cloud.api.client.paper.toLocation import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag import dev.slne.surf.cloud.api.common.player.teleport.TeleportLocation +import dev.slne.surf.cloud.bukkit.listener.player.SilentDisconnectListener import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.cloud.core.client.player.ClientCloudPlayerImpl import dev.slne.surf.cloud.core.common.netty.network.protocol.running.DisconnectPlayerPacket @@ -17,14 +18,27 @@ import org.bukkit.conversations.* import org.bukkit.entity.Player import java.util.* -class BukkitClientCloudPlayerImpl(uuid: UUID) : ClientCloudPlayerImpl(uuid) { - override val audience: Player? get() = Bukkit.getPlayer(uuid) +class BukkitClientCloudPlayerImpl(uuid: UUID, name: String) : ClientCloudPlayerImpl(uuid, + name +) { + public override val audience: Player? get() = Bukkit.getPlayer(uuid) override val platformClass: Class = Player::class.java override fun disconnect(reason: Component) { DisconnectPlayerPacket(uuid, reason).fireAndForget() } + override fun disconnectSilent() { + val player = audience + + if (player == null) { + // TODO: 13.04.2025 14:05 - send packet + return + } + + SilentDisconnectListener.silentDisconnect(this) + } + fun test() { val player = audience ?: return diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitCloudPlayerManagerImpl.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitCloudPlayerManagerImpl.kt index 43c0dfa4..ec979170 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitCloudPlayerManagerImpl.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitCloudPlayerManagerImpl.kt @@ -22,7 +22,7 @@ class BukkitCloudPlayerManagerImpl : CommonClientCloudPlayerManagerImpl) + suspend fun teleportPlayerToPlayer(uuid: UUID, target: UUID): Boolean fun triggerShutdown() } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt index 9d3c1e53..debcdec3 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt @@ -1,9 +1,11 @@ package dev.slne.surf.cloud.core.client.player +import dev.slne.surf.cloud.api.client.netty.packet.awaitOrThrow import dev.slne.surf.cloud.api.client.netty.packet.fireAndAwait import dev.slne.surf.cloud.api.client.netty.packet.fireAndAwaitOrThrow import dev.slne.surf.cloud.api.client.netty.packet.fireAndForget import dev.slne.surf.cloud.api.common.netty.packet.DEFAULT_URGENT_TIMEOUT +import dev.slne.surf.cloud.api.common.player.CloudPlayer import dev.slne.surf.cloud.api.common.player.ConnectionResult import dev.slne.surf.cloud.api.common.player.name.NameHistory import dev.slne.surf.cloud.api.common.player.playtime.Playtime @@ -40,8 +42,8 @@ import java.util.* import kotlin.time.Duration import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataResponse.NameHistory as NameHistoryResponse -abstract class ClientCloudPlayerImpl(uuid: UUID) : - CommonCloudPlayerImpl(uuid) { +abstract class ClientCloudPlayerImpl(uuid: UUID, name: String) : + CommonCloudPlayerImpl(uuid, name) { @Volatile var proxyServerUid: Long? = null @@ -336,6 +338,10 @@ abstract class ClientCloudPlayerImpl(uuid: UUID) : vararg flags: TeleportFlag ) = TeleportPlayerPacket(uuid, location, teleportCause, *flags).fireAndAwaitOrThrow().result + override suspend fun teleport(target: CloudPlayer): Boolean { + return TeleportPlayerToPlayerPacket(uuid, target.uuid).awaitOrThrow() + } + protected fun withLuckpermsOrThrow(block: (User) -> R): R { val user = luckperms.userManager.getUser(uuid) ?: error("User not found in LuckPerms! Are you sure the player is online?") diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/ConnectionImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/ConnectionImpl.kt index 2cbb8152..a9719380 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/ConnectionImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/ConnectionImpl.kt @@ -339,15 +339,16 @@ class ConnectionImpl( ) is DisconnectPlayerPacket -> listener.handleDisconnectPlayer(msg) + is SilentDisconnectPlayerPacket -> listener.handleSilentDisconnectPlayer(msg) is TeleportPlayerPacket -> listener.handleTeleportPlayer(msg) + is TeleportPlayerToPlayerPacket -> listener.handleTeleportPlayerToPlayer(msg) is ServerboundShutdownServerPacket -> listener.handleShutdownServer(msg) is ServerboundRequestPlayerDataPacket -> listener.handleRequestPlayerData(msg) is ServerboundUpdateAFKState -> listener.handleUpdateAFKState(msg) else -> listener.handlePacket(msg) // handle other packets } - } } @@ -462,7 +463,9 @@ class ConnectionImpl( ) is DisconnectPlayerPacket -> listener.handleDisconnectPlayer(msg) + is SilentDisconnectPlayerPacket -> listener.handleSilentDisconnectPlayer(msg) is TeleportPlayerPacket -> listener.handleTeleportPlayer(msg) + is TeleportPlayerToPlayerPacket -> listener.handleTeleportPlayerToPlayer(msg) is ClientboundRegisterCloudServersToProxyPacket -> listener.handleRegisterCloudServersToProxy( msg ) diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt index eaa3a913..56221b1d 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt @@ -63,8 +63,12 @@ interface RunningClientPacketListener : ClientCommonPacketListener { fun handleDisconnectPlayer(packet: DisconnectPlayerPacket) + fun handleSilentDisconnectPlayer(packet: SilentDisconnectPlayerPacket) + suspend fun handleTeleportPlayer(packet: TeleportPlayerPacket) + suspend fun handleTeleportPlayerToPlayer(packet: TeleportPlayerToPlayerPacket) + fun handleRegisterCloudServersToProxy(packet: ClientboundRegisterCloudServersToProxyPacket) fun handleTriggerShutdown(packet: ClientboundTriggerShutdownPacket) diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt index 19ad109d..590b33be 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt @@ -54,6 +54,8 @@ object RunningProtocols { .addPacket(ClientboundBatchUpdateServer.STREAM_CODEC) .addPacket(ServerboundRequestPlayerDataResponse.STREAM_CODEC) .addPacket(PullPlayersToGroupResponsePacket::class.createCodec()) + .addPacket(SilentDisconnectPlayerPacket::class.createCodec()) + .addPacket(TeleportPlayerToPlayerPacket::class.createCodec()) } val CLIENTBOUND by lazy { CLIENTBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } @@ -100,6 +102,8 @@ object RunningProtocols { .addPacket(ServerboundRequestPlayerDataPacket.STREAM_CODEC) .addPacket(ServerboundUpdateAFKState::class.createCodec()) .addPacket(ServerboundPullPlayersToGroupPacket::class.createCodec()) + .addPacket(SilentDisconnectPlayerPacket::class.createCodec()) + .addPacket(TeleportPlayerToPlayerPacket::class.createCodec()) } val SERVERBOUND by lazy { SERVERBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt index 77e30f68..1b8f1edc 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt @@ -58,8 +58,12 @@ interface RunningServerPacketListener : ServerCommonPacketListener, TickablePack fun handleDisconnectPlayer(packet: DisconnectPlayerPacket) + fun handleSilentDisconnectPlayer(packet: SilentDisconnectPlayerPacket) + suspend fun handleTeleportPlayer(packet: TeleportPlayerPacket) + suspend fun handleTeleportPlayerToPlayer(packet: TeleportPlayerToPlayerPacket) + suspend fun handleShutdownServer(packet: ServerboundShutdownServerPacket) suspend fun handleRequestPlayerData(packet: ServerboundRequestPlayerDataPacket) diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/SilentDisconnectPlayerPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/SilentDisconnectPlayerPacket.kt new file mode 100644 index 00000000..87b7f04f --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/SilentDisconnectPlayerPacket.kt @@ -0,0 +1,14 @@ +package dev.slne.surf.cloud.core.common.netty.network.protocol.running + +import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow +import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.util.* + +@SurfNettyPacket("cloud:bidirectional:silent_disconnect_player", PacketFlow.BIDIRECTIONAL) +@Serializable +class SilentDisconnectPlayerPacket( + val uuid: @Contextual UUID +) : NettyPacket() \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/TeleportPlayerToPlayerPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/TeleportPlayerToPlayerPacket.kt new file mode 100644 index 00000000..dc075825 --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/TeleportPlayerToPlayerPacket.kt @@ -0,0 +1,15 @@ +package dev.slne.surf.cloud.core.common.netty.network.protocol.running + +import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow +import dev.slne.surf.cloud.api.common.netty.network.protocol.boolean.BooleanResponsePacket +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.util.* + +@SurfNettyPacket("cloud:bidirectional:teleport_player_to_player", PacketFlow.BIDIRECTIONAL) +@Serializable +class TeleportPlayerToPlayerPacket( + val uuid: @Contextual UUID, + val target: @Contextual UUID, +) : BooleanResponsePacket() \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt index f08ae76e..9905533e 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt @@ -5,6 +5,7 @@ import dev.slne.surf.cloud.api.common.player.ConnectionResult import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.TeleportPlayerToPlayerPacket import java.time.ZonedDateTime import java.util.* diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CloudServerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CloudServerImpl.kt index ef4b7fd2..e4d6d335 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CloudServerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CloudServerImpl.kt @@ -2,9 +2,18 @@ package dev.slne.surf.cloud.core.common.server import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodec import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.UserListImpl +import dev.slne.surf.cloud.api.common.util.emptyObjectList +import dev.slne.surf.cloud.api.common.util.mapAsync +import dev.slne.surf.cloud.api.common.util.objectListOf import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ClientInformation +import it.unimi.dsi.fastutil.objects.ObjectList +import kotlinx.coroutines.awaitAll +import org.jetbrains.annotations.Unmodifiable +import kotlin.to open class CloudServerImpl( uid: Long, @@ -32,4 +41,21 @@ open class CloudServerImpl( } override val allowlist get() = information.allowlist + + override suspend fun pullPlayers(players: Collection): ObjectList> { + if (players.isEmpty()) return emptyObjectList() + if (players.size == 1) { + val player = players.first() + val (result, _) = player.connectToServer(this) + return objectListOf(player to result) + } + + val results = players.mapAsync { it to it.connectToServer(this) }.awaitAll() + .map { (player, rawResult) -> + val (result, _) = rawResult + player to result + } + + return objectListOf(*results.toTypedArray()) + } } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt index 86fd1610..c212dc6f 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/server/CommonCloudServerManagerImpl.kt @@ -9,6 +9,7 @@ import dev.slne.surf.cloud.api.common.util.mutableObjectListOf import dev.slne.surf.cloud.api.common.util.synchronize import dev.slne.surf.cloud.api.common.util.toObjectSet import it.unimi.dsi.fastutil.objects.ObjectCollection +import it.unimi.dsi.fastutil.objects.ObjectList import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.kyori.adventure.text.Component @@ -32,7 +33,7 @@ abstract class CommonCloudServerManagerImpl : CloudServer name: String ) = serversMutex.withLock { servers.values.asSequence() - .filter { it.group.equals(category, true) && it.name.equals(name, true) } + .filter { it.isInGroup(category) && it.name.equals(name, true) } .minByOrNull { it.currentPlayerCount } } @@ -47,6 +48,11 @@ abstract class CommonCloudServerManagerImpl : CloudServer .filter { it.name.equals(name, ignoreCase = true) } .minByOrNull { it.currentPlayerCount } as? CloudServer + override suspend fun retrieveServersInGroup(group: String): ObjectList = + serversMutex.withLock { + servers.values.filterTo(mutableObjectListOf()) { it.isInGroup(group) } + } + @InternalApi override fun existsServerGroup(name: String): Boolean = diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt index 8b733300..2158e9e9 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt @@ -3,6 +3,7 @@ package dev.slne.surf.cloud.standalone.netty.server.network import dev.slne.surf.cloud.api.common.netty.network.protocol.respond import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket import dev.slne.surf.cloud.api.common.netty.packet.NettyPacketInfo +import dev.slne.surf.cloud.api.common.player.CloudPlayerManager import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager @@ -250,6 +251,10 @@ class ServerRunningPacketListenerImpl( withPlayer(packet.uuid) { disconnect(packet.reason) } } + override fun handleSilentDisconnectPlayer(packet: SilentDisconnectPlayerPacket) { + withPlayer(packet.uuid) { disconnectSilent() } + } + override suspend fun handleTeleportPlayer(packet: TeleportPlayerPacket) { withPlayer(packet.uuid) { val result = teleport( @@ -262,6 +267,18 @@ class ServerRunningPacketListenerImpl( } } + override suspend fun handleTeleportPlayerToPlayer(packet: TeleportPlayerToPlayerPacket) { + val player = CloudPlayerManager.getPlayer(packet.uuid) + val targetPlayer = CloudPlayerManager.getPlayer(packet.target) + + if (player == null || targetPlayer == null) { + packet.respond(false) + return + } + + packet.respond(player.teleport(targetPlayer)) + } + override suspend fun handleShutdownServer(packet: ServerboundShutdownServerPacket) { val server = CloudServerManager.retrieveServerById(packet.serverId) ?: return server.shutdown() diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt index d749d0e1..daf5c18b 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt @@ -1,6 +1,8 @@ package dev.slne.surf.cloud.standalone.player +import dev.slne.surf.cloud.api.common.netty.network.protocol.await import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket +import dev.slne.surf.cloud.api.common.player.CloudPlayer import dev.slne.surf.cloud.api.common.player.ConnectionResult import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum import dev.slne.surf.cloud.api.common.player.name.NameHistory @@ -50,8 +52,8 @@ import java.util.concurrent.TimeUnit import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Address) : - CommonCloudPlayerImpl(uuid) { +class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) : + CommonCloudPlayerImpl(uuid, name) { companion object { private val log = logger() @@ -136,6 +138,10 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre proxyServer?.connection?.send(DisconnectPlayerPacket(uuid, reason)) } + override fun disconnectSilent() { + server?.connection?.send(SilentDisconnectPlayerPacket(uuid)) + } + suspend fun getPersistentData() = ppdcMutex.withLock { ppdc.toTagCompound() } suspend fun updatePersistentData(tag: CompoundTag) = ppdcMutex.withLock { ppdc.fromTagCompound(tag) } @@ -274,6 +280,23 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre // return switchServerOrQueueUnderNoProxy(server).also { connecting = false } } + override fun isOnServer(server: CloudServer): Boolean { + if (server !is StandaloneCloudServerImpl) { + return false + } + + return server == this.server + } + + override fun isInGroup(group: String): Boolean { + val server = server + if (server == null) { + return false + } + + return server.group == group + } + override suspend fun getLuckpermsMetaData(key: String): String? { return RequestLuckpermsMetaDataPacket(uuid, key).fireAndAwait(anyServer.connection)?.data } @@ -411,6 +434,25 @@ class StandaloneCloudPlayerImpl(uuid: UUID, val name: String, val ip: Inet4Addre ).fireAndAwait(server.connection)?.result == true } + override suspend fun teleport(target: CloudPlayer): Boolean { + require(target is StandaloneCloudPlayerImpl) { "Target must be a StandaloneCloudPlayerImpl" } + + val targetServer = target.server + if (targetServer == null || targetServer != this.server) { + val (result) = this.connectToServer(targetServer ?: return false) + if (!result.isSuccess) { + this.sendMessage(result.message) + return false + } + } + + val result = TeleportPlayerToPlayerPacket( + this.uuid, + target.uuid + ).await(targetServer.connection) == true + return result + } + private fun send(packet: NettyPacket) { anyServer.connection.send(packet) } diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt index 42e5b2f3..8b5e5b48 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/server/StandaloneCloudServerImpl.kt @@ -85,23 +85,6 @@ class StandaloneCloudServerImpl( Status.SERVER_DISCONNECTED -> ConnectionResultEnum.SERVER_DISCONNECTED } } - - override suspend fun pullPlayers(players: Collection): ObjectList> { - if (players.isEmpty()) return emptyObjectList() - if (players.size == 1) { - val player = players.first() - val (result, _) = player.connectToServer(this) - return objectListOf(player to result) - } - - val results = players.mapAsync { it to it.connectToServer(this) }.awaitAll() - .map { (player, rawResult) -> - val (result, _) = rawResult - player to result - } - - return objectListOf(*results.toTypedArray()) - } } fun ServerCommonCloudServer?.asStandaloneServer() = this as? StandaloneCloudServerImpl \ No newline at end of file diff --git a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt index 7cc62b7a..7d71044c 100644 --- a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt +++ b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt @@ -43,6 +43,10 @@ object VelocitySpecificPacketListenerExtension : PlatformSpecificPacketListenerE player.disconnect(reason) } + override fun silentDisconnectPlayer(playerUuid: UUID) { + error("Silent disconnect is not supported on Velocity") + } + override suspend fun teleportPlayer( uuid: UUID, location: TeleportLocation, @@ -57,6 +61,13 @@ object VelocitySpecificPacketListenerExtension : PlatformSpecificPacketListenerE .forEach { proxy.registerServer(it) } } + override suspend fun teleportPlayerToPlayer( + uuid: UUID, + target: UUID + ): Boolean { + error("Teleporting players is not supported on Velocity") + } + override fun triggerShutdown() { proxy.shutdown() } diff --git a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityClientCloudPlayerImpl.kt b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityClientCloudPlayerImpl.kt index eb51c535..b2dc9358 100644 --- a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityClientCloudPlayerImpl.kt +++ b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityClientCloudPlayerImpl.kt @@ -1,15 +1,23 @@ package dev.slne.surf.cloud.velocity.player import com.velocitypowered.api.proxy.Player +import dev.slne.surf.cloud.api.client.netty.packet.fireAndForget import dev.slne.surf.cloud.core.client.player.ClientCloudPlayerImpl +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.SilentDisconnectPlayerPacket import dev.slne.surf.cloud.velocity.proxy import net.kyori.adventure.text.Component import java.util.* -class VelocityClientCloudPlayerImpl(uuid: UUID) : ClientCloudPlayerImpl(uuid) { +class VelocityClientCloudPlayerImpl(uuid: UUID, name: String) : ClientCloudPlayerImpl(uuid, + name +) { override val platformClass = Player::class.java override val audience: Player? get() = proxy.getPlayer(uuid).orElse(null) override fun disconnect(reason: Component) { audience?.disconnect(reason) } + + override fun disconnectSilent() { + SilentDisconnectPlayerPacket(uuid).fireAndForget() + } } \ No newline at end of file diff --git a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityCloudPlayerManagerImpl.kt b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityCloudPlayerManagerImpl.kt index 10a8fe74..f0becdef 100644 --- a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityCloudPlayerManagerImpl.kt +++ b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/player/VelocityCloudPlayerManagerImpl.kt @@ -23,7 +23,7 @@ class VelocityCloudPlayerManagerImpl : ip: Inet4Address, serverUid: Long ): VelocityClientCloudPlayerImpl { - return VelocityClientCloudPlayerImpl(uuid).also { + return VelocityClientCloudPlayerImpl(uuid, name).also { if (proxy) { it.proxyServerUid = serverUid } else { From d47897e4901b805533ab86c9dd3843a624f90831 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 13 Apr 2025 18:56:36 +0200 Subject: [PATCH 4/7] feat: refactor broadcast command for improved message handling and concurrency --- .../command/broadcast/BroadcastCommand.kt | 16 +++++++++------- .../common/player/CloudPlayerManagerImpl.kt | 19 +------------------ 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt index ebb6f167..910f5dc8 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt @@ -1,8 +1,6 @@ package dev.slne.surf.cloud.bukkit.command.broadcast import com.github.shynixn.mccoroutine.folia.launch -import dev.jorel.commandapi.CommandTree -import dev.jorel.commandapi.arguments.Argument import dev.jorel.commandapi.kotlindsl.* import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument @@ -13,8 +11,10 @@ import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument import dev.slne.surf.surfapi.core.api.messages.Colors import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import kotlinx.coroutines.launch import net.kyori.adventure.text.Component import org.bukkit.command.CommandSender +import java.util.function.BiFunction @Suppress("DuplicatedCode") fun broadcastCommand() = commandTree("broadcast") { @@ -137,15 +137,17 @@ private fun executeBroadcast( ) = plugin.launch{ val prefix = prefix ?: Colors.PREFIX val message = message.replaceText { - it.match("(?m)^") + it.match("(?m)^|\\A") .replacement(prefix) .replaceInsideHoverEvents(false) } - when { - server != null -> server.sendMessage(message) - group != null -> CloudServerManager.broadcastToGroup(group, message) - else -> CloudServerManager.broadcast(message) + launch { + when { + server != null -> server.broadcast(message) + group != null -> CloudServerManager.broadcastToGroup(group, message) + else -> CloudServerManager.broadcast(message) + } } sender.sendText { diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt index b6eba0be..a75053ad 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt @@ -119,29 +119,12 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa } } + @MustBeInvokedByOverriders open suspend fun onServerConnect(uuid: UUID, player: P, serverUid: Long) { - coroutineScope { - launch { - val userList = CloudServerManager.retrieveServerById(serverUid)?.users ?: return@launch - if (userList !is UserListImpl || !userList.add(uuid)) { - log.atWarning() - .log("Failed to add player to server user list: $userList") - } - } - } } @MustBeInvokedByOverriders open suspend fun onServerDisconnect(uuid: UUID, player: P, serverUid: Long) { - coroutineScope { - launch { - val userList = CloudServerManager.retrieveServerById(serverUid)?.users ?: return@launch - if (userList !is UserListImpl || !userList.remove(uuid)) { - log.atWarning() - .log("Failed to remove player from server user list: $userList") - } - } - } } @MustBeInvokedByOverriders From 41bcd421ce41bb4797bde90dbf412ab34ef0bc5a Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 14 Apr 2025 18:24:11 +0200 Subject: [PATCH 5/7] feat: improve error handling and refactor silent disconnect functionality --- .../slne/surf/cloud/bukkit/BukkitBootstrap.kt | 2 +- .../dev/slne/surf/cloud/bukkit/BukkitMain.kt | 288 ++---------------- .../command/broadcast/BroadcastCommand.kt | 13 +- .../command/lastseen/LastSeenCommand.kt | 8 +- .../SurfFatalErrorExceptionListener.kt | 2 +- .../player/SilentDisconnectListener.kt | 5 - .../BukkitSpecificPacketListenerExtension.kt | 3 +- .../player/BukkitClientCloudPlayerImpl.kt | 12 +- .../cloud/core/common/CloudCoreInstance.kt | 4 +- .../slne/surf/cloud/standalone/Bootstrap.kt | 4 +- .../player/StandaloneCloudPlayerImpl.kt | 3 +- .../slne/surf/cloud/velocity/VelocityMain.kt | 6 +- ...VelocitySpecificPacketListenerExtension.kt | 4 +- 13 files changed, 51 insertions(+), 303 deletions(-) diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitBootstrap.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitBootstrap.kt index 2b04be55..0f82af19 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitBootstrap.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitBootstrap.kt @@ -20,7 +20,7 @@ class BukkitBootstrap : PluginBootstrap { ) ) } catch (e: Throwable) { - e.handleEventuallyFatalError({ exitProcess(it.exitCode) }) + e.handleEventuallyFatalError { exitProcess(it.exitCode) } } } diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitMain.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitMain.kt index 011d00ea..02397580 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitMain.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/BukkitMain.kt @@ -3,31 +3,20 @@ package dev.slne.surf.cloud.bukkit import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import com.github.shynixn.mccoroutine.folia.globalRegionDispatcher import com.github.shynixn.mccoroutine.folia.launch -import com.github.shynixn.mccoroutine.folia.ticks import dev.jorel.commandapi.CommandAPIBukkit import dev.jorel.commandapi.kotlindsl.* import dev.slne.surf.cloud.api.client.netty.packet.fireAndForget -import dev.slne.surf.cloud.api.client.paper.player.toCloudOfflinePlayer import dev.slne.surf.cloud.api.common.TestPacket import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.fineLocation import dev.slne.surf.cloud.api.common.player.toCloudPlayer import dev.slne.surf.cloud.api.common.server.CloudServerManager -import dev.slne.surf.cloud.bukkit.player.BukkitClientCloudPlayerImpl import dev.slne.surf.cloud.core.common.handleEventuallyFatalError import dev.slne.surf.surfapi.bukkit.api.event.listen -import dev.slne.surf.surfapi.core.api.messages.Colors -import dev.slne.surf.surfapi.core.api.messages.CommonComponents -import dev.slne.surf.surfapi.core.api.messages.adventure.buildText -import dev.slne.surf.surfapi.core.api.messages.adventure.sendText -import kotlinx.coroutines.async -import kotlinx.coroutines.delay import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.Bukkit import org.bukkit.Location -import org.bukkit.NamespacedKey -import org.bukkit.OfflinePlayer import org.bukkit.entity.Player import org.bukkit.event.server.ServerLoadEvent import kotlin.contracts.ExperimentalContracts @@ -38,7 +27,7 @@ class BukkitMain : SuspendingJavaPlugin() { try { bukkitCloudInstance.onLoad() } catch (t: Throwable) { - t.handleEventuallyFatalError({ Bukkit.shutdown() }) + t.handleEventuallyFatalError { Bukkit.shutdown() } } } @@ -46,130 +35,32 @@ class BukkitMain : SuspendingJavaPlugin() { try { bukkitCloudInstance.onEnable() } catch (t: Throwable) { - t.handleEventuallyFatalError({ Bukkit.shutdown() }) + t.handleEventuallyFatalError { Bukkit.shutdown() } } var serverLoaded = false listen { - if (serverLoaded) { - return@listen - } - + if (serverLoaded) return@listen serverLoaded = true - System.err.println("Server loaded ##############################################") - } - // TODO: does this actually delay until the server is fully loaded? - launch(globalRegionDispatcher) { - delay(1.ticks) - try { - bukkitCloudInstance.afterStart() - } catch (t: Throwable) { - t.handleEventuallyFatalError({ Bukkit.shutdown() }) - } - } - - commandTree("getServer") { - literalArgument("byId") { - longArgument("id") { - anyExecutor { sender, args -> - val id: Long by args - launch { - val server = CloudServerManager.retrieveServerById(id) - sender.sendMessage("Server: $server") - } - } - } - } - literalArgument("byCategoryAndName") { - stringArgument("category") { - stringArgument("name") { - anyExecutor { sender, args -> - val category: String by args - val name: String by args - launch { - val server = - CloudServerManager.retrieveServerByCategoryAndName( - category, - name - ) - sender.sendMessage("Server: $server") - } - } - } - } - } - literalArgument("byName") { - stringArgument("name") { - anyExecutor { sender, args -> - val name: String by args - launch { - val server = CloudServerManager.retrieveServerByName(name) - sender.sendMessage("Server: $server") - } - } - } - } - literalArgument("byCategory") { - stringArgument("category") { - anyExecutor { sender, args -> - val category: String by args - launch { - val servers = CloudServerManager.retrieveServersByCategory(category) - sender.sendMessage("Servers: $servers") - } - } - } - } - literalArgument("self") { - playerExecutor { player, _ -> - player.sendPlainMessage("Server: ${(player.toCloudPlayer()!! as BukkitClientCloudPlayerImpl).serverUid}") + launch(globalRegionDispatcher) { + try { + bukkitCloudInstance.afterStart() + } catch (t: Throwable) { + t.handleEventuallyFatalError { Bukkit.shutdown() } } } } - commandAPICommand("changePlayers") { - integerArgument("amount", min = 0) - anyExecutor { sender, args -> - val amount: Int by args - Bukkit.setMaxPlayers(amount) - } - } - - commandAPICommand("setPdc") { - namespacedKeyArgument("key") - stringArgument("value") - - playerExecutor { player, args -> - val key: NamespacedKey by args - val value: String by args - - launch { - player.toCloudPlayer()!!.withPersistentData { - setString(key, value) - } - } - } - } - - commandAPICommand("disconnect") { - entitySelectorArgumentOnePlayer("target") - adventureChatComponentArgument("reason", optional = true) - - playerExecutor { player, args -> - val target: Player by args - val reason = - args.getOptionalUnchecked("reason").orElse(Component.empty()) - - target.toCloudPlayer()?.disconnect(reason) - player.sendMessage( - Component.text( - "Disconnected player ${target.name}", - NamedTextColor.GREEN - ) - ) - } - } + // TODO: does this actually delay until the server is fully loaded? +// launch(globalRegionDispatcher) { +// delay(1.ticks) +// try { +// bukkitCloudInstance.afterStart() +// } catch (t: Throwable) { +// t.handleEventuallyFatalError({ Bukkit.shutdown() }) +// } +// } commandAPICommand("deport") { entitySelectorArgumentOnePlayer("target") @@ -214,155 +105,12 @@ class BukkitMain : SuspendingJavaPlugin() { } } - commandAPICommand("offlinePlayer") { - offlinePlayerArgument("player") - anyExecutor { sender, args -> - val player: OfflinePlayer by args - val offlinePlayer = player.toCloudOfflinePlayer() - - launch { - val displayName = async { offlinePlayer.displayName() } - val lastServer = async { offlinePlayer.lastServer() } - val lastSeen = async { offlinePlayer.lastSeen() } - val lastIpAddress = async { offlinePlayer.latestIpAddress() } - val playedBefore = async { offlinePlayer.playedBefore() } - val nameHistory = async { offlinePlayer.nameHistory() } - - sender.sendText { - appendPrefix() - appendNewPrefixedLine { - variableKey("UUID") - spacer(": ") - variableValue(offlinePlayer.uuid.toString()) - } - appendNewPrefixedLineAsync { - variableKey("Display Name") - spacer(": ") - append(displayName.await() ?: Component.text("#Unknown")) - } - appendNewPrefixedLineAsync { - variableKey("Last Server") - spacer(": ") - variableValue(lastServer.await()?.name ?: "#Unknown") - } - appendNewPrefixedLineAsync { - variableKey("Last Seen") - spacer(": ") - variableValue(lastSeen.await()?.toString() ?: "#Unknown") - } - appendNewPrefixedLineAsync { - variableKey("Last IP Address") - spacer(": ") - variableValue(lastIpAddress.await()?.hostAddress ?: "#Unknown") - } - appendNewPrefixedLineAsync { - variableKey("Played Before") - spacer(": ") - variableValue(playedBefore.await().toString()) - } - appendAsync { - appendCollectionNewLine(nameHistory.await().names()) {(timestamp, name) -> - buildText { - variableKey(timestamp.toString()) - append(CommonComponents.MAP_SEPERATOR) - variableValue(name) - } - } - } - } - } - } - } - commandAPICommand("send-test-packet") { anyExecutor { sender, args -> TestPacket.random().fireAndForget() sender.sendPlainMessage("Test packet sent") } } - - commandAPICommand("playtime") { - offlinePlayerArgument("player") - anyExecutor { sender, args -> - val player: OfflinePlayer by args - val cloudPlayer = player.toCloudOfflinePlayer() - launch { - val playtime = cloudPlayer.playtime() - val complete = playtime.sumPlaytimes() - val playtimeMap = playtime.playtimePerCategoryPerServer() - - sender.sendText { - appendPrefix() - info("Playtime for player ${player.name} (${player.uniqueId})") - appendNewPrefixedLine() - appendNewPrefixedLine { - variableKey("Total") - spacer(": ") - variableValue(complete.toString()) - } - appendNewPrefixedLine() - for ((group, groupServer) in playtimeMap) { - appendNewPrefixedLine { - spacer("- ") - variableKey(group) - spacer(": ") - variableValue(playtime.sumByCategory(group).toString()) - - for ((serverName, playtime) in groupServer) { - appendNewPrefixedLine { - text(" ") - variableKey(serverName) - spacer(": ") - variableValue(playtime.toString()) - } - } - appendNewPrefixedLine() - } - } - } - } - } - } - - commandAPICommand("lastSeen") { - offlinePlayerArgument("player") - anyExecutor { sender, args -> - val player: OfflinePlayer by args - val cloudPlayer = player.toCloudOfflinePlayer() - launch { - val lastSeen = cloudPlayer.lastSeen() - sender.sendText { - appendPrefix() - info("Last seen for player ${player.name} (${player.uniqueId})") - appendNewPrefixedLine { - variableKey("Last Seen") - spacer(": ") - variableValue(lastSeen?.toString() ?: "#Unknown") - } - } - } - } - } - - commandAPICommand("currentSessionDuration") { - entitySelectorArgumentOnePlayer("player") - anyExecutor { sender, args -> - val player: Player by args - val cloudPlayer = player.toCloudPlayer() - launch { - val currentSessionDuration = cloudPlayer?.currentSessionDuration() - sender.sendText { - appendPrefix() - info("Current session duration for player ${player.name} (${player.uniqueId})") - appendNewPrefixedLine { - variableKey("Current Session Duration") - spacer(": ") - variableValue(currentSessionDuration?.toString() ?: "#Unknown") - } - } - } - } - } } @OptIn(ExperimentalContracts::class) @@ -387,7 +135,7 @@ class BukkitMain : SuspendingJavaPlugin() { try { bukkitCloudInstance.onDisable() } catch (t: Throwable) { - t.handleEventuallyFatalError({ }) + t.handleEventuallyFatalError {} } } diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt index 910f5dc8..4c268546 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt @@ -13,6 +13,7 @@ import dev.slne.surf.surfapi.core.api.messages.Colors import dev.slne.surf.surfapi.core.api.messages.adventure.sendText import kotlinx.coroutines.launch import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent import org.bukkit.command.CommandSender import java.util.function.BiFunction @@ -136,10 +137,14 @@ private fun executeBroadcast( group: String? = null ) = plugin.launch{ val prefix = prefix ?: Colors.PREFIX - val message = message.replaceText { - it.match("(?m)^|\\A") - .replacement(prefix) - .replaceInsideHoverEvents(false) + val message = if (message is TextComponent && !message.content().contains("\n")) { + prefix.append(message) + } else { + message.replaceText { + it.match("(?m)^|\\A") + .replacement(prefix) + .replaceInsideHoverEvents(false) + } } launch { diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt index c5aafd7b..f8819cb3 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/lastseen/LastSeenCommand.kt @@ -12,6 +12,7 @@ import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.surfapi.core.api.messages.adventure.buildText import dev.slne.surf.surfapi.core.api.messages.adventure.sendText import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder +import kotlinx.coroutines.Deferred import org.bukkit.command.CommandSender import java.time.ZoneId import java.time.ZonedDateTime @@ -30,13 +31,14 @@ fun lastSeenCommand() = commandTree("lastseen") { withPermission(CloudPermissionRegistry.LAST_SEEN_COMMAND) offlineCloudPlayerArgument("player") { anyExecutor { sender, args -> - val player: OfflineCloudPlayer? by args - player?.let { sendLastSeen(sender, it) } + val player: Deferred by args + sendLastSeen(sender, player) } } } -private fun sendLastSeen(sender: CommandSender, player: OfflineCloudPlayer) = plugin.launch { +private fun sendLastSeen(sender: CommandSender, player: Deferred) = plugin.launch { + val player = player.await() ?: return@launch val lastSeen = player.lastSeen() val onlinePlayer = player.player diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/exception/SurfFatalErrorExceptionListener.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/exception/SurfFatalErrorExceptionListener.kt index dfcac66c..e24323e5 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/exception/SurfFatalErrorExceptionListener.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/exception/SurfFatalErrorExceptionListener.kt @@ -17,6 +17,6 @@ object SurfFatalErrorExceptionListener : Listener { if (e.cause?.handle() == true) return } - private fun Throwable.handle() = handleEventuallyFatalError(handler, false, false) + private fun Throwable.handle() = handleEventuallyFatalError(false, false, handler) } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt index 7dc2d618..11d4e9d5 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/player/SilentDisconnectListener.kt @@ -21,11 +21,6 @@ object SilentDisconnectListener : NmsClientboundPacketListener return PacketListenerResult.CANCEL } - fun silentDisconnect(player: BukkitClientCloudPlayerImpl) { - val player = player.audience ?: return - silentDisconnect(player) - } - fun silentDisconnect(player: Player) { silentDisconnects.add(player.uniqueId) player.kick() diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt index 6b54e54d..2b4e26a4 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/netty/network/BukkitSpecificPacketListenerExtension.kt @@ -30,7 +30,8 @@ object BukkitSpecificPacketListenerExtension : PlatformSpecificPacketListenerExt } override fun disconnectPlayer(playerUuid: UUID, reason: Component) { - error("Requested wrong server! This packet can only be acknowledged on a proxy!") + val player = Bukkit.getPlayer(playerUuid) ?: return + player.kick(reason) } override fun silentDisconnectPlayer(playerUuid: UUID) { diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt index 39c680a9..f5e64a6d 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/player/BukkitClientCloudPlayerImpl.kt @@ -11,6 +11,7 @@ import dev.slne.surf.cloud.bukkit.listener.player.SilentDisconnectListener import dev.slne.surf.cloud.bukkit.plugin import dev.slne.surf.cloud.core.client.player.ClientCloudPlayerImpl import dev.slne.surf.cloud.core.common.netty.network.protocol.running.DisconnectPlayerPacket +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.SilentDisconnectPlayerPacket import kotlinx.coroutines.future.await import net.kyori.adventure.text.Component import org.bukkit.Bukkit @@ -29,14 +30,9 @@ class BukkitClientCloudPlayerImpl(uuid: UUID, name: String) : ClientCloudPlayerI } override fun disconnectSilent() { - val player = audience - - if (player == null) { - // TODO: 13.04.2025 14:05 - send packet - return - } - - SilentDisconnectListener.silentDisconnect(this) + SilentDisconnectListener.silentDisconnect( + audience ?: return SilentDisconnectPlayerPacket(uuid).fireAndForget() + ) } fun test() { diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/CloudCoreInstance.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/CloudCoreInstance.kt index 804993ce..393b93e4 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/CloudCoreInstance.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/CloudCoreInstance.kt @@ -201,9 +201,9 @@ inline fun FatalSurfError.handle(additionalHandling: (FatalSurfError) -> Unit) { } inline fun Throwable.handleEventuallyFatalError( - additionalHandling: (FatalSurfError) -> Unit, log: Boolean = true, - handleTimeout: Boolean = true + handleTimeout: Boolean = true, + additionalHandling: (FatalSurfError) -> Unit ): Boolean { if (this is OutOfMemoryError) { throw this diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/Bootstrap.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/Bootstrap.kt index 5cf22719..57635bad 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/Bootstrap.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/Bootstrap.kt @@ -40,14 +40,14 @@ object Bootstrap { } }) } catch (e: Throwable) { - e.handleEventuallyFatalError({ + e.handleEventuallyFatalError { val context = standaloneCloudInstance.dataContext if (context.isActive) { SpringApplication.exit(context, it) } else { exitProcess(it.exitCode) } - }) + } } } } \ No newline at end of file diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt index daf5c18b..3307fab5 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt @@ -135,7 +135,8 @@ class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) } override fun disconnect(reason: Component) { - proxyServer?.connection?.send(DisconnectPlayerPacket(uuid, reason)) + val connection = proxyServer?.connection ?: server?.connection + connection?.send(DisconnectPlayerPacket(uuid, reason)) } override fun disconnectSilent() { diff --git a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/VelocityMain.kt b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/VelocityMain.kt index 9ec60fc9..c4b169ac 100644 --- a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/VelocityMain.kt +++ b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/VelocityMain.kt @@ -40,7 +40,7 @@ class VelocityMain @Inject constructor( velocityCloudInstance.onLoad() } } catch (e: Throwable) { - e.handleEventuallyFatalError({ exitProcess(it.exitCode) }) + e.handleEventuallyFatalError { exitProcess(it.exitCode) } } } @@ -50,7 +50,7 @@ class VelocityMain @Inject constructor( velocityCloudInstance.onEnable() velocityCloudInstance.afterStart() } catch (e: Throwable) { - e.handleEventuallyFatalError({ exitProcess(it.exitCode) }) + e.handleEventuallyFatalError { exitProcess(it.exitCode) } } } @@ -59,7 +59,7 @@ class VelocityMain @Inject constructor( try { velocityCloudInstance.onDisable() } catch (e: Throwable) { - e.handleEventuallyFatalError({}) + e.handleEventuallyFatalError {} } } diff --git a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt index 7d71044c..71af1391 100644 --- a/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt +++ b/surf-cloud-velocity/src/main/kotlin/dev/slne/surf/cloud/velocity/netty/network/VelocitySpecificPacketListenerExtension.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.future.await import net.kyori.adventure.text.Component import java.net.InetSocketAddress import java.util.* +import kotlin.jvm.optionals.getOrNull object VelocitySpecificPacketListenerExtension : PlatformSpecificPacketListenerExtension { override fun isServerManagedByThisProxy(address: InetSocketAddress) = @@ -38,8 +39,7 @@ object VelocitySpecificPacketListenerExtension : PlatformSpecificPacketListenerE } override fun disconnectPlayer(playerUuid: UUID, reason: Component) { - val player = - proxy.getPlayer(playerUuid).orElseThrow { error("Player $playerUuid not found") } + val player = proxy.getPlayer(playerUuid).getOrNull() ?: return player.disconnect(reason) } From a014afcadd7e177e862a0a5df3de36b1b96bebed Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 14 Apr 2025 18:44:37 +0200 Subject: [PATCH 6/7] feat: add icon.svg and update .gitignore to include icon file --- .gitignore | 1 + .idea/icon.svg | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .idea/icon.svg diff --git a/.gitignore b/.gitignore index 7ed408b0..0db925fa 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ bin/ !**/src/test/**/bin/ ### IntelliJ IDEA ### +!.idea/icon.svg .idea *.iws *.iml diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 00000000..36443774 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + From 88d6f33ca3f43a2d5d2b65fa52b835c89e112b78 Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 14 Apr 2025 18:51:17 +0200 Subject: [PATCH 7/7] feat: rename silent disconnect command to timeout and update related functionality --- .../dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt | 4 ++-- .../command/connection/SilentDisconnectPlayerCommand.kt | 4 ++-- .../surf/cloud/bukkit/permission/CloudPermissionRegistry.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt index d41bc785..f84a7185 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt @@ -2,7 +2,7 @@ package dev.slne.surf.cloud.bukkit.command import dev.slne.surf.cloud.bukkit.command.broadcast.broadcastCommand import dev.slne.surf.cloud.bukkit.command.connection.disconnectPlayerCommand -import dev.slne.surf.cloud.bukkit.command.connection.silentDisconnectPlayerCommand +import dev.slne.surf.cloud.bukkit.command.connection.timeoutPlayerCommand import dev.slne.surf.cloud.bukkit.command.lastseen.lastSeenCommand import dev.slne.surf.cloud.bukkit.command.network.findCommand import dev.slne.surf.cloud.bukkit.command.network.glistCommand @@ -19,7 +19,7 @@ object PaperCommandManager { lastSeenCommand() broadcastCommand() glistCommand() - silentDisconnectPlayerCommand() + timeoutPlayerCommand() disconnectPlayerCommand() } } \ No newline at end of file diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt index f7a61b7b..157c74b4 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/connection/SilentDisconnectPlayerCommand.kt @@ -8,8 +8,8 @@ import dev.slne.surf.cloud.api.common.player.CloudPlayer import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry import dev.slne.surf.surfapi.core.api.messages.adventure.sendText -fun silentDisconnectPlayerCommand() = commandTree("silentdisconnect") { // TODO: 13.04.2025 14:15 - better name - withPermission(CloudPermissionRegistry.SILENT_DISCONNECT_COMMAND) +fun timeoutPlayerCommand() = commandTree("timeout") { + withPermission(CloudPermissionRegistry.TIMEOUT_COMMAND) onlineCloudPlayerArgument("player") { anyExecutor { sender, args -> diff --git a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt index ba568861..86a7d9d3 100644 --- a/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt +++ b/surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/permission/CloudPermissionRegistry.kt @@ -15,5 +15,5 @@ object CloudPermissionRegistry: PermissionRegistry() { val LAST_SEEN_COMMAND = create("$COMMAND_PREFIX.lastseen") val BROADCAST_COMMAND = create("$COMMAND_PREFIX.broadcast") val GLIST_COMMAND = create("$COMMAND_PREFIX.glist") - val SILENT_DISCONNECT_COMMAND = create("$COMMAND_PREFIX.silentdisconnect") + val TIMEOUT_COMMAND = create("$COMMAND_PREFIX.timeout") } \ No newline at end of file