1+ package dev.slne.surf.surfapi.bukkit.api.command.args
2+
3+ import com.github.shynixn.mccoroutine.folia.SuspendingPlugin
4+ import com.github.shynixn.mccoroutine.folia.scope
5+ import com.mojang.brigadier.context.CommandContext
6+ import dev.jorel.commandapi.CommandAPIBukkit
7+ import dev.jorel.commandapi.CommandAPIHandler
8+ import dev.jorel.commandapi.arguments.Argument
9+ import dev.jorel.commandapi.arguments.CustomArgument
10+ import dev.jorel.commandapi.executors.CommandArguments
11+ import dev.slne.surf.surfapi.bukkit.api.util.getCallingPlugin
12+ import kotlinx.coroutines.CoroutineScope
13+ import kotlinx.coroutines.Deferred
14+ import kotlinx.coroutines.async
15+
16+ /* *
17+ * Base class for custom CommandAPI arguments whose parsing logic is executed asynchronously
18+ * using Kotlin coroutines.
19+ *
20+ * This abstraction allows implementing suspendable parsing logic while still integrating
21+ * with CommandAPI's synchronous argument parsing pipeline by returning a [Deferred].
22+ *
23+ * @param T The final parsed argument type.
24+ * @param B The base argument type produced by the underlying [Argument].
25+ * @property base The underlying base argument used for initial parsing.
26+ * @property scope The [CoroutineScope] used to execute the suspendable parsing logic.
27+ */
28+ abstract class SuspendCustomArgument <T , B >(
29+ private val base : Argument <B >,
30+ private val scope : CoroutineScope = extractCallingPluginScopeOrThrow()
31+ ) : CustomArgument<Deferred<T>, B>(base, { _ ->
32+ throw UnsupportedOperationException ("Parsing is performed asynchronously via the overridden parse method")
33+ }) {
34+
35+ /* *
36+ * Parses the argument asynchronously.
37+ *
38+ * This method is executed inside the provided [CoroutineScope] and may perform
39+ * suspendable or asynchronous operations such as database lookups or network calls.
40+ *
41+ * Implementations should throw [CustomArgumentException] to signal a user-facing
42+ * parsing error.
43+ *
44+ * @param info Contextual information about the argument being parsed.
45+ * @return The parsed argument value.
46+ * @throws CustomArgumentException If parsing fails with a user-facing error.
47+ */
48+ abstract suspend fun CoroutineScope.parse (info : CustomArgumentInfo <B >): T
49+
50+ /* *
51+ * Synchronously invoked by CommandAPI to parse the argument.
52+ *
53+ * Internally, this method delegates to [parse] by launching a coroutine in [scope]
54+ * and returning a [Deferred] representing the eventual parsing result.
55+ *
56+ * Any thrown [CustomArgumentException] is converted into a Brigadier
57+ * [com.mojang.brigadier.exceptions.CommandSyntaxException].
58+ *
59+ * @param cmdCtx The Brigadier command context.
60+ * @param key The argument node name.
61+ * @param previousArgs Previously parsed command arguments.
62+ * @return A [Deferred] that completes with the parsed argument value.
63+ */
64+ final override fun <CommandSourceStack : Any > parseArgument (
65+ cmdCtx : CommandContext <CommandSourceStack >,
66+ key : String ,
67+ previousArgs : CommandArguments
68+ ): Deferred <T > {
69+ val customResult = CommandAPIHandler .getRawArgumentInput(cmdCtx, key)
70+ val parsedInput = base.parseArgument(cmdCtx, key, previousArgs)
71+
72+ return scope.async {
73+ try {
74+ val sender = CommandAPIBukkit .get<CommandSourceStack >()
75+ .getCommandSenderFromCommandSource(cmdCtx.source).source
76+ val info = CustomArgumentInfo (sender, previousArgs, customResult, parsedInput)
77+ parse(info)
78+ } catch (e: CustomArgumentException ) {
79+ throw e.toCommandSyntax(customResult, cmdCtx)
80+ }
81+ }
82+ }
83+
84+ companion object {
85+ /* *
86+ * Extracts the [CoroutineScope] of the calling plugin.
87+ *
88+ * The calling plugin must implement [SuspendingPlugin]. If it does not,
89+ * an exception is thrown to prevent executing coroutines without
90+ * proper lifecycle management.
91+ *
92+ * @return The plugin's coroutine scope.
93+ * @throws IllegalArgumentException If the plugin does not implement [SuspendingPlugin].
94+ */
95+ private fun extractCallingPluginScopeOrThrow (): CoroutineScope {
96+ val plugin = getCallingPlugin(2 )
97+
98+ require(plugin is SuspendingPlugin ) {
99+ " Failed to extract CoroutineScope: plugin '${plugin.name} ' does not implement SuspendingPlugin. " +
100+ " Provide a CoroutineScope explicitly or use a SuspendingPlugin."
101+ }
102+
103+ return plugin.scope
104+ }
105+ }
106+ }
0 commit comments