diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ArgumentPath.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ArgumentPath.kt index ac25e666..ddf7bcef 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ArgumentPath.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ArgumentPath.kt @@ -4,7 +4,6 @@ import com.github.spigotbasics.common.Either import com.github.spigotbasics.core.Basics import com.github.spigotbasics.core.command.parsed.arguments.CommandArgument import com.github.spigotbasics.core.command.parsed.context.CommandContext -import com.github.spigotbasics.core.extensions.lastOrEmpty import com.github.spigotbasics.core.logger.BasicsLoggerFactory import com.github.spigotbasics.core.messages.Message import org.bukkit.command.CommandSender @@ -216,36 +215,109 @@ class ArgumentPath( sender: CommandSender, input: List, ): Boolean { - logger.debug(200, "TabComplete matchesStart: input: $input @ $this") - if (input.size > arguments.size) { - logger.debug(200, " input.size > arguments.size") - return false - } - input.forEachIndexed { index, s -> - logger.debug(200, " Checking index $index, s: $s") - if (index == input.size - 1) { - logger.debug(200, " Last argument, skipping") - return@forEachIndexed - } // Last argument is still typing - val arg = arguments[index].second - val parsed = arg.parse(sender, s) - if (parsed == null) { - logger.debug(200, " parsed == null") + if (!isCorrectSender(sender) || !hasPermission(sender)) return false + + var accumulatedInputCount = 0 + for ((index, pair) in arguments.withIndex()) { + val (_, argument) = pair + val isLastArgument = index == arguments.size - 1 + + // Calculate the expected count of inputs for this argument + val expectedCount = if (argument.greedy) input.size - accumulatedInputCount else argument.length + val availableInputs = input.drop(accumulatedInputCount).take(expectedCount) + val argumentInput = availableInputs.joinToString(" ") + + // Update the accumulated input count for the next argument + accumulatedInputCount += availableInputs.size + + // If it's not the last argument and parsing fails, return false + if (!isLastArgument && argument.parse(sender, argumentInput) == null) { return false } + + // If it's the last argument, it's okay if parse returns null (user might still be typing) + if (isLastArgument) { + // If there's enough input for the argument to potentially parse, return true (assume user might complete it) + // If argument is greedy, always return true since we're accommodating partial input + if (argument.greedy || argument.parse(sender, argumentInput) != null) { + return true + } else { + // For non-greedy last argument, it's okay if parsing fails due to partial input + return availableInputs.size >= argument.length + } + } } - logger.debug(200, " All arguments parsed, this path matches the input") + + // If the loop completes without returning, all arguments except possibly the last have parsed successfully return true } +// fun matchesStart( +// sender: CommandSender, +// input: List, +// ): Boolean { +// if (!isCorrectSender(sender) || !hasPermission(sender)) return false +// +// var currentIndex = 0 +// arguments.forEachIndexed { index, (_, argument) -> +// if (!argument.greedy) { +// // Check if current argument can be fully covered by remaining input +// if (currentIndex + argument.length > input.size) { +// // If this is the last argument or a greedy argument is next and the user is still typing it, partial input is okay +// if (index == arguments.size - 1 || (arguments.getOrNull(index + 1)?.second?.greedy ?: false)) { +// return true // Accept partial input for the last non-greedy argument or before a greedy one +// } +// return false // Not enough input for this non-greedy argument +// } +// currentIndex += argument.length +// } else { +// // For a greedy argument, ensure at least its minimum length is available +// if (currentIndex + argument.length > input.size) { +// // Allow partial input for greedy arguments if it's the last argument +// return index == arguments.size - 1 +// } +// // Greedy argument consumes the rest; no need to check further +// return true +// } +// } +// +// // If all non-greedy arguments up to the last one were satisfied with correct length, or a greedy argument had enough input +// return true +// } + fun tabComplete( sender: CommandSender, - args: List, + input: List, ): List { - if (args.isEmpty() || args.size > arguments.size) return emptyList() + if (!isCorrectSender(sender) || !hasPermission(sender)) return emptyList() + + // Identify the argument the user is currently completing + val currentIndex = input.size - 1 + var accumulatedIndex = 0 + + arguments.forEachIndexed { index, (argName, argument) -> + val isLastArgument = index == arguments.size - 1 + val isGreedyArgument = argument.greedy + + // Calculate the start and end index for the current argument's input + val startIndex = accumulatedIndex + var endIndex = accumulatedIndex + argument.length + + // If the argument is greedy, or we are at the last argument, adjust endIndex to include all remaining input + if (isGreedyArgument || isLastArgument) endIndex = input.size + + // Update accumulatedIndex for the next iteration + accumulatedIndex += argument.length + + // Check if the current argument is the one being completed + if (currentIndex >= startIndex && currentIndex < endIndex) { + // For greedy and last arguments, include all remaining input; otherwise, limit to the argument's length + val relevantInput = if (currentIndex < input.size) input.subList(startIndex, endIndex).joinToString(" ") else "" + return argument.tabComplete(sender, relevantInput) + } + } - val currentArgIndex = args.size - 1 - return arguments[currentArgIndex].second.tabComplete(sender, args.lastOrEmpty()) + return emptyList() } fun isCorrectSender(sender: CommandSender): Boolean { diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorEntityArgBase.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorEntityArgBase.kt index 41d1c6f3..1b466b02 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorEntityArgBase.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorEntityArgBase.kt @@ -10,7 +10,11 @@ import org.bukkit.command.CommandSender import org.bukkit.entity.Entity import org.bukkit.entity.Player -abstract class SelectorEntityArgBase(name: String) : CommandArgument(name) { +abstract class SelectorEntityArgBase( + name: String, + private val allowMultiple: Boolean, + private val allowEntities: Boolean, +) : CommandArgument(name) { protected enum class ErrorType { NOT_FOUND, NO_PERMISSION_SELECTORS, @@ -27,21 +31,39 @@ abstract class SelectorEntityArgBase(name: String) : CommandArgument(name) sender: CommandSender, typing: String, ): List { + val playerNames = getPlayerNames(sender) + val selectors = getSelectors(sender) + return (playerNames + selectors).partialMatches(typing) + } + + private fun getPlayerNames(sender: CommandSender): List { return Bukkit.getOnlinePlayers().filter { if (sender is Player) { sender.canSee(it) } else { true } - }.map { it.name }.partialMatches(typing) + }.map { it.name } + } + + private fun getSelectors(sender: CommandSender): List { + if (!sender.hasPermission(selectorPermission)) { + return emptyList() + } + val list = mutableListOf("@p", "@r", "@s") + if (allowEntities && allowMultiple) { + list.add("@e") + } + if (allowMultiple) { + list.add("@a") + } + return list } // TODO: Additional canSee check for all matched players? protected fun get( sender: CommandSender, value: String, - allowMultiple: Boolean, - allowEntities: Boolean, ): Either> { val onePlayer = Bukkit.getPlayer(value) if (onePlayer != null) { @@ -75,10 +97,8 @@ abstract class SelectorEntityArgBase(name: String) : CommandArgument(name) protected fun errorMessage0( sender: CommandSender, value: String, - allowMultiple: Boolean, - allowEntities: Boolean, ): Message { - return when (get(sender, value, allowMultiple, allowEntities).leftOrNull()) { + return when (get(sender, value).leftOrNull()) { ErrorType.NOT_FOUND -> Basics.messages.playerNotFound(value) ErrorType.NOT_FOUND_ENTITY -> Basics.messages.selectorMatchesNoEntities(name, value) ErrorType.NO_PERMISSION_SELECTORS -> Basics.messages.noPermission(selectorPermission) diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiEntityArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiEntityArg.kt index 8e6651fb..58ed35af 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiEntityArg.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiEntityArg.kt @@ -4,12 +4,12 @@ import com.github.spigotbasics.core.messages.Message import org.bukkit.command.CommandSender import org.bukkit.entity.Entity -class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase>(name) { +class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase>(name, allowMultiple = true, allowEntities = true) { override fun parse( sender: CommandSender, value: String, ): List? { - return get(sender, value, allowMultiple = true, allowEntities = true).fold( + return get(sender, value).fold( { _ -> null }, { it }, ) @@ -19,6 +19,6 @@ class SelectorMultiEntityArg(name: String) : SelectorEntityArgBase> sender: CommandSender, value: String, ): Message { - return errorMessage0(sender, value, allowMultiple = true, allowEntities = true) + return errorMessage0(sender, value) } } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiPlayerArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiPlayerArg.kt index 4831449b..da628d76 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiPlayerArg.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorMultiPlayerArg.kt @@ -4,12 +4,12 @@ import com.github.spigotbasics.core.messages.Message import org.bukkit.command.CommandSender import org.bukkit.entity.Player -class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase>(name) { +class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase>(name, allowMultiple = true, allowEntities = false) { override fun parse( sender: CommandSender, value: String, ): List? { - return get(sender, value, allowMultiple = true, allowEntities = false).fold( + return get(sender, value).fold( { _ -> null }, { it }, )?.map { it as Player } @@ -19,6 +19,6 @@ class SelectorMultiPlayerArg(name: String) : SelectorEntityArgBase> sender: CommandSender, value: String, ): Message { - return errorMessage0(sender, value, allowMultiple = true, allowEntities = false) + return errorMessage0(sender, value) } } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSingleEntityArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSingleEntityArg.kt index 15828d69..46ae50a7 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSingleEntityArg.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSingleEntityArg.kt @@ -4,18 +4,18 @@ import com.github.spigotbasics.core.messages.Message import org.bukkit.command.CommandSender import org.bukkit.entity.Entity -class SelectorSingleEntityArg(name: String) : SelectorEntityArgBase(name) { +class SelectorSingleEntityArg(name: String) : SelectorEntityArgBase(name, allowMultiple = false, allowEntities = true) { override fun parse( sender: CommandSender, value: String, ): Entity? { - return get(sender, value, allowMultiple = false, allowEntities = true).rightOrNull()?.singleOrNull() + return get(sender, value).rightOrNull()?.singleOrNull() } override fun errorMessage( sender: CommandSender, value: String, ): Message { - return errorMessage0(sender, value, allowMultiple = false, allowEntities = true) + return errorMessage0(sender, value) } } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSinglePlayerArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSinglePlayerArg.kt index dfcd5d26..535356f2 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSinglePlayerArg.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/SelectorSinglePlayerArg.kt @@ -4,18 +4,18 @@ import com.github.spigotbasics.core.messages.Message import org.bukkit.command.CommandSender import org.bukkit.entity.Player -class SelectorSinglePlayerArg(name: String) : SelectorEntityArgBase(name) { +class SelectorSinglePlayerArg(name: String) : SelectorEntityArgBase(name, allowMultiple = false, allowEntities = false) { override fun parse( sender: CommandSender, value: String, ): Player? { - return get(sender, value, allowMultiple = false, allowEntities = false).rightOrNull()?.singleOrNull() as Player? + return get(sender, value).rightOrNull()?.singleOrNull() as Player? } override fun errorMessage( sender: CommandSender, value: String, ): Message { - return errorMessage0(sender, value, allowMultiple = false, allowEntities = false) + return errorMessage0(sender, value) } } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/TripleContextCoordinatesArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/TripleContextCoordinatesArg.kt index 26dbadb8..d0d7e449 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/TripleContextCoordinatesArg.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/TripleContextCoordinatesArg.kt @@ -1,9 +1,16 @@ package com.github.spigotbasics.core.command.parsed.arguments +import com.github.spigotbasics.core.extensions.lastOrEmpty +import com.github.spigotbasics.core.extensions.partialMatches +import com.github.spigotbasics.core.logger.BasicsLoggerFactory import com.github.spigotbasics.core.model.TripleContextCoordinates import org.bukkit.command.CommandSender class TripleContextCoordinatesArg(name: String) : CommandArgument(name) { + companion object { + val logger = BasicsLoggerFactory.getCoreLogger(TripleContextCoordinatesArg::class) + } + override fun parse( sender: CommandSender, value: String, @@ -19,10 +26,16 @@ class TripleContextCoordinatesArg(name: String) : CommandArgument { - // println(typing) - // TODO: Add selectors and ~ ~~ for tabcomplete if has permission - // TODO: That requires passing the concat-ed string to the tabComplete method - return super.tabComplete(sender, typing) + logger.debug(1, typing) + + val split = typing.split(" ") + if (split.size > 5) return emptyList() + for (previous in split.dropLast(1)) { + if (!TripleContextCoordinates.isValidSinglePart(previous)) { + return emptyList() + } + } + return listOf("~", "~~").partialMatches(split.lastOrEmpty()) } override val greedy = true diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/model/TripleContextCoordinates.kt b/core/src/main/kotlin/com/github/spigotbasics/core/model/TripleContextCoordinates.kt index 219bac13..d55297f9 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/model/TripleContextCoordinates.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/model/TripleContextCoordinates.kt @@ -103,5 +103,11 @@ data class TripleContextCoordinates( Pair(string.toDouble(), Relativity.ABSOLUTE) } } + + private val validSinglePartRegex = Regex("^(~|~~)?-?\\d*(\\.\\d+)?$") + + fun isValidSinglePart(string: String): Boolean { + return string.matches(validSinglePartRegex) + } } } diff --git a/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/BasicsCoreModule.kt b/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/BasicsCoreModule.kt index 5def6459..e5a7224b 100644 --- a/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/BasicsCoreModule.kt +++ b/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/BasicsCoreModule.kt @@ -1,6 +1,7 @@ package com.github.spigotbasics.modules.basicscore import com.github.spigotbasics.core.command.parsed.arguments.IntRangeArg +import com.github.spigotbasics.core.command.parsed.arguments.TripleContextCoordinatesArg import com.github.spigotbasics.core.module.AbstractBasicsModule import com.github.spigotbasics.core.module.loader.ModuleInstantiationContext import com.github.spigotbasics.modules.basicscore.commands.CreateGiveCommand @@ -141,5 +142,14 @@ class BasicsCoreModule(context: ModuleInstantiationContext) : AbstractBasicsModu } } .register() + + commandFactory.parsedCommandBuilder("tabtest", debugPermission) + .mapContext { + path { + arguments { + named("test", TripleContextCoordinatesArg("Test")) + } + } + }.executor(TabTestCommand()).register() } } diff --git a/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/TabTestCommand.kt b/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/TabTestCommand.kt new file mode 100644 index 00000000..bfe11a5e --- /dev/null +++ b/modules/basics-core/src/main/kotlin/com/github/spigotbasics/modules/basicscore/TabTestCommand.kt @@ -0,0 +1,15 @@ +package com.github.spigotbasics.modules.basicscore + +import com.github.spigotbasics.core.command.parsed.CommandContextExecutor +import com.github.spigotbasics.core.command.parsed.context.MapContext +import org.bukkit.command.CommandSender + +class TabTestCommand : + CommandContextExecutor { + override fun execute( + sender: CommandSender, + context: MapContext, + ) { + sender.sendMessage("TabTestCommand executed: $context") + } +}