Skip to content

Commit c904a4f

Browse files
committed
feat: add SuspendCustomArgument for asynchronous command argument parsing
1 parent df4e958 commit c904a4f

File tree

1 file changed

+96
-0
lines changed
  • surf-api-velocity/surf-api-velocity-api/src/main/kotlin/dev/slne/surf/surfapi/velocity/api/command/args

1 file changed

+96
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package dev.slne.surf.surfapi.velocity.api.command.args
2+
3+
import com.mojang.brigadier.context.CommandContext
4+
import com.velocitypowered.api.command.CommandSource
5+
import dev.jorel.commandapi.CommandAPIHandler
6+
import dev.jorel.commandapi.arguments.Argument
7+
import dev.jorel.commandapi.arguments.CommandAPIArgumentType
8+
import dev.jorel.commandapi.executors.CommandArguments
9+
import kotlinx.coroutines.*
10+
import net.kyori.adventure.text.logger.slf4j.ComponentLogger
11+
12+
/**
13+
* Base class for custom CommandAPI arguments whose parsing logic is executed asynchronously
14+
* using Kotlin coroutines.
15+
*
16+
* This abstraction allows implementing suspendable parsing logic while still integrating
17+
* with CommandAPI's synchronous argument parsing pipeline by returning a [Deferred].
18+
*
19+
* @param T The final parsed argument type.
20+
* @param B The base argument type produced by the underlying [Argument].
21+
* @property base The underlying base argument used for initial parsing.
22+
* @property scope The [CoroutineScope] used to execute the suspendable parsing logic.
23+
*/
24+
abstract class SuspendCustomArgument<T, B>(
25+
private val base: Argument<B>,
26+
private val scope: CoroutineScope = defaultScope
27+
) : Argument<Deferred<T>>(base.nodeName, base.rawType) {
28+
29+
/**
30+
* Parses the argument asynchronously.
31+
*
32+
* This method is executed inside the provided [CoroutineScope] and may perform
33+
* suspendable or asynchronous operations such as database lookups or network calls.
34+
*
35+
* Implementations should throw [com.mojang.brigadier.exceptions.CommandSyntaxException] / [dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException]
36+
* to signal a user-facing parsing error.
37+
*
38+
* @param info Contextual information about the argument being parsed.
39+
* @return The parsed argument value.
40+
*/
41+
abstract suspend fun CoroutineScope.parse(info: CustomArgumentInfo<B>): T
42+
43+
/**
44+
* Synchronously invoked by CommandAPI to parse the argument.
45+
*
46+
* Internally, this method delegates to [parse] by launching a coroutine in [scope]
47+
* and returning a [Deferred] representing the eventual parsing result.
48+
*
49+
* @param cmdCtx The Brigadier command context.
50+
* @param key The argument node name.
51+
* @param previousArgs Previously parsed command arguments.
52+
* @return A [Deferred] that completes with the parsed argument value.
53+
*/
54+
final override fun <CommandSourceStack : Any> parseArgument(
55+
cmdCtx: CommandContext<CommandSourceStack>,
56+
key: String,
57+
previousArgs: CommandArguments
58+
): Deferred<T> {
59+
val customResult = CommandAPIHandler.getRawArgumentInput(cmdCtx, key)
60+
val parsedInput = base.parseArgument(cmdCtx, key, previousArgs)
61+
62+
return scope.async {
63+
val sender = cmdCtx.source as CommandSource
64+
val info = CustomArgumentInfo(sender, previousArgs, customResult, parsedInput)
65+
parse(info)
66+
}
67+
}
68+
69+
override fun getArgumentType(): CommandAPIArgumentType = base.argumentType
70+
71+
@JvmRecord
72+
data class CustomArgumentInfo<B>(
73+
val sender: CommandSource,
74+
val previousArgs: CommandArguments,
75+
val input: String,
76+
val currentInput: B
77+
)
78+
79+
companion object {
80+
private val logger = ComponentLogger.logger()
81+
82+
private val defaultScopeExceptionHandler = CoroutineExceptionHandler { _, t ->
83+
if (t is CancellationException) return@CoroutineExceptionHandler
84+
logger.atError()
85+
.setCause(t)
86+
.log("Uncaught exception in command parsing coroutine")
87+
}
88+
89+
private val defaultScope = CoroutineScope(
90+
Dispatchers.IO.limitedParallelism(16) // We are not planning to do blocking work but preparing in case a developer messes up
91+
+ SupervisorJob()
92+
+ CoroutineName("Velocity Default Command Parsing Scope")
93+
+ defaultScopeExceptionHandler
94+
)
95+
}
96+
}

0 commit comments

Comments
 (0)