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

Commit 560a06e

Browse files
committed
feat: implement player group management and enhance server commands
1 parent 14d22b3 commit 560a06e

File tree

32 files changed

+1282
-52
lines changed

32 files changed

+1282
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.slne.surf.cloud.api.client.server
2+
3+
import dev.slne.surf.cloud.api.common.server.CloudServer
4+
import dev.slne.surf.cloud.api.common.server.CloudServerManager
5+
import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
6+
7+
interface CloudClientServerManager : CloudServerManager {
8+
fun currentServer(): CloudServer
9+
10+
@OptIn(InternalApi::class)
11+
companion object :
12+
CloudClientServerManager by CloudServerManager.instance as CloudClientServerManager
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package dev.slne.surf.cloud.api.client.paper.command.args
2+
3+
import dev.jorel.commandapi.CommandAPICommand
4+
import dev.jorel.commandapi.CommandTree
5+
import dev.jorel.commandapi.arguments.Argument
6+
import dev.jorel.commandapi.arguments.ArgumentSuggestions
7+
import dev.jorel.commandapi.arguments.CustomArgument
8+
import dev.jorel.commandapi.arguments.StringArgument
9+
import dev.slne.surf.cloud.api.common.server.CloudServer
10+
import dev.slne.surf.cloud.api.common.server.CloudServerManager
11+
import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
12+
import dev.slne.surf.surfapi.core.api.util.logger
13+
import kotlinx.coroutines.CoroutineExceptionHandler
14+
import kotlinx.coroutines.CoroutineName
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.future.future
18+
19+
20+
@OptIn(InternalApi::class)
21+
class CloudServerArgument(nodeName: String) : CustomArgument<CloudServer, String>(
22+
StringArgument(nodeName),
23+
{ info ->
24+
val serverName = info.input
25+
val server = CloudServerManager.getServerByNameUnsafe(serverName) ?: run {
26+
throw CustomArgumentException.fromMessageBuilder(
27+
MessageBuilder()
28+
.append("Server '")
29+
.appendArgInput()
30+
.append("' not found")
31+
)
32+
}
33+
server
34+
}
35+
) {
36+
init {
37+
replaceSuggestions(ArgumentSuggestions.stringCollectionAsync {
38+
scope.future {
39+
CloudServerManager.retrieveAllServers()
40+
.filterIsInstance<CloudServer>()
41+
.map { it.name }
42+
}
43+
})
44+
}
45+
46+
private companion object {
47+
private val log = logger()
48+
private val scope =
49+
CoroutineScope(Dispatchers.Default + CoroutineName("CloudServerSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable ->
50+
log.atWarning()
51+
.withCause(throwable)
52+
.log("Failed to suggest cloud servers")
53+
})
54+
}
55+
}
56+
57+
inline fun CommandTree.cloudServerArgument(
58+
nodeName: String,
59+
optional: Boolean = false,
60+
block: Argument<*>.() -> Unit = {}
61+
): CommandTree = then(CloudServerArgument(nodeName).setOptional(optional).apply(block))
62+
63+
inline fun Argument<*>.cloudServerArgument(
64+
nodeName: String,
65+
optional: Boolean = false,
66+
block: Argument<*>.() -> Unit = {}
67+
): Argument<*> = then(CloudServerArgument(nodeName).setOptional(optional).apply(block))
68+
69+
inline fun CommandAPICommand.cloudServerArgument(
70+
nodeName: String,
71+
optional: Boolean = false,
72+
block: Argument<*>.() -> Unit = {}
73+
): CommandAPICommand = withArguments(CloudServerArgument(nodeName).setOptional(optional).apply(block))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package dev.slne.surf.cloud.api.client.paper.command.args
2+
3+
import dev.jorel.commandapi.CommandAPICommand
4+
import dev.jorel.commandapi.CommandTree
5+
import dev.jorel.commandapi.arguments.Argument
6+
import dev.jorel.commandapi.arguments.ArgumentSuggestions
7+
import dev.jorel.commandapi.arguments.CustomArgument
8+
import dev.jorel.commandapi.arguments.TextArgument
9+
import dev.slne.surf.cloud.api.common.server.CloudServerManager
10+
import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
11+
import dev.slne.surf.surfapi.core.api.util.logger
12+
import kotlinx.coroutines.CoroutineExceptionHandler
13+
import kotlinx.coroutines.CoroutineName
14+
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.Dispatchers
16+
import kotlinx.coroutines.future.future
17+
18+
@OptIn(InternalApi::class)
19+
class CloudServerGroupArgument(nodeName: String) : CustomArgument<String, String>(
20+
TextArgument(nodeName),
21+
{ info ->
22+
val groupName = info.currentInput
23+
val exists = CloudServerManager.existsServerGroup(groupName)
24+
if (!exists) {
25+
throw CustomArgumentException.fromMessageBuilder(
26+
MessageBuilder()
27+
.append("Server group '")
28+
.appendArgInput()
29+
.append("' not found or no servers in this group are online!")
30+
)
31+
}
32+
groupName
33+
}
34+
) {
35+
init {
36+
replaceSuggestions(ArgumentSuggestions.stringCollectionAsync {
37+
scope.future {
38+
CloudServerManager.retrieveAllServers()
39+
.filter { it.group.isNotEmpty() }
40+
.map { it.group }
41+
.distinct()
42+
}
43+
})
44+
}
45+
46+
companion object {
47+
private val log = logger()
48+
private val scope =
49+
CoroutineScope(Dispatchers.Default + CoroutineName("CloudServerGroupSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable ->
50+
log.atWarning()
51+
.withCause(throwable)
52+
.log("Failed to suggest cloud server groups")
53+
})
54+
}
55+
}
56+
57+
inline fun CommandTree.cloudServerGroupArgument(
58+
nodeName: String,
59+
optional: Boolean = false,
60+
block: Argument<*>.() -> Unit = {}
61+
): CommandTree = then(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block))
62+
63+
inline fun Argument<*>.cloudServerGroupArgument(
64+
nodeName: String,
65+
optional: Boolean = false,
66+
block: Argument<*>.() -> Unit = {}
67+
): Argument<*> = then(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block))
68+
69+
inline fun CommandAPICommand.cloudServerGroupArgument(
70+
nodeName: String,
71+
optional: Boolean = false,
72+
block: Argument<*>.() -> Unit = {}
73+
): CommandAPICommand = withArguments(CloudServerGroupArgument(nodeName).setOptional(optional).apply(block))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package dev.slne.surf.cloud.api.client.paper.command.args
2+
3+
import dev.jorel.commandapi.CommandAPICommand
4+
import dev.jorel.commandapi.CommandTree
5+
import dev.jorel.commandapi.arguments.Argument
6+
import dev.jorel.commandapi.arguments.ArgumentSuggestions
7+
import dev.jorel.commandapi.arguments.AsyncOfflinePlayerArgument
8+
import dev.jorel.commandapi.arguments.CustomArgument
9+
import dev.slne.surf.cloud.api.client.paper.player.toCloudOfflinePlayer
10+
import dev.slne.surf.cloud.api.common.player.CloudPlayerManager
11+
import dev.slne.surf.cloud.api.common.player.OfflineCloudPlayer
12+
import dev.slne.surf.surfapi.core.api.messages.adventure.sendText
13+
import dev.slne.surf.surfapi.core.api.util.logger
14+
import kotlinx.coroutines.*
15+
import kotlinx.coroutines.future.await
16+
import kotlinx.coroutines.future.future
17+
import org.bukkit.OfflinePlayer
18+
import java.util.concurrent.CompletableFuture
19+
20+
class OfflineCloudPlayerArgument(nodeName: String) :
21+
CustomArgument<Deferred<OfflineCloudPlayer?>, CompletableFuture<OfflinePlayer>>(
22+
AsyncOfflinePlayerArgument(nodeName),
23+
{ info ->
24+
scope.async {
25+
try {
26+
val player = info.currentInput.await()
27+
player.toCloudOfflinePlayer()
28+
} catch (e: RuntimeException) {
29+
val cause = e.cause
30+
val rootCause = if (cause is RuntimeException) cause.cause else cause
31+
32+
info.sender.sendText {
33+
error(
34+
rootCause?.message
35+
?: "Unknown error occurred while fetching offline player"
36+
)
37+
}
38+
null
39+
}
40+
}
41+
}
42+
) {
43+
init {
44+
includeSuggestions(ArgumentSuggestions.stringCollectionAsync {
45+
scope.future {
46+
CloudPlayerManager.getOnlinePlayers().map { it.name }
47+
}
48+
})
49+
}
50+
51+
companion object {
52+
private val log = logger()
53+
private val scope =
54+
CoroutineScope(Dispatchers.IO + CoroutineName("OfflineCloudPlayerArgument") + CoroutineExceptionHandler { _, throwable ->
55+
log.atWarning()
56+
.withCause(throwable)
57+
.log("An error occurred in OfflineCloudPlayerArgument")
58+
})
59+
}
60+
}
61+
62+
inline fun CommandTree.offlineCloudPlayerArgument(
63+
nodeName: String,
64+
optional: Boolean = false,
65+
block: Argument<*>.() -> Unit = {}
66+
): CommandTree = then(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))
67+
68+
inline fun Argument<*>.offlineCloudPlayerArgument(
69+
nodeName: String,
70+
optional: Boolean = false,
71+
block: Argument<*>.() -> Unit = {}
72+
): Argument<*> = then(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))
73+
74+
inline fun CommandAPICommand.offlineCloudPlayerArgument(
75+
nodeName: String,
76+
optional: Boolean = false,
77+
block: Argument<*>.() -> Unit = {}
78+
): CommandAPICommand =
79+
withArguments(OfflineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package dev.slne.surf.cloud.api.client.paper.command.args
2+
3+
import dev.jorel.commandapi.CommandAPICommand
4+
import dev.jorel.commandapi.CommandTree
5+
import dev.jorel.commandapi.arguments.*
6+
import dev.slne.surf.cloud.api.common.player.CloudPlayer
7+
import dev.slne.surf.cloud.api.common.player.CloudPlayerManager
8+
import dev.slne.surf.surfapi.core.api.util.logger
9+
import kotlinx.coroutines.CoroutineExceptionHandler
10+
import kotlinx.coroutines.CoroutineName
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.future.future
14+
15+
private val log = logger()
16+
private val scope =
17+
CoroutineScope(Dispatchers.Default + CoroutineName("OnlineCloudPlayerSuggestionScope") + CoroutineExceptionHandler { coroutineContext, throwable ->
18+
log.atWarning()
19+
.withCause(throwable)
20+
.log("Failed to suggest online players")
21+
})
22+
23+
class OnlineCloudPlayerArgument(nodeName: String) : CustomArgument<CloudPlayer, String>(
24+
StringArgument(nodeName),
25+
{ info ->
26+
val playerName = info.input
27+
CloudPlayerManager.getPlayer(playerName) ?: run {
28+
throw CustomArgumentException.fromMessageBuilder(
29+
MessageBuilder()
30+
.append("Player '")
31+
.appendArgInput()
32+
.append("' not found")
33+
)
34+
}
35+
}
36+
) {
37+
init {
38+
replaceSuggestions(ArgumentSuggestions.stringCollectionAsync {
39+
scope.future {
40+
CloudPlayerManager.getOnlinePlayers().map { it.name }
41+
}
42+
})
43+
}
44+
}
45+
46+
inline fun CommandTree.onlineCloudPlayerArgument(
47+
nodeName: String,
48+
optional: Boolean = false,
49+
block: Argument<*>.() -> Unit = {}
50+
): CommandTree = then(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))
51+
52+
inline fun Argument<*>.onlineCloudPlayerArgument(
53+
nodeName: String,
54+
optional: Boolean = false,
55+
block: Argument<*>.() -> Unit = {}
56+
): Argument<*> = then(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))
57+
58+
inline fun CommandAPICommand.onlineCloudPlayerArgument(
59+
nodeName: String,
60+
optional: Boolean = false,
61+
block: Argument<*>.() -> Unit = {}
62+
): CommandAPICommand = withArguments(OnlineCloudPlayerArgument(nodeName).setOptional(optional).apply(block))

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause
55
import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag
66
import dev.slne.surf.cloud.api.common.player.teleport.TeleportLocation
77
import dev.slne.surf.cloud.api.common.server.CloudServer
8+
import dev.slne.surf.surfapi.core.api.messages.adventure.buildText
89
import net.kyori.adventure.audience.Audience
910
import net.kyori.adventure.text.Component
1011
import java.net.Inet4Address
@@ -19,6 +20,8 @@ import kotlin.time.Duration
1920
* it enables sending messages or components to the player.
2021
*/
2122
interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but done correctly?
23+
val name: String
24+
2225
override suspend fun latestIpAddress(): Inet4Address
2326
override suspend fun lastServerRaw(): String
2427

@@ -97,6 +100,9 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
97100
*/
98101
suspend fun connectToServerOrQueue(group: String): ConnectionResult
99102

103+
fun isOnServer(server: CloudServer): Boolean
104+
fun isInGroup(group: String): Boolean
105+
100106
/**
101107
* Disconnects the player from the network with a specified reason.
102108
*
@@ -145,27 +151,32 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
145151
vararg flags: TeleportFlag,
146152
) = teleport(TeleportLocation(world, x, y, z, yaw, pitch), teleportCause, *flags)
147153

154+
suspend fun teleport(target: CloudPlayer): Boolean
155+
148156
override suspend fun displayName(): Component
149157
override suspend fun name(): String
150158
}
151159

152160
/**
153161
* Enum representing the result of a player's connection attempt to a server.
154162
*/
155-
enum class ConnectionResultEnum {
156-
SUCCESS,
157-
SERVER_NOT_FOUND,
158-
SERVER_FULL,
159-
CATEGORY_FULL,
160-
SERVER_OFFLINE,
161-
ALREADY_CONNECTED,
162-
CANNOT_SWITCH_PROXY,
163-
OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET,
164-
CANNOT_COMMUNICATE_WITH_PROXY,
165-
CONNECTION_IN_PROGRESS,
166-
CONNECTION_CANCELLED,
167-
SERVER_DISCONNECTED,
168-
CANNOT_CONNECT_TO_PROXY,
163+
enum class ConnectionResultEnum(
164+
val message: Component,
165+
val isSuccess: Boolean = false
166+
) {
167+
SUCCESS(buildText { success("Du hast dich erfolgreich Verbunden.") }, isSuccess = true),
168+
SERVER_NOT_FOUND(buildText { error("Der Server wurde nicht gefunden.") }),
169+
SERVER_FULL(buildText { error("Der Server ist voll.") }),
170+
CATEGORY_FULL(buildText { error("Die Kategorie ist voll.") }),
171+
SERVER_OFFLINE(buildText { error("Der Server ist offline.") }),
172+
ALREADY_CONNECTED(buildText { error("Du bist bereits mit diesem Server verbunden.") }),
173+
CANNOT_SWITCH_PROXY(buildText { error("Du kannst nicht zu diesem Server wechseln, da dieser unter einem anderen Proxy läuft.") }),
174+
OTHER_SERVER_CANNOT_ACCEPT_TRANSFER_PACKET(buildText { error("Der Server kann das Transfer-Paket nicht akzeptieren.") }),
175+
CANNOT_COMMUNICATE_WITH_PROXY(buildText { error("Der Proxy kann nicht erreicht werden.") }),
176+
CONNECTION_IN_PROGRESS(buildText { error("Du versucht bereits eine Verbindung zu einem Server herzustellen.") }),
177+
CONNECTION_CANCELLED(buildText { error("Die Verbindung wurde abgebrochen.") }),
178+
SERVER_DISCONNECTED(buildText { error("Der Server hat die Verbindung getrennt.") }),
179+
CANNOT_CONNECT_TO_PROXY(buildText { error("Der Proxy kann nicht erreicht werden.") }),
169180
}
170181

171182
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ interface CloudPlayerManager {
2727
*/
2828
fun getPlayer(uuid: UUID?): CloudPlayer?
2929

30+
fun getPlayer(name: String): CloudPlayer?
31+
3032
fun getOfflinePlayer(uuid: UUID): OfflineCloudPlayer
3133

3234
fun getOnlinePlayers(): UserList

0 commit comments

Comments
 (0)