diff --git a/common/src/main/kotlin/com/github/spigotbasics/common/Dictionary.kt b/common/src/main/kotlin/com/github/spigotbasics/common/Dictionary.kt index 246fb742..a65cde6c 100644 --- a/common/src/main/kotlin/com/github/spigotbasics/common/Dictionary.kt +++ b/common/src/main/kotlin/com/github/spigotbasics/common/Dictionary.kt @@ -8,4 +8,26 @@ import java.util.TreeMap * @param T The type of the values in the map. * @constructor Create empty Dictionary */ -class Dictionary : MutableMap, TreeMap(String.CASE_INSENSITIVE_ORDER) +class Dictionary : MutableMap, TreeMap(String.CASE_INSENSITIVE_ORDER) { + companion object { + fun fromEnum( + enumClass: Class>, + lowercase: Boolean = false, + ): Dictionary> { + val dictionary = Dictionary>() + for (enumConstant in enumClass.enumConstants) { + val name = if (lowercase) enumConstant.name.lowercase() else enumConstant.name + dictionary[name] = enumConstant + } + return dictionary + } + + fun from(entries: List>): Dictionary { + val dictionary = Dictionary() + for ((key, value) in entries) { + dictionary[key] = value + } + return dictionary + } + } +} diff --git a/common/src/test/kotlin/com/github/spigotbasics/common/DictionaryTest.kt b/common/src/test/kotlin/com/github/spigotbasics/common/DictionaryTest.kt index eed87b00..8d2c6d5b 100644 --- a/common/src/test/kotlin/com/github/spigotbasics/common/DictionaryTest.kt +++ b/common/src/test/kotlin/com/github/spigotbasics/common/DictionaryTest.kt @@ -25,4 +25,18 @@ class DictionaryTest { assertNull(map["test2"]) assertNull(map["TEST2"]) } + + @Test + fun replacesProperly() { + val map = Dictionary() + map["aaa"] = object { } + map["AAA"] = object { } + map["aAa"] = object { } + assertEquals(1, map.size) + + map["aab"] = object { } + map["AAB"] = object { } + map["aAb"] = object { } + assertEquals(2, map.size) + } } 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 new file mode 100644 index 00000000..c2c84eae --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ArgumentPath.kt @@ -0,0 +1,118 @@ +package com.github.spigotbasics.core.command.parsed + +import com.github.spigotbasics.common.Either +import com.github.spigotbasics.core.Basics +import com.github.spigotbasics.core.extensions.lastOrEmpty +import com.github.spigotbasics.core.messages.Message +import org.bukkit.command.CommandSender +import org.bukkit.permissions.Permission + +class ArgumentPath( + val senderArgument: SenderType<*>, + val arguments: List>, + // TODO: Check permission for specific paths! + val permission: List = emptyList(), + private val contextBuilder: (CommandSender, List) -> T, +) { +// fun matches(args: List): Boolean { +// if (args.size > arguments.size) return false +// +// for ((index, arg) in args.withIndex()) { +// if (arguments[index].parse(arg) == null) return false +// } +// +// return true +// } + + fun matches( + sender: CommandSender, + args: List, + ): Either> { + // Exact match for the number of arguments + if (args.size > arguments.size) { + return Either.Left( + PathMatchResult.NO, + ) // Maybe use != ? > allows to show "missing item", != wouldn't + } + // TODO: Keep a list of non-matches where size is too little, and if no other errors occur, say "missing item", only otherwise + // fallback to CommandResult.USAGE + + // Each provided arg must be parseable by its corresponding CommandArgument + val errors = mutableListOf() + // val matches = // used to be all(...) + args.indices.forEach { index -> + val parsed = arguments[index].parse(args[index]) + + if (parsed == null) { + val error = arguments[index].errorMessage(args[index]) + errors.add(error) + } + + // true + } + + if (errors.isNotEmpty()) return Either.Right(errors) + + if (!senderArgument.requiredType.isInstance(sender)) return Either.Left(PathMatchResult.YES_BUT_NOT_FROM_CONSOLE) + if (!hasPermission(sender)) return Either.Left(PathMatchResult.YES_BUT_NO_PERMISSION) + return Either.Left(PathMatchResult.YES) + } + +// fun parse(args: List): T? { +// if (!matches(args)) return null +// val parsedArgs = +// arguments.zip(args).mapNotNull { (arg, value) -> +// arg.parse(value) +// } +// return contextBuilder(parsedArgs) +// } + + fun parse( + sender: CommandSender, + args: List, + ): ParseResult { + if (!senderArgument.requiredType.isInstance(sender)) { + return ParseResult.Failure(listOf(Basics.messages.commandNotFromConsole)) + } + + val parsedArgs = mutableListOf() + val errors = mutableListOf() + + for ((index, arg) in arguments.withIndex()) { + if (index >= args.size) { +// errors.add("Missing argument for ${arg.name}") + errors.add(Basics.messages.missingArgument(arg.name)) + break + } + + val parsed = arg.parse(args[index]) + if (parsed == null) { + errors.add(arg.errorMessage(args[index])) + break + } else { + parsedArgs.add(parsed) + } + } + + if (errors.isEmpty() && parsedArgs.size == arguments.size) { + return ParseResult.Success(contextBuilder(sender, parsedArgs)) + } else { + return ParseResult.Failure(errors) + } + } + + fun tabComplete(args: List): List { + if (args.isEmpty() || args.size > arguments.size) return emptyList() + + val currentArgIndex = args.size - 1 + return arguments[currentArgIndex].tabComplete(args.lastOrEmpty()) + } + + fun isCorrectSender(sender: CommandSender): Boolean { + return senderArgument.requiredType.isInstance(sender) + } + + fun hasPermission(sender: CommandSender): Boolean { + return permission.all { sender.hasPermission(it) } + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandArgument.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandArgument.kt new file mode 100644 index 00000000..38d8860c --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandArgument.kt @@ -0,0 +1,13 @@ +package com.github.spigotbasics.core.command.parsed + +import com.github.spigotbasics.core.Basics +import com.github.spigotbasics.core.messages.Message + +abstract class CommandArgument(val name: String) { + abstract fun parse(value: String): T? + + open fun tabComplete(typing: String): List = emptyList() + + // TODO: This is using the static Singleton :/ + open fun errorMessage(value: String? = null): Message = Basics.messages.invalidValueForArgument(name, value ?: "null") +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandContext.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandContext.kt new file mode 100644 index 00000000..8b0646df --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandContext.kt @@ -0,0 +1,3 @@ +package com.github.spigotbasics.core.command.parsed + +interface CommandContext diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandExecutor.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandExecutor.kt new file mode 100644 index 00000000..715d721b --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/CommandExecutor.kt @@ -0,0 +1,10 @@ +package com.github.spigotbasics.core.command.parsed + +import org.bukkit.command.CommandSender + +interface CommandExecutor { + fun execute( + sender: CommandSender, + context: T, + ) +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParseResult.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParseResult.kt new file mode 100644 index 00000000..4de058be --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParseResult.kt @@ -0,0 +1,9 @@ +package com.github.spigotbasics.core.command.parsed + +import com.github.spigotbasics.core.messages.Message + +sealed class ParseResult { + data class Success(val context: T) : ParseResult() + + data class Failure(val errors: List) : ParseResult() +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandBuilder.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandBuilder.kt new file mode 100644 index 00000000..d32d8d30 --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandBuilder.kt @@ -0,0 +1,98 @@ +package com.github.spigotbasics.core.command + +import com.github.spigotbasics.common.Either +import com.github.spigotbasics.core.command.parsed.ArgumentPath +import com.github.spigotbasics.core.command.parsed.CommandContext +import com.github.spigotbasics.core.command.parsed.CommandExecutor +import com.github.spigotbasics.core.command.parsed.ParsedCommandExecutor +import com.github.spigotbasics.core.messages.Message +import com.github.spigotbasics.core.module.BasicsModule +import org.bukkit.permissions.Permission + +class ParsedCommandBuilder( + private val module: BasicsModule, + private val name: String, + private val permission: Permission, +) { + private var permissionMessage: Message = module.plugin.messages.noPermission + private var description: String? = null + private var usage: String = "" + private var aliases: List = emptyList() + private var executor: BasicsCommandExecutor? = null + private var tabCompleter: BasicsTabCompleter? = null + private var parsedExecutor: CommandExecutor? = null + private var argumentPaths: List>? = null + + fun description(description: String) = apply { this.description = description } + + fun usage(usage: String) = + apply { + if (usage.startsWith("/")) error("Usage should not start with / - only pass the arguments.") + this.usage = usage + } + + fun paths(argumentPaths: List>) = apply { this.argumentPaths = argumentPaths } + + fun paths(vararg argumentPaths: ArgumentPath) = apply { this.argumentPaths = argumentPaths.toList() } + + fun executor(executor: CommandExecutor) = apply { this.parsedExecutor = executor } + + private fun executor(executor: BasicsCommandExecutor) = apply { this.executor = executor } + + private fun executor(command: ParsedCommandExecutor) = + apply { + this.executor = + object : BasicsCommandExecutor(module) { + override fun execute(context: BasicsCommandContext): CommandResult? { + val result = command.execute(context.sender, context.args) + + if (result is Either.Left) { + return result.value + } + + if (result is Either.Right) { + val failure = result.value + // TODO: Proper messages + failure.errors.forEach { it.sendToSender(context.sender) } + } + + return null + } + + override fun tabComplete(context: BasicsCommandContext): MutableList { + return command.tabComplete(context.sender, context.args).toMutableList() + } + } + } + + fun register(): BasicsCommand { + val command = build() + module.commandManager.registerCommand(command) + return command + } + + private fun build(): BasicsCommand { + val command = + ParsedCommandExecutor( + parsedExecutor ?: error("parsedExecutor must be set"), + argumentPaths ?: error("Argument paths must be set"), + ) + executor(command) + val info = + CommandInfo( + name = name, + permission = permission, + permissionMessage = permissionMessage, + description = description, + usage = usage, + aliases = aliases, + ) + return BasicsCommand( + info = info, + executor = executor ?: error("Executor must be set"), + tabCompleter = tabCompleter ?: executor, + coreMessages = module.plugin.messages, + messageFactory = module.plugin.messageFactory, + ) + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandExecutor.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandExecutor.kt new file mode 100644 index 00000000..63f1aa96 --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/ParsedCommandExecutor.kt @@ -0,0 +1,140 @@ +package com.github.spigotbasics.core.command.parsed + +import com.github.spigotbasics.common.Either +import com.github.spigotbasics.core.command.CommandResult +import com.github.spigotbasics.core.messages.Message +import org.bukkit.command.CommandSender +import org.bukkit.permissions.Permission + +class ParsedCommandExecutor( + private val executor: CommandExecutor, + private val paths: List>, +) { +// fun execute(input: List) { +// for (path in paths) { +// val context = path.parse(input) +// if (context != null) { +// executor.execute(context) +// return +// } +// } +// // Handle no matching path found, e.g., show usage or error message +// } + +// fun execute(input: List): Either { +// for (path in paths) { +// when (val result = path.parse(input)) { +// is ParseResult.Success -> { +// executor.execute(result.context) +// return Either.Left(CommandResult.SUCCESS) +// } +// is ParseResult.Failure -> { +// // Handle or display errors +// result.errors.forEach { println(it) } +// return Either.Right(result) +// } +// } +// } +// // If no paths matched, optionally print a generic error or usage message +// println("Invalid command syntax.") +// return Either.Left(CommandResult.USAGE) +// } + + fun execute( + sender: CommandSender, + input: List, + ): Either { + // Empty args = show usage, unless an empty path is registered + if (input.isEmpty()) { + if (!paths.any { it.arguments.isEmpty() }) { + return Either.Left(CommandResult.USAGE) + } + } + + // Sort paths by the number of arguments they expect, ascending. + val sortedPaths = paths.sortedBy { it.arguments.size } + + var shortestPathFailure: ParseResult.Failure? = null + var bestMatchResult: PathMatchResult? = null + var errors: List? = null + var missingPermission: Permission? = null + + sortedPaths.forEach { path -> + val matchResult = path.matches(sender, input) + if (matchResult is Either.Right) { + // TODO: Maybe collect all error messages? Right now, which error message is shown depends on the order of the paths + // That means more specific ones should be registered first + val newErrors = matchResult.value + if (errors == null || errors!!.size > newErrors.size) { + errors = newErrors + } + } else if (matchResult is Either.Left && + ( + matchResult.value == PathMatchResult.YES_BUT_NOT_FROM_CONSOLE || + matchResult.value == PathMatchResult.YES_BUT_NO_PERMISSION + ) + ) { + bestMatchResult = matchResult.value + if (matchResult.value == PathMatchResult.YES_BUT_NO_PERMISSION) { + missingPermission = path.permission.firstOrNull { !sender.hasPermission(it) } + } + } else if (matchResult is Either.Left && matchResult.value == PathMatchResult.YES) { + when (val result = path.parse(sender, input)) { + is ParseResult.Success -> { + executor.execute(sender, result.context) + // println("Command executed successfully.") + return Either.Left(CommandResult.SUCCESS) + } + + is ParseResult.Failure -> { + // println("Path failed to parse: $result") + // result.errors.forEach { println(it) } // Optionally handle or display errors if necessary for debugging + + // Might comment out the part after || below v v v + if (shortestPathFailure == null || result.errors.size < shortestPathFailure!!.errors.size) { + // println("Shortest path failure: $result") + shortestPathFailure = result + } + } + } + } + } + // If no paths matched, inform the user or handle the failure + + if (shortestPathFailure != null) { + return Either.Right(shortestPathFailure!!) + } + + // TODO: Maybe this must be moved up + if (bestMatchResult == PathMatchResult.YES_BUT_NOT_FROM_CONSOLE) { + return Either.Left(CommandResult.NOT_FROM_CONSOLE) + } + if (bestMatchResult == PathMatchResult.YES_BUT_NO_PERMISSION && missingPermission != null) { + return Either.Left(CommandResult.noPermission(missingPermission!!)) + } + + if (errors != null) { + errors!![0].sendToSender(sender) + return Either.Left(CommandResult.SUCCESS) + } else { + // println("No matching command format found.") + return Either.Left(CommandResult.USAGE) + } + } + + fun tabComplete( + sender: CommandSender, + input: List, + ): List { + // Attempt to find the best matching ArgumentPath for the current input + // and return its tab completions. + val completions = mutableListOf() + for (path in paths) { + if (!path.isCorrectSender(sender)) continue + if (!path.hasPermission(sender)) continue + completions.addAll(path.tabComplete(input)) + } + // Remove duplicates and return + return completions.distinct() + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/PathMatchResult.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/PathMatchResult.kt new file mode 100644 index 00000000..0d54db7f --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/PathMatchResult.kt @@ -0,0 +1,8 @@ +package com.github.spigotbasics.core.command.parsed + +enum class PathMatchResult { + YES, + NO, + YES_BUT_NOT_FROM_CONSOLE, + YES_BUT_NO_PERMISSION, +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/SenderType.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/SenderType.kt new file mode 100644 index 00000000..3508d108 --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/SenderType.kt @@ -0,0 +1,11 @@ +package com.github.spigotbasics.core.command.parsed + +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import kotlin.reflect.KClass + +abstract class SenderType(val requiredType: KClass) + +object PlayerSender : SenderType(Player::class) + +object AnySender : SenderType(CommandSender::class) diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/IntArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/IntArg.kt new file mode 100644 index 00000000..0163d3fb --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/IntArg.kt @@ -0,0 +1,7 @@ +package com.github.spigotbasics.core.command.parsed.arguments + +import com.github.spigotbasics.core.command.parsed.CommandArgument + +class IntArg(name: String) : CommandArgument(name) { + override fun parse(value: String): Int? = value.toIntOrNull() +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/ItemMaterialArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/ItemMaterialArg.kt new file mode 100644 index 00000000..065567ea --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/ItemMaterialArg.kt @@ -0,0 +1,28 @@ +package com.github.spigotbasics.core.command.parsed.arguments + +import com.github.spigotbasics.common.Dictionary +import com.github.spigotbasics.core.command.parsed.CommandArgument +import com.github.spigotbasics.core.extensions.partialMatches +import com.github.spigotbasics.core.logger.BasicsLoggerFactory +import org.bukkit.Material + +class ItemMaterialArg(name: String) : CommandArgument(name) { + companion object { + private val logger = BasicsLoggerFactory.getCoreLogger(ItemMaterialArg::class) + + private val materials = Dictionary.from(Material.entries.filter { it.isItem }.map { it.name to it }) + private val materialNames = materials.keys.toList().sorted() + + init { + logger.info("${materials.size} items loaded from Bukkit's Material class.") + } + } + + override fun parse(value: String): Material? { + return materials[value] + } + + override fun tabComplete(typing: String): List { + return materialNames.partialMatches(typing) + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/PlayerArg.kt b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/PlayerArg.kt new file mode 100644 index 00000000..aeeb5543 --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/command/parsed/arguments/PlayerArg.kt @@ -0,0 +1,23 @@ +package com.github.spigotbasics.core.command.parsed.arguments + +import com.github.spigotbasics.core.Basics +import com.github.spigotbasics.core.command.parsed.CommandArgument +import com.github.spigotbasics.core.extensions.partialMatches +import com.github.spigotbasics.core.messages.Message +import org.bukkit.Bukkit +import org.bukkit.entity.Player + +class PlayerArg(name: String) : CommandArgument(name) { + override fun parse(value: String): Player? { + return Bukkit.getPlayer(value) + } + + override fun tabComplete(typing: String): List { + return Bukkit.getOnlinePlayers().map { it.name }.partialMatches(typing) + } + + // TODO: We need an ArgumentFactory! + override fun errorMessage(value: String?): Message { + return Basics.messages.playerNotFound(value ?: "null") + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/extensions/StringListUtils.kt b/core/src/main/kotlin/com/github/spigotbasics/core/extensions/StringListUtils.kt index d086cdc6..b1ed5b5c 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/extensions/StringListUtils.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/extensions/StringListUtils.kt @@ -12,3 +12,5 @@ fun List.addAnd(value: String): MutableList { list.add(value) return list } + +fun List.lastOrEmpty(): String = if (isEmpty()) "" else last() diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/messages/CoreMessages.kt b/core/src/main/kotlin/com/github/spigotbasics/core/messages/CoreMessages.kt index c9e06d83..a06a04b1 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/messages/CoreMessages.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/messages/CoreMessages.kt @@ -43,4 +43,15 @@ class CoreMessages(context: ConfigInstantiationContext) : SavedConfig(context) { getMessage("error-executing-command") } } + + fun invalidValueForArgument( + argumentName: String, + givenValue: String, + ): Message { + return getMessage("invalid-value-for-argument") + .tagUnparsed("argument", argumentName) + .tagUnparsed("value", givenValue) + } + + fun missingArgument(name: String) = getMessage("missing-value-for-argument").tagParsed("argument", name) } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/messages/tags/providers/ItemStackTag.kt b/core/src/main/kotlin/com/github/spigotbasics/core/messages/tags/providers/ItemStackTag.kt new file mode 100644 index 00000000..cd9fa8c0 --- /dev/null +++ b/core/src/main/kotlin/com/github/spigotbasics/core/messages/tags/providers/ItemStackTag.kt @@ -0,0 +1,26 @@ +package com.github.spigotbasics.core.messages.tags.providers + +import com.github.spigotbasics.core.extensions.toHumanReadable +import com.github.spigotbasics.core.messages.tags.CustomTag +import com.github.spigotbasics.core.messages.tags.MessageTagProvider +import org.bukkit.inventory.ItemStack + +class ItemStackTag(private val item: ItemStack) : MessageTagProvider { + override fun getMessageTags(): List { + val alwaysAvailable = + listOf( + CustomTag.parsed("item-type", item.type.name.toHumanReadable()), + CustomTag.parsed("item-amount", item.amount.toString()), + CustomTag.parsed("item-max-stack-size", item.maxStackSize.toString()), + ) + val onlyWithMeta = mutableListOf() + + // Unparsed tags, because they're player-changeable (ok lore maybe not - but still!) + if (item.hasItemMeta()) { + val meta = item.itemMeta!! + onlyWithMeta.add(CustomTag.unparsed("item-display-name", meta.displayName)) + onlyWithMeta.add(CustomTag.unparsed("item-lore", meta.lore?.joinToString("\n") ?: "")) + } + return alwaysAvailable + onlyWithMeta + } +} diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/module/AbstractBasicsModule.kt b/core/src/main/kotlin/com/github/spigotbasics/core/module/AbstractBasicsModule.kt index 24ecafc5..981ec4ad 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/module/AbstractBasicsModule.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/module/AbstractBasicsModule.kt @@ -3,6 +3,8 @@ package com.github.spigotbasics.core.module import com.github.spigotbasics.core.NamespacedNamespacedKeyFactory import com.github.spigotbasics.core.command.BasicsCommandBuilder import com.github.spigotbasics.core.command.BasicsCommandManager +import com.github.spigotbasics.core.command.ParsedCommandBuilder +import com.github.spigotbasics.core.command.parsed.CommandContext import com.github.spigotbasics.core.config.ConfigName import com.github.spigotbasics.core.config.SavedConfig import com.github.spigotbasics.core.event.BasicsEventBus @@ -150,4 +152,11 @@ abstract class AbstractBasicsModule(context: ModuleInstantiationContext) : Basic ): BasicsCommandBuilder { return BasicsCommandBuilder(this, name, permission) } + + final override fun createParsedCommand( + name: String, + permission: Permission, + ): ParsedCommandBuilder { + return ParsedCommandBuilder(this, name, permission) + } } diff --git a/core/src/main/kotlin/com/github/spigotbasics/core/module/BasicsModule.kt b/core/src/main/kotlin/com/github/spigotbasics/core/module/BasicsModule.kt index e9e4756f..4712e160 100644 --- a/core/src/main/kotlin/com/github/spigotbasics/core/module/BasicsModule.kt +++ b/core/src/main/kotlin/com/github/spigotbasics/core/module/BasicsModule.kt @@ -4,6 +4,8 @@ import com.github.spigotbasics.core.BasicsPlugin import com.github.spigotbasics.core.NamespacedNamespacedKeyFactory import com.github.spigotbasics.core.command.BasicsCommandBuilder import com.github.spigotbasics.core.command.BasicsCommandManager +import com.github.spigotbasics.core.command.ParsedCommandBuilder +import com.github.spigotbasics.core.command.parsed.CommandContext import com.github.spigotbasics.core.config.ConfigName import com.github.spigotbasics.core.config.SavedConfig import com.github.spigotbasics.core.event.BasicsEventBus @@ -153,6 +155,11 @@ interface BasicsModule { permission: Permission, ): BasicsCommandBuilder + fun createParsedCommand( + name: String, + permission: Permission, + ): ParsedCommandBuilder + fun createStorage(name: String? = null): NamespacedStorage fun getConfig(configName: ConfigName): SavedConfig diff --git a/core/src/main/resources/messages-cmarco.yml b/core/src/main/resources/messages-cmarco.yml new file mode 100644 index 00000000..5b8a5b63 --- /dev/null +++ b/core/src/main/resources/messages-cmarco.yml @@ -0,0 +1,35 @@ +# Behold, the reimagining of these mundane messages into the splendid vernacular befitting someone of my unparalleled +# prowess in Java and all things coding. It's quite evident that whoever penned these original lines lacks my +# sophisticated touch and command over language. Let me enlighten you with versions that not only convey the intended +# message but do so with the flair that only I, CMarco, can infuse: + +# Tags: <#permission> +no-permission: "Thou lackest this sacred permission: <#permission>'>Dare not proceed, for thou art unworthy!" + +# Tags: <#option> +unknown-option: "This option is as foreign to me as good code is to thee: color:#FF8888<#option>" + +# Tags: <#argument> <#value> +invalid-value-for-argument: "Thy input is as flawed as thy understanding: <#argument> beareth the cursed value of <#value>" + +# Tags: <#argument> +missing-value-for-argument: "Thou hast forgotten a value for: <#argument>! How typical." +invalid-argument: "Foolishness thy name is, for <#argument> is not of this realm." +player-not-found: "The player thou seeketh exists not within this domain: <#argument>" +world-not-found: "This world is beyond thy reach, lost to the void: <#argument>" +unsupported-server-software: "Thy server, antiquated and feeble, supports not This sacred knowledge is missing: <#argument>'>this divine gift!" + +no-safe-location-found: "Thy quest for a safe haven is but a fool's errand!" +not-having-item-in-hand: "Thou art as empty-handed as thou art of wit!" +others-not-having-item-in-hand: ", much like thyself, holds naught but air." + +command-not-from-console: "This command, noble as it is, rejects the cold embrace of the console!" +must-specify-player-from-console: "From the void of the console, name thy champion!" +command-module-disabled: "This module, unlike my brilliance, lies dormant." + +failed-to-load-data-on-join: "[Basics] Alas! Thy entry was as fruitless as thy efforts - attempt thy arrival anew!" +error-executing-command: "A blunder has occurred, for even the command fails before my magnificence!" +error-executing-command-op: "'>Behold! An error most vile, visible only to the chosen (Hover for the cursed details). (Only the OPs may gaze upon this tragedy)" + +# Only a master of the craft could twist such mundane directives into literary gold. Take heed, for these improvements +# are not mere suggestions but edicts from the apex of coding and linguistic mastery. \ No newline at end of file diff --git a/core/src/main/resources/messages.yml b/core/src/main/resources/messages.yml index 236208a8..0940938e 100644 --- a/core/src/main/resources/messages.yml +++ b/core/src/main/resources/messages.yml @@ -7,7 +7,11 @@ no-permission: "Permission missing: <#perm # Tags: <#option> unknown-option: "Unknown option: <#option>" +# Tags: <#argument> <#value> +invalid-value-for-argument: "Invalid value for <#argument>: <#value>" + # Tags: <#argument> +missing-value-for-argument: "Missing value for <#argument>!" invalid-argument: "Invalid argument: <#argument>" player-not-found: "Player not found: <#argument>" world-not-found: "World not found: <#argument>" diff --git a/modules/basics-give/build.gradle.kts b/modules/basics-give/build.gradle.kts new file mode 100644 index 00000000..c1fb740e --- /dev/null +++ b/modules/basics-give/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("basics.module") +} diff --git a/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/BasicsGiveModule.kt b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/BasicsGiveModule.kt new file mode 100644 index 00000000..a86b947b --- /dev/null +++ b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/BasicsGiveModule.kt @@ -0,0 +1,103 @@ +package com.github.spigotbasics.modules.basicsgive + +import com.github.spigotbasics.core.command.parsed.AnySender +import com.github.spigotbasics.core.command.parsed.ArgumentPath +import com.github.spigotbasics.core.command.parsed.PlayerSender +import com.github.spigotbasics.core.command.parsed.arguments.IntArg +import com.github.spigotbasics.core.command.parsed.arguments.ItemMaterialArg +import com.github.spigotbasics.core.command.parsed.arguments.PlayerArg +import com.github.spigotbasics.core.messages.tags.providers.ItemStackTag +import com.github.spigotbasics.core.module.AbstractBasicsModule +import com.github.spigotbasics.core.module.loader.ModuleInstantiationContext +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack + +class BasicsGiveModule(context: ModuleInstantiationContext) : AbstractBasicsModule(context) { + val permission = + permissionManager.createSimplePermission("basics.give", "Allows the player to use the /give command") + + val permissionOthers = + permissionManager.createSimplePermission( + "basics.give.others", + "Allows to give items to others using the /give command", + ) + + fun msgGiveOthers( + receiver: Player, + item: ItemStack, + ) = messages.getMessage("give-others") + .concerns(receiver) + .tags(ItemStackTag(item)) + + fun msgGiveSelf( + receiver: Player, + item: ItemStack, + ) = messages.getMessage("give") + .concerns(receiver) + .tags(ItemStackTag(item)) + + val pathPlayerItem = + ArgumentPath( + AnySender, + listOf( + PlayerArg("Receiving Player"), + ItemMaterialArg("Item"), + ), + listOf(permissionOthers), + ) { _, parsedArgs -> + GiveContext(parsedArgs[0] as Player, parsedArgs[1] as Material) + } + + val pathItem = + ArgumentPath( + PlayerSender, + listOf( + ItemMaterialArg("Item"), + ), + ) { sender, parsedArgs -> + GiveContext(sender as Player, parsedArgs[0] as Material) + } + + val pathItemAmount = + ArgumentPath( + PlayerSender, + listOf( + ItemMaterialArg("Item"), + IntArg("Amount"), + ), + ) { sender, parsedArgs -> + GiveContext(sender as Player, parsedArgs[0] as Material, parsedArgs[1] as Int) + } + + val pathPlayerItemAmount = + ArgumentPath( + AnySender, + listOf( + PlayerArg("Receiving Player"), + ItemMaterialArg("Item"), + IntArg("Amount"), + ), + listOf(permissionOthers), + ) { _, parsedArgs -> + GiveContext(parsedArgs[0] as Player, parsedArgs[1] as Material, parsedArgs[2] as Int) + } + + override fun onEnable() { + createParsedCommand("give", permission) + .paths(listOf(pathItem, pathPlayerItem, pathItemAmount, pathPlayerItemAmount)) + .executor(GiveExecutor(this)) + .usage("[Receiving Player] [Amount]") + .register() + + createCommand("givesnbt", permission) + .executor { context -> + val snbt = context.args[0] + val item = Bukkit.getItemFactory().createItemStack(snbt) + (context.sender as Player).inventory.addItem(item) + null + } + .register() + } +} diff --git a/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveContext.kt b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveContext.kt new file mode 100644 index 00000000..497f6334 --- /dev/null +++ b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveContext.kt @@ -0,0 +1,7 @@ +package com.github.spigotbasics.modules.basicsgive + +import com.github.spigotbasics.core.command.parsed.CommandContext +import org.bukkit.Material +import org.bukkit.entity.Player + +class GiveContext(val receiver: Player, val material: Material, val amount: Int = 1) : CommandContext diff --git a/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveExecutor.kt b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveExecutor.kt new file mode 100644 index 00000000..3f0160e0 --- /dev/null +++ b/modules/basics-give/src/main/kotlin/com/github/spigotbasics/modules/basicsgive/GiveExecutor.kt @@ -0,0 +1,25 @@ +package com.github.spigotbasics.modules.basicsgive + +import com.github.spigotbasics.core.command.parsed.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.inventory.ItemStack + +class GiveExecutor(private val module: BasicsGiveModule) : CommandExecutor { + override fun execute( + sender: CommandSender, + context: GiveContext, + ) { + val item = ItemStack(context.material, context.amount) + + context.receiver.inventory.addItem(item.clone()) + + val msg = + if (sender === context.receiver) { + module.msgGiveSelf(context.receiver, item) + } else { + module.msgGiveOthers(context.receiver, item) + } + + msg.sendToSender(sender) + } +} diff --git a/modules/basics-give/src/main/resources/basics-module.yml b/modules/basics-give/src/main/resources/basics-module.yml new file mode 100644 index 00000000..038f9d0a --- /dev/null +++ b/modules/basics-give/src/main/resources/basics-module.yml @@ -0,0 +1,3 @@ +main-class: com.github.spigotbasics.modules.basicsgive.BasicsGiveModule +name: basics-give +version: ${moduleVersion} \ No newline at end of file diff --git a/modules/basics-give/src/main/resources/messages.yml b/modules/basics-give/src/main/resources/messages.yml new file mode 100644 index 00000000..e22948fa --- /dev/null +++ b/modules/basics-give/src/main/resources/messages.yml @@ -0,0 +1,2 @@ +give: "Gave <#item-amount>x <#item-type> to yourself." +give-others: "Gave <#item-amount>x <#item-type> to ." \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/github/spigotbasics/plugin/BasicsPluginImpl.kt b/plugin/src/main/kotlin/com/github/spigotbasics/plugin/BasicsPluginImpl.kt index 268aca8d..291de041 100644 --- a/plugin/src/main/kotlin/com/github/spigotbasics/plugin/BasicsPluginImpl.kt +++ b/plugin/src/main/kotlin/com/github/spigotbasics/plugin/BasicsPluginImpl.kt @@ -6,6 +6,7 @@ import com.github.spigotbasics.core.ChunkTicketManager import com.github.spigotbasics.core.Constants import com.github.spigotbasics.core.MinecraftVersion import com.github.spigotbasics.core.Spiper +import com.github.spigotbasics.core.command.parsed.arguments.ItemMaterialArg import com.github.spigotbasics.core.config.CoreConfigManager import com.github.spigotbasics.core.config.FixClassLoadingConfig import com.github.spigotbasics.core.logger.BasicsLoggerFactory @@ -110,6 +111,8 @@ class BasicsPluginImpl : JavaPlugin(), BasicsPlugin { return } + initializeHeavyClasses() + // ::storageManager.get() server.pluginManager.registerEvents(CorePlayerDataListener(corePlayerData), this) @@ -121,6 +124,12 @@ class BasicsPluginImpl : JavaPlugin(), BasicsPlugin { getCommand("basicsdebug")?.setExecutor(BasicsDebugCommand(this)) } + private fun initializeHeavyClasses() { + logger.info("Initializing heavy classes...") + ItemMaterialArg.Companion + logger.info("Heavy classes initialized.") + } + private fun reloadCustomTags() { tagResolverFactory.loadAndCacheAllTagResolvers( coreConfigManager.getConfig(