Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit a04f540

Browse files
committed
feat: implement silent disconnect command and related functionality
1 parent 61b223f commit a04f540

File tree

32 files changed

+475
-30
lines changed

32 files changed

+475
-30
lines changed

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
110110
*/
111111
fun disconnect(reason: Component)
112112

113+
/**
114+
* Disconnects the player from the network silently.
115+
*
116+
* The player will think they are timed out.
117+
*/
118+
fun disconnectSilent()
119+
113120
/**
114121
* Teleports the player to a specified location.
115122
*

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/server/CloudServerManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ interface CloudServerManager {
5252
*/
5353
suspend fun retrieveServerByName(name: String): CommonCloudServer?
5454

55+
suspend fun retrieveServersInGroup(group: String): ObjectList<out CommonCloudServer>
56+
5557
@InternalApi
5658
fun getServerByNameUnsafe(name: String): CloudServer?
5759

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/util.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.slne.surf.cloud.api.common.util
22

3+
import it.unimi.dsi.fastutil.objects.ObjectList
34
import kotlinx.coroutines.Deferred
45
import kotlinx.coroutines.async
56
import kotlinx.coroutines.coroutineScope
@@ -146,7 +147,7 @@ private fun createFileDeletedCheck(path: Path): () -> Boolean = {
146147

147148
fun Method.isSuspending() = kotlinFunction?.isSuspend == true
148149

149-
suspend inline fun <T, R> Iterable<T>.mapAsync(crossinline transform: suspend (T) -> R): List<Deferred<R>> =
150+
suspend inline fun <T, R> Iterable<T>.mapAsync(crossinline transform: suspend (T) -> R): ObjectList<Deferred<R>> =
150151
coroutineScope {
151-
map { async { transform(it) } }
152+
mapTo(mutableObjectListOf()) { async { transform(it) } }
152153
}

surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/PaperCommandManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package dev.slne.surf.cloud.bukkit.command
22

33
import dev.slne.surf.cloud.bukkit.command.broadcast.broadcastCommand
4+
import dev.slne.surf.cloud.bukkit.command.connection.disconnectPlayerCommand
5+
import dev.slne.surf.cloud.bukkit.command.connection.silentDisconnectPlayerCommand
46
import dev.slne.surf.cloud.bukkit.command.lastseen.lastSeenCommand
57
import dev.slne.surf.cloud.bukkit.command.network.findCommand
8+
import dev.slne.surf.cloud.bukkit.command.network.glistCommand
69
import dev.slne.surf.cloud.bukkit.command.network.sendCommand
710
import dev.slne.surf.cloud.bukkit.command.network.serverCommand
811
import dev.slne.surf.cloud.bukkit.command.playtime.playtimeCommand
@@ -15,5 +18,8 @@ object PaperCommandManager {
1518
playtimeCommand()
1619
lastSeenCommand()
1720
broadcastCommand()
21+
glistCommand()
22+
silentDisconnectPlayerCommand()
23+
disconnectPlayerCommand()
1824
}
1925
}

surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/command/broadcast/BroadcastCommand.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument
88
import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument
99
import dev.slne.surf.cloud.api.common.server.CloudServer
1010
import dev.slne.surf.cloud.api.common.server.CloudServerManager
11+
import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry
1112
import dev.slne.surf.cloud.bukkit.plugin
1213
import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument
1314
import dev.slne.surf.surfapi.core.api.messages.Colors
@@ -17,6 +18,8 @@ import org.bukkit.command.CommandSender
1718

1819
@Suppress("DuplicatedCode")
1920
fun broadcastCommand() = commandTree("broadcast") {
21+
withPermission(CloudPermissionRegistry.BROADCAST_COMMAND)
22+
2023
literalArgument("--server") {
2124
cloudServerArgument("server") {
2225
literalArgument("--message") {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dev.slne.surf.cloud.bukkit.command.connection
2+
3+
import dev.jorel.commandapi.kotlindsl.anyExecutor
4+
import dev.jorel.commandapi.kotlindsl.argument
5+
import dev.jorel.commandapi.kotlindsl.commandTree
6+
import dev.jorel.commandapi.kotlindsl.getValue
7+
import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument
8+
import dev.slne.surf.cloud.api.common.player.CloudPlayer
9+
import dev.slne.surf.surfapi.bukkit.api.command.args.MiniMessageArgument
10+
import dev.slne.surf.surfapi.core.api.messages.CommonComponents
11+
import dev.slne.surf.surfapi.core.api.messages.adventure.appendNewline
12+
import dev.slne.surf.surfapi.core.api.messages.adventure.buildText
13+
import dev.slne.surf.surfapi.core.api.messages.adventure.sendText
14+
import net.kyori.adventure.text.Component
15+
import org.bukkit.command.CommandSender
16+
17+
fun disconnectPlayerCommand() = commandTree("disconnect") {
18+
onlineCloudPlayerArgument("player") {
19+
anyExecutor { sender, args ->
20+
val player: CloudPlayer by args
21+
disconnect(sender, player)
22+
}
23+
24+
argument(MiniMessageArgument("reason")) {
25+
anyExecutor { sender, args ->
26+
val player: CloudPlayer by args
27+
val reason: Component by args
28+
disconnect(sender, player, reason)
29+
}
30+
}
31+
}
32+
}
33+
34+
private fun disconnect(sender: CommandSender, player: CloudPlayer, reason: Component? = null) {
35+
val reason = reason ?: buildText {
36+
appendDisconnectHeader()
37+
error("DU WURDEST VOM NETZWERK GEWORFEN")
38+
appendNewline(3)
39+
append(CommonComponents.RETRY_LATER_FOOTER)
40+
}
41+
42+
player.disconnect(reason)
43+
sender.sendText {
44+
appendPrefix()
45+
success("Der Spielende ")
46+
variableValue(player.name)
47+
success(" wurde erfolgreich vom Netzwerk getrennt.")
48+
}
49+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.slne.surf.cloud.bukkit.command.connection
2+
3+
import dev.jorel.commandapi.kotlindsl.anyExecutor
4+
import dev.jorel.commandapi.kotlindsl.commandTree
5+
import dev.jorel.commandapi.kotlindsl.getValue
6+
import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument
7+
import dev.slne.surf.cloud.api.common.player.CloudPlayer
8+
import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry
9+
import dev.slne.surf.surfapi.core.api.messages.adventure.sendText
10+
11+
fun silentDisconnectPlayerCommand() = commandTree("silentdisconnect") { // TODO: 13.04.2025 14:15 - better name
12+
withPermission(CloudPermissionRegistry.SILENT_DISCONNECT_COMMAND)
13+
14+
onlineCloudPlayerArgument("player") {
15+
anyExecutor { sender, args ->
16+
val player: CloudPlayer by args
17+
player.disconnectSilent()
18+
sender.sendText {
19+
appendPrefix()
20+
success("Der Spielende ")
21+
variableValue(player.name)
22+
success(" wurde erfolgreich still getrennt.")
23+
}
24+
}
25+
}
26+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package dev.slne.surf.cloud.bukkit.command.network
2+
3+
import com.github.shynixn.mccoroutine.folia.launch
4+
import dev.jorel.commandapi.kotlindsl.anyExecutor
5+
import dev.jorel.commandapi.kotlindsl.commandTree
6+
import dev.jorel.commandapi.kotlindsl.getValue
7+
import dev.jorel.commandapi.kotlindsl.literalArgument
8+
import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerArgument
9+
import dev.slne.surf.cloud.api.client.paper.command.args.cloudServerGroupArgument
10+
import dev.slne.surf.cloud.api.common.player.CloudPlayerManager
11+
import dev.slne.surf.cloud.api.common.server.CloudServer
12+
import dev.slne.surf.cloud.api.common.server.CloudServerManager
13+
import dev.slne.surf.cloud.api.common.server.UserList
14+
import dev.slne.surf.cloud.api.common.util.mapAsync
15+
import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf
16+
import dev.slne.surf.cloud.bukkit.permission.CloudPermissionRegistry
17+
import dev.slne.surf.cloud.bukkit.plugin
18+
import dev.slne.surf.surfapi.core.api.messages.Colors
19+
import dev.slne.surf.surfapi.core.api.messages.adventure.sendText
20+
import dev.slne.surf.surfapi.core.api.messages.joinToComponent
21+
import it.unimi.dsi.fastutil.objects.Object2ObjectMap
22+
import kotlinx.coroutines.awaitAll
23+
import net.kyori.adventure.text.Component
24+
import org.bukkit.command.CommandSender
25+
import dev.slne.surf.surfapi.core.api.messages.adventure.text as component
26+
27+
fun glistCommand() = commandTree("glist") {
28+
withPermission(CloudPermissionRegistry.GLIST_COMMAND)
29+
30+
anyExecutor { sender, args ->
31+
sender.sendText {
32+
appendPrefix()
33+
info("Es sind gerade ")
34+
variableValue(CloudPlayerManager.getOnlinePlayers().size)
35+
info(" Spielende auf dem Netzwerk online.")
36+
}
37+
}
38+
39+
literalArgument("all") {
40+
anyExecutor { sender, args ->
41+
plugin.launch {
42+
displayOnlinePlayers(
43+
sender,
44+
CloudServerManager.retrieveAllServers().filterIsInstance<CloudServer>()
45+
)
46+
}
47+
}
48+
}
49+
50+
literalArgument("group") {
51+
cloudServerGroupArgument("group") {
52+
anyExecutor { sender, args ->
53+
val group: String by args
54+
plugin.launch {
55+
displayOnlinePlayers(
56+
sender,
57+
CloudServerManager.retrieveServersInGroup(group)
58+
.filterIsInstance<CloudServer>()
59+
)
60+
}
61+
}
62+
}
63+
}
64+
65+
literalArgument("server") {
66+
cloudServerArgument("server") {
67+
anyExecutor { sender, args ->
68+
val server: CloudServer by args
69+
plugin.launch {
70+
displayOnlinePlayers(sender, listOf(server))
71+
}
72+
}
73+
}
74+
}
75+
}
76+
77+
private suspend fun displayOnlinePlayers(sender: CommandSender, servers: Collection<CloudServer>) {
78+
sender.sendText {
79+
appendNewPrefixedLine()
80+
if (servers.size == 1) {
81+
val server = servers.first()
82+
83+
variableKey("${server.name} (")
84+
variableKey(server.currentPlayerCount)
85+
variableKey("): ")
86+
append(server.users.displayNames())
87+
} else {
88+
val groupedServers = servers.groupBy { it.group }.withDisplayNames()
89+
for ((group, serversWithName) in groupedServers) {
90+
if (serversWithName.isEmpty()) continue
91+
variableKey("$group:")
92+
appendNewPrefixedLine()
93+
for ((server, names) in serversWithName) {
94+
variableKey(" ${server.name} (")
95+
variableValue(server.currentPlayerCount)
96+
variableKey("): ")
97+
append(names)
98+
appendNewPrefixedLine()
99+
}
100+
}
101+
}
102+
103+
if (servers.size != 1) {
104+
val totalUsers = servers.sumOf { it.currentPlayerCount }
105+
appendNewPrefixedLine()
106+
variableKey("Insgesamt: ")
107+
variableValue(totalUsers)
108+
}
109+
}
110+
}
111+
112+
private suspend fun Map<String, List<CloudServer>>.withDisplayNames(): Object2ObjectMap<String, List<Pair<CloudServer, Component>>> =
113+
mapValuesTo(mutableObject2ObjectMapOf(size)) { (_, servers) ->
114+
servers.mapAsync { server ->
115+
server to server.users.displayNames()
116+
}.awaitAll()
117+
}
118+
119+
120+
private suspend fun UserList.displayNames() =
121+
mapAsync { it.displayName().hoverEvent(component(it.uuid, Colors.VARIABLE_VALUE)) }
122+
.awaitAll()
123+
.joinToComponent { it }

surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/listener/ListenerManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ package dev.slne.surf.cloud.bukkit.listener
22

33
import dev.slne.surf.cloud.bukkit.listener.exception.SurfFatalErrorExceptionListener
44
import dev.slne.surf.cloud.bukkit.listener.player.ConnectionListener
5+
import dev.slne.surf.cloud.bukkit.listener.player.SilentDisconnectListener
56
import dev.slne.surf.cloud.bukkit.plugin
67
import dev.slne.surf.surfapi.bukkit.api.event.register
8+
import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
9+
import dev.slne.surf.surfapi.bukkit.api.nms.nmsBridge
710
import org.bukkit.event.HandlerList
811

12+
@OptIn(NmsUseWithCaution::class)
913
object ListenerManager {
1014

1115
fun registerListeners() {
1216
ConnectionListener.register()
1317
SurfFatalErrorExceptionListener.register()
18+
nmsBridge.registerClientboundPacketListener(SilentDisconnectListener)
1419
}
1520

1621
fun unregisterListeners() {
1722
HandlerList.unregisterAll(plugin)
23+
nmsBridge.unregisterClientboundPacketListener(SilentDisconnectListener)
1824
}
1925
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package dev.slne.surf.cloud.bukkit.listener.player
2+
3+
import dev.slne.surf.cloud.api.common.util.mutableObjectSetOf
4+
import dev.slne.surf.cloud.bukkit.player.BukkitClientCloudPlayerImpl
5+
import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
6+
import dev.slne.surf.surfapi.bukkit.api.nms.listener.NmsClientboundPacketListener
7+
import dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.clientbound.DisconnectPacket
8+
import dev.slne.surf.surfapi.bukkit.api.packet.listener.listener.PacketListenerResult
9+
import org.bukkit.entity.Player
10+
import java.util.*
11+
12+
@OptIn(NmsUseWithCaution::class)
13+
object SilentDisconnectListener : NmsClientboundPacketListener<DisconnectPacket> {
14+
private val silentDisconnects = mutableObjectSetOf<UUID>()
15+
16+
override fun handleClientboundPacket(
17+
packet: DisconnectPacket,
18+
player: Player
19+
): PacketListenerResult {
20+
if (!silentDisconnects.remove(player.uniqueId)) return PacketListenerResult.CONTINUE
21+
return PacketListenerResult.CANCEL
22+
}
23+
24+
fun silentDisconnect(player: BukkitClientCloudPlayerImpl) {
25+
val player = player.audience ?: return
26+
silentDisconnect(player)
27+
}
28+
29+
fun silentDisconnect(player: Player) {
30+
silentDisconnects.add(player.uniqueId)
31+
player.kick()
32+
}
33+
}

0 commit comments

Comments
 (0)