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