diff --git a/ButtonProcessor/pom.xml b/ButtonProcessor/pom.xml index 7c0d6f0..dfbfe2b 100644 --- a/ButtonProcessor/pom.xml +++ b/ButtonProcessor/pom.xml @@ -35,11 +35,11 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.8.1 -proc:none - 8 - 8 + 17 + 17 diff --git a/Chroma-Core/pom.xml b/Chroma-Core/pom.xml index 22d1edc..e457d89 100755 --- a/Chroma-Core/pom.xml +++ b/Chroma-Core/pom.xml @@ -27,31 +27,62 @@ Chroma-Core + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.2.1 package shade - - - + + + me.lucko:commodore org.javatuples:javatuples - - - - me.lucko.commodore - - buttondevteam.core.commodore - - - + + + + me.lucko.commodore + + buttondevteam.core.commodore + + + @@ -200,6 +231,11 @@ javatuples 1.2 + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + TBMCPlugins @@ -217,6 +253,7 @@ github UTF-8 1.0.1 + 1.8.10 https://github.com/TBMCPlugins/mvn-repo diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/architecture/ButtonPlugin.kt b/Chroma-Core/src/main/java/buttondevteam/lib/architecture/ButtonPlugin.kt index 4f0a96a..7124b04 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/architecture/ButtonPlugin.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/architecture/ButtonPlugin.kt @@ -1,171 +1,158 @@ -package buttondevteam.lib.architecture; +package buttondevteam.lib.architecture -import buttondevteam.buttonproc.HasConfig; -import buttondevteam.core.ComponentManager; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.Command2MC; -import buttondevteam.lib.chat.ICommand2MC; -import lombok.AccessLevel; -import lombok.Getter; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.Arrays; -import java.util.Optional; -import java.util.Stack; +import buttondevteam.buttonproc.HasConfig +import buttondevteam.core.ComponentManager +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.Component.Companion.updateConfig +import buttondevteam.lib.chat.Command2MC +import buttondevteam.lib.chat.Command2MC.registerCommand +import buttondevteam.lib.chat.Command2MC.unregisterCommands +import buttondevteam.lib.chat.ICommand2MC +import lombok.AccessLevel +import lombok.Getter +import org.bukkit.configuration.InvalidConfigurationException +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.plugin.java.JavaPlugin +import java.io.File +import java.io.IOException +import java.util.* +import java.util.function.Consumer +import java.util.function.Function @HasConfig(global = true) -public abstract class ButtonPlugin extends JavaPlugin { - @Getter //Needs to be static as we don't know the plugin when a command is handled - private static final Command2MC command2MC = new Command2MC(); - @Getter(AccessLevel.PROTECTED) - private final IHaveConfig iConfig = new IHaveConfig(this::saveConfig); - private CommentedConfiguration yaml; - @Getter(AccessLevel.PROTECTED) - private IHaveConfig data; //TODO - /** - * Used to unregister components in the right order - and to reload configs - */ - @Getter - private final Stack> componentStack = new Stack<>(); +abstract class ButtonPlugin : JavaPlugin() { + @Getter(AccessLevel.PROTECTED) + private val iConfig = IHaveConfig { saveConfig() } + private var yaml: CommentedConfiguration? = null - protected abstract void pluginEnable(); + @Getter(AccessLevel.PROTECTED) + private val data //TODO + : IHaveConfig? = null - /** - * Called after the components are unregistered - */ - protected abstract void pluginDisable(); + /** + * Used to unregister components in the right order - and to reload configs + */ + @Getter + private val componentStack = Stack>() + protected abstract fun pluginEnable() - /** - * Called before the components are unregistered - */ - protected void pluginPreDisable() { - } + /** + * Called after the components are unregistered + */ + protected abstract fun pluginDisable() - @Override - public final void onEnable() { - if (!loadConfig()) { - getLogger().warning("Please fix the issues and restart the server to load the plugin."); - return; - } - try { - pluginEnable(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while enabling plugin " + getName() + "!", e, this); - } - if (configGenAllowed(this)) //If it's not disabled (by default it's not) - IHaveConfig.pregenConfig(this, null); - } + /** + * Called before the components are unregistered + */ + protected fun pluginPreDisable() {} + override fun onEnable() { + if (!loadConfig()) { + logger.warning("Please fix the issues and restart the server to load the plugin.") + return + } + try { + pluginEnable() + } catch (e: Exception) { + TBMCCoreAPI.SendException("Error while enabling plugin $name!", e, this) + } + if (configGenAllowed(this)) //If it's not disabled (by default it's not) + IHaveConfig.pregenConfig(this, null) + } - private boolean loadConfig() { - var config = getConfig(); - if (config == null) - return false; - var section = config.getConfigurationSection("global"); - if (section == null) section = config.createSection("global"); - iConfig.reset(section); - return true; - } + private fun loadConfig(): Boolean { + val config = config ?: return false + var section = config.getConfigurationSection("global") + if (section == null) section = config.createSection("global") + iConfig.reset(section) + return true + } - @Override - public final void onDisable() { - try { - pluginPreDisable(); - ComponentManager.unregComponents(this); - pluginDisable(); - if (ConfigData.saveNow(getConfig())) - getLogger().info("Saved configuration changes."); - getCommand2MC().unregisterCommands(this); - } catch (Exception e) { - TBMCCoreAPI.SendException("Error while disabling plugin " + getName() + "!", e, this); - } - } + override fun onDisable() { + try { + pluginPreDisable() + ComponentManager.unregComponents(this) + pluginDisable() + if (ConfigData.saveNow(config)) logger.info("Saved configuration changes.") + ButtonPlugin.getCommand2MC().unregisterCommands(this) + } catch (e: Exception) { + TBMCCoreAPI.SendException("Error while disabling plugin $name!", e, this) + } + } - @Override - public void reloadConfig() { - tryReloadConfig(); - } + override fun reloadConfig() { + tryReloadConfig() + } - public boolean tryReloadConfig() { - if (!justReload()) return false; - loadConfig(); - componentStack.forEach(c -> Component.updateConfig(this, c)); - return true; - } + fun tryReloadConfig(): Boolean { + if (!justReload()) return false + loadConfig() + componentStack.forEach(Consumer { c: Component<*>? -> updateConfig(this, c!!) }) + return true + } - public boolean justReload() { - if (yaml != null && ConfigData.saveNow(getConfig())) { - getLogger().warning("Saved pending configuration changes to the file, didn't reload. Apply your changes again."); - return false; - } - var file = new File(getDataFolder(), "config.yml"); - var yaml = new CommentedConfiguration(file); - if (file.exists()) { - try { - yaml.load(file); - } catch (IOException | InvalidConfigurationException e) { - getLogger().warning("Failed to load config! Check for syntax errors."); - e.printStackTrace(); - return false; - } - } - this.yaml = yaml; - var res = getTextResource("configHelp.yml"); - if (res == null) - return true; - var yc = YamlConfiguration.loadConfiguration(res); - for (var kv : yc.getValues(true).entrySet()) - if (kv.getValue() instanceof String) - yaml.addComment(kv.getKey().replace(".generalDescriptionInsteadOfAConfig", ""), - Arrays.stream(((String) kv.getValue()).split("\n")) - .map(str -> "# " + str.trim()).toArray(String[]::new)); - return true; - } + fun justReload(): Boolean { + if (yaml != null && ConfigData.saveNow(config)) { + logger.warning("Saved pending configuration changes to the file, didn't reload. Apply your changes again.") + return false + } + val file = File(dataFolder, "config.yml") + val yaml = CommentedConfiguration(file) + if (file.exists()) { + try { + yaml.load(file) + } catch (e: IOException) { + logger.warning("Failed to load config! Check for syntax errors.") + e.printStackTrace() + return false + } catch (e: InvalidConfigurationException) { + logger.warning("Failed to load config! Check for syntax errors.") + e.printStackTrace() + return false + } + } + this.yaml = yaml + val res = getTextResource("configHelp.yml") ?: return true + val yc = YamlConfiguration.loadConfiguration(res) + for ((key, value) in yc.getValues(true)) if (value is String) yaml.addComment(key.replace(".generalDescriptionInsteadOfAConfig", ""), + *Arrays.stream(value.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .map { str: String -> "# " + str.trim { it <= ' ' } }.toArray { _Dummy_.__Array__() }) + return true + } - @Override - public FileConfiguration getConfig() { - if (yaml == null) - justReload(); - if (yaml == null) return new YamlConfiguration(); //Return a temporary instance - return yaml; - } + override fun getConfig(): FileConfiguration { + if (yaml == null) justReload() + return if (yaml == null) YamlConfiguration() else yaml //Return a temporary instance + } - @Override - public void saveConfig() { - try { - if (yaml != null) - yaml.save(); - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to save config", e, this); - } - } + override fun saveConfig() { + try { + if (yaml != null) yaml!!.save() + } catch (e: Exception) { + TBMCCoreAPI.SendException("Failed to save config", e, this) + } + } - /** - * Registers command and sets its plugin. - * - * @param command The command to register - */ - protected void registerCommand(ICommand2MC command) { - command.registerToPlugin(this); - getCommand2MC().registerCommand(command); - } + /** + * Registers command and sets its plugin. + * + * @param command The command to register + */ + fun registerCommand(command: ICommand2MC) { + command.registerToPlugin(this) + ButtonPlugin.getCommand2MC().registerCommand(command) + } - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - public @interface ConfigOpts { - boolean disableConfigGen() default false; - } + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) + annotation class ConfigOpts(val disableConfigGen: Boolean = false) + companion object { + @Getter //Needs to be static as we don't know the plugin when a command is handled - public static boolean configGenAllowed(Object obj) { - return !Optional.ofNullable(obj.getClass().getAnnotation(ConfigOpts.class)) - .map(ConfigOpts::disableConfigGen).orElse(false); - } -} + private val command2MC = Command2MC() + fun configGenAllowed(obj: Any): Boolean { + return !Optional.ofNullable(obj.javaClass.getAnnotation(ConfigOpts::class.java)) + .map(Function { obj: ConfigOpts -> obj.disableConfigGen() }).orElse(false) + } + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/architecture/Component.kt b/Chroma-Core/src/main/java/buttondevteam/lib/architecture/Component.kt index 29cb5cf..93c24c0 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/architecture/Component.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/architecture/Component.kt @@ -1,274 +1,281 @@ -package buttondevteam.lib.architecture; +package buttondevteam.lib.architecture -import buttondevteam.buttonproc.HasConfig; -import buttondevteam.core.ComponentManager; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.exceptions.UnregisteredComponentException; -import buttondevteam.lib.chat.ICommand2MC; -import lombok.Getter; -import lombok.NonNull; -import lombok.val; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.event.Listener; -import org.bukkit.plugin.java.JavaPlugin; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.stream.Collectors; +import buttondevteam.buttonproc.HasConfig +import buttondevteam.core.ComponentManager +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.exceptions.UnregisteredComponentException +import buttondevteam.lib.chat.ICommand2MC +import lombok.Getter +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.event.Listener +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import java.util.function.Consumer +import java.util.function.Function +import java.util.stream.Collectors /** * Configuration is based on class name */ @HasConfig(global = false) //Used for obtaining javadoc -public abstract class Component { - @SuppressWarnings("rawtypes") private static HashMap, Component> components = new HashMap<>(); - @Getter - private boolean enabled = false; - @Getter - @NonNull - private TP plugin; - private @Getter final IHaveConfig config = new IHaveConfig(null); - private @Getter IHaveConfig data; //TODO +abstract class Component { + @Getter + private var enabled = false - public final ConfigData shouldBeEnabled = config.getData("enabled", - Optional.ofNullable(getClass().getAnnotation(ComponentMetadata.class)).map(ComponentMetadata::enabledByDefault).orElse(true)); + @Getter + private var plugin: TP = null - /** - * Registers a component checking it's dependencies and calling {@link #register(JavaPlugin)}.
- * Make sure to register the dependencies first.
- * The component will be enabled automatically, regardless of when it was registered.
- * If not using {@link ButtonPlugin}, call {@link ComponentManager#unregComponents(ButtonPlugin)} on plugin disable. - * - * @param component The component to register - * @return Whether the component is registered successfully (it may have failed to enable) - */ - public static boolean registerComponent(T plugin, Component component) { - return registerUnregisterComponent(plugin, component, true); - } + @Getter + private val config = IHaveConfig(null) - /** - * Unregisters a component by calling {@link #unregister(JavaPlugin)}.
- * Make sure to unregister the dependencies last.
- * Components will be unregistered in opposite order of registering by default by {@link ButtonPlugin} or {@link ComponentManager#unregComponents(ButtonPlugin)}. - * - * @param component The component to unregister - * @return Whether the component is unregistered successfully (it also got disabled) - */ - public static boolean unregisterComponent(T plugin, Component component) { - return registerUnregisterComponent(plugin, component, false); - } + @Getter + private val data //TODO + : IHaveConfig? = null - public static boolean registerUnregisterComponent(T plugin, Component component, boolean register) { - try { - val metaAnn = component.getClass().getAnnotation(ComponentMetadata.class); - if (metaAnn != null) { - @SuppressWarnings("rawtypes") Class[] dependencies = metaAnn.depends(); - for (val dep : dependencies) { //TODO: Support dependencies at enable/disable as well - if (!components.containsKey(dep)) { - plugin.getLogger().warning("Failed to " + (register ? "" : "un") + "register component " + component.getClassName() + " as a required dependency is missing/disabled: " + dep.getSimpleName()); - return false; - } - } - } - if (register) { - if (components.containsKey(component.getClass())) { - TBMCCoreAPI.SendException("Failed to register component " + component.getClassName(), new IllegalArgumentException("The component is already registered!"), plugin); - return false; - } - component.plugin = plugin; - component.config.setSaveAction(plugin::saveConfig); - updateConfig(plugin, component); - component.register(plugin); - components.put(component.getClass(), component); - if (plugin instanceof ButtonPlugin) - ((ButtonPlugin) plugin).getComponentStack().push(component); - if (ComponentManager.areComponentsEnabled() && component.shouldBeEnabled.get()) { - try { //Enable components registered after the previous ones getting enabled - setComponentEnabled(component, true); - return true; - } catch (Exception | NoClassDefFoundError e) { - TBMCCoreAPI.SendException("Failed to enable component " + component.getClassName() + "!", e, component); - return true; - } - } - } else { - if (!components.containsKey(component.getClass())) - return true; //Already unregistered - if (component.enabled) { - try { - setComponentEnabled(component, false); - } catch (Exception | NoClassDefFoundError e) { - TBMCCoreAPI.SendException("Failed to disable component " + component.getClassName() + "!", e, component); - return false; //If failed to disable, won't unregister either - } - } - component.unregister(plugin); - components.remove(component.getClass()); - } - return true; - } catch (Exception e) { - TBMCCoreAPI.SendException("Failed to " + (register ? "" : "un") + "register component " + component.getClassName() + "!", e, plugin); - return false; - } - } + @JvmField + val shouldBeEnabled = config.getData("enabled", + Optional.ofNullable(javaClass.getAnnotation(ComponentMetadata::class.java)).map(Function { obj: ComponentMetadata -> obj.enabledByDefault() }).orElse(true)) - /** - * Enables or disables the given component. If the component fails to enable, it will be disabled. - * - * @param component The component to register - * @param enabled Whether it's enabled or not - */ - public static void setComponentEnabled(Component component, boolean enabled) throws UnregisteredComponentException { - if (!components.containsKey(component.getClass())) - throw new UnregisteredComponentException(component); - if (component.enabled == enabled) return; //Don't do anything - if (component.enabled = enabled) { - try { - updateConfig(component.getPlugin(), component); - component.enable(); - if (ButtonPlugin.configGenAllowed(component)) { - IHaveConfig.pregenConfig(component, null); - } - } catch (Exception e) { - try { //Automatically disable components that fail to enable properly - setComponentEnabled(component, false); - throw e; - } catch (Exception ex) { - Throwable t = ex; - for (var th = t; th != null; th = th.getCause()) - t = th; //Set if not null - if (t != e) - t.initCause(e); - throw ex; - } - } - } else { - component.disable(); - ButtonPlugin.getCommand2MC().unregisterCommands(component); - } - } + fun log(message: String) { + plugin!!.logger.info("[" + className + "] " + message) + } - public static void updateConfig(JavaPlugin plugin, Component component) { - if (plugin.getConfig() != null) { //Production - var compconf = plugin.getConfig().getConfigurationSection("components"); - if (compconf == null) compconf = plugin.getConfig().createSection("components"); - var configSect = compconf.getConfigurationSection(component.getClassName()); - if (configSect == null) - configSect = compconf.createSection(component.getClassName()); - component.config.reset(configSect); - } //Testing: it's already set - } + fun logWarn(message: String) { + plugin!!.logger.warning("[" + className + "] " + message) + } - /** - * Returns the currently registered components
- * - * @return The currently registered components - */ - @SuppressWarnings("rawtypes") - public static Map, Component> getComponents() { - return Collections.unmodifiableMap(components); - } + /** + * Registers the module, when called by the JavaPlugin class. + * This gets fired when the plugin is enabled. Use [.enable] to register commands and such. + * + * @param plugin Plugin object + */ + protected open fun register(plugin: JavaPlugin?) {} - public void log(String message) { - plugin.getLogger().info("[" + getClassName() + "] " + message); - } + /** + * Unregisters the module, when called by the JavaPlugin class. + * This gets fired when the plugin is disabled. + * Do any cleanups needed within this method. + * + * @param plugin Plugin object + */ + protected open fun unregister(plugin: JavaPlugin?) {} - public void logWarn(String message) { - plugin.getLogger().warning("[" + getClassName() + "] " + message); - } + /** + * Enables the module, when called by the JavaPlugin class. Call + * registerCommand() and registerListener() within this method.

+ * To access the plugin, use [.getPlugin]. + */ + protected abstract fun enable() - /** - * Registers the module, when called by the JavaPlugin class. - * This gets fired when the plugin is enabled. Use {@link #enable()} to register commands and such. - * - * @param plugin Plugin object - */ - @SuppressWarnings({"unused"}) - protected void register(JavaPlugin plugin) { - } + /** + * Disables the module, when called by the JavaPlugin class. Do + * any cleanups needed within this method. + * To access the plugin, use [.getPlugin]. + */ + protected abstract fun disable() - /** - * Unregisters the module, when called by the JavaPlugin class. - * This gets fired when the plugin is disabled. - * Do any cleanups needed within this method. - * - * @param plugin Plugin object - */ - @SuppressWarnings({"unused"}) - protected void unregister(JavaPlugin plugin) { - } + /** + * Registers a command to the component. Make sure to use [buttondevteam.lib.chat.CommandClass] and [buttondevteam.lib.chat.Command2.Subcommand]. + * You don't need to register the command in plugin.yml. + * + * @param command Custom coded command class + */ + fun registerCommand(command: ICommand2MC) { + if (plugin is ButtonPlugin) command.registerToPlugin(plugin as ButtonPlugin) + command.registerToComponent(this) + ButtonPlugin.getCommand2MC().registerCommand(command) + } - /** - * Enables the module, when called by the JavaPlugin class. Call - * registerCommand() and registerListener() within this method.
- * To access the plugin, use {@link #getPlugin()}. - */ - protected abstract void enable(); + /** + * Registers a Listener to this component + * + * @param listener The event listener to register + * @return The provided listener + */ + protected fun registerListener(listener: Listener): Listener { + TBMCCoreAPI.RegisterEventsForExceptions(listener, plugin) + return listener + } - /** - * Disables the module, when called by the JavaPlugin class. Do - * any cleanups needed within this method. - * To access the plugin, use {@link #getPlugin()}. - */ - protected abstract void disable(); + /** + * Returns a map of configs that are under the given key. + * + * @param key The key to use + * @param defaultProvider A mapping between config paths and config generators + * @return A map containing configs + */ + fun getConfigMap(key: String?, defaultProvider: Map>): Map { + val c: ConfigurationSection = getConfig().getConfig() + var cs = c.getConfigurationSection(key) + if (cs == null) cs = c.createSection(key) + val res = cs!!.getValues(false).entries.stream().filter { (_, value): Map.Entry -> value is ConfigurationSection } + .collect(Collectors.toMap, String, IHaveConfig>(Function, String> { (key1, value) -> java.util.Map.Entry.key }, Function, IHaveConfig> { (_, value): Map.Entry -> + val conf = IHaveConfig { getPlugin().saveConfig() } + conf.reset(value as ConfigurationSection?) + conf + })) + if (res.size == 0) { + for ((key1, value) in defaultProvider) { + val conf = IHaveConfig { getPlugin().saveConfig() } + conf.reset(cs.createSection(key1)) + value.accept(conf) + res[key1] = conf + } + } + return res + } - /** - * Registers a command to the component. Make sure to use {@link buttondevteam.lib.chat.CommandClass} and {@link buttondevteam.lib.chat.Command2.Subcommand}. - * You don't need to register the command in plugin.yml. - * - * @param command Custom coded command class - */ - protected final void registerCommand(ICommand2MC command) { - if (plugin instanceof ButtonPlugin) - command.registerToPlugin((ButtonPlugin) plugin); - command.registerToComponent(this); - ButtonPlugin.getCommand2MC().registerCommand(command); - } + private val className: String + private get() = javaClass.simpleName - /** - * Registers a Listener to this component - * - * @param listener The event listener to register - * @return The provided listener - */ - protected final Listener registerListener(Listener listener) { - TBMCCoreAPI.RegisterEventsForExceptions(listener, plugin); - return listener; - } + companion object { + private val components = HashMap>, Component>() - /** - * Returns a map of configs that are under the given key. - * - * @param key The key to use - * @param defaultProvider A mapping between config paths and config generators - * @return A map containing configs - */ - protected Map getConfigMap(String key, Map> defaultProvider) { - val c = getConfig().getConfig(); - var cs = c.getConfigurationSection(key); - if (cs == null) cs = c.createSection(key); - val res = cs.getValues(false).entrySet().stream().filter(e -> e.getValue() instanceof ConfigurationSection) - .collect(Collectors.toMap(Map.Entry::getKey, kv -> { - var conf = new IHaveConfig(getPlugin()::saveConfig); - conf.reset((ConfigurationSection) kv.getValue()); - return conf; - })); - if (res.size() == 0) { - for (val entry : defaultProvider.entrySet()) { - val conf = new IHaveConfig(getPlugin()::saveConfig); - conf.reset(cs.createSection(entry.getKey())); - entry.getValue().accept(conf); - res.put(entry.getKey(), conf); - } - } - return res; - } + /** + * Registers a component checking it's dependencies and calling [.register].

+ * Make sure to register the dependencies first.

+ * The component will be enabled automatically, regardless of when it was registered.

+ * **If not using [ButtonPlugin], call [ComponentManager.unregComponents] on plugin disable.** + * + * @param component The component to register + * @return Whether the component is registered successfully (it may have failed to enable) + */ + @JvmStatic + fun registerComponent(plugin: T, component: Component): Boolean { + return registerUnregisterComponent(plugin, component, true) + } - private String getClassName() { - return getClass().getSimpleName(); - } -} + /** + * Unregisters a component by calling [.unregister].

+ * Make sure to unregister the dependencies last.

+ * **Components will be unregistered in opposite order of registering by default by [ButtonPlugin] or [ComponentManager.unregComponents].** + * + * @param component The component to unregister + * @return Whether the component is unregistered successfully (it also got disabled) + */ + @JvmStatic + fun unregisterComponent(plugin: T, component: Component): Boolean { + return registerUnregisterComponent(plugin, component, false) + } + + fun registerUnregisterComponent(plugin: T, component: Component, register: Boolean): Boolean { + return try { + val metaAnn = component.javaClass.getAnnotation(ComponentMetadata::class.java) + if (metaAnn != null) { + val dependencies: Array>> = metaAnn.depends() + for (dep in dependencies) { //TODO: Support dependencies at enable/disable as well + if (!components.containsKey(dep)) { + plugin!!.logger.warning("Failed to " + (if (register) "" else "un") + "register component " + component.className + " as a required dependency is missing/disabled: " + dep.simpleName) + return false + } + } + } + if (register) { + if (components.containsKey(component.javaClass)) { + TBMCCoreAPI.SendException("Failed to register component " + component.className, IllegalArgumentException("The component is already registered!"), plugin) + return false + } + component.plugin = plugin + component.config.saveAction = Runnable { plugin!!.saveConfig() } + updateConfig(plugin, component) + component.register(plugin) + components[component.javaClass] = component + if (plugin is ButtonPlugin) (plugin as ButtonPlugin).componentStack.push(component) + if (ComponentManager.areComponentsEnabled() && component.shouldBeEnabled.get()) { + return try { //Enable components registered after the previous ones getting enabled + setComponentEnabled(component, true) + true + } catch (e: Exception) { + TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component) + true + } catch (e: NoClassDefFoundError) { + TBMCCoreAPI.SendException("Failed to enable component " + component.className + "!", e, component) + true + } + } + } else { + if (!components.containsKey(component.javaClass)) return true //Already unregistered + if (component.enabled) { + try { + setComponentEnabled(component, false) + } catch (e: Exception) { + TBMCCoreAPI.SendException("Failed to disable component " + component.className + "!", e, component) + return false //If failed to disable, won't unregister either + } catch (e: NoClassDefFoundError) { + TBMCCoreAPI.SendException("Failed to disable component " + component.className + "!", e, component) + return false + } + } + component.unregister(plugin) + components.remove(component.javaClass) + } + true + } catch (e: Exception) { + TBMCCoreAPI.SendException("Failed to " + (if (register) "" else "un") + "register component " + component.className + "!", e, plugin) + false + } + } + + /** + * Enables or disables the given component. If the component fails to enable, it will be disabled. + * + * @param component The component to register + * @param enabled Whether it's enabled or not + */ + @JvmStatic + @Throws(UnregisteredComponentException::class) + fun setComponentEnabled(component: Component<*>, enabled: Boolean) { + if (!components.containsKey(component.javaClass)) throw UnregisteredComponentException(component) + if (component.enabled == enabled) return //Don't do anything + if (enabled.also { component.enabled = it }) { + try { + updateConfig(component.getPlugin(), component) + component.enable() + if (ButtonPlugin.configGenAllowed(component)) { + IHaveConfig.pregenConfig(component, null) + } + } catch (e: Exception) { + try { //Automatically disable components that fail to enable properly + setComponentEnabled(component, false) + throw e + } catch (ex: Exception) { + var t: Throwable = ex + var th: Throwable? = t + while (th != null) { + t = th //Set if not null + th = th.cause + } + if (t !== e) t.initCause(e) + throw ex + } + } + } else { + component.disable() + ButtonPlugin.getCommand2MC().unregisterCommands(component) + } + } + + @JvmStatic + fun updateConfig(plugin: JavaPlugin, component: Component<*>) { + if (plugin.config != null) { //Production + var compconf = plugin.config.getConfigurationSection("components") + if (compconf == null) compconf = plugin.config.createSection("components") + var configSect = compconf!!.getConfigurationSection(component.className) + if (configSect == null) configSect = compconf.createSection(component.className) + component.config.reset(configSect) + } //Testing: it's already set + } + + /** + * Returns the currently registered components

+ * + * @return The currently registered components + */ + @JvmStatic + fun getComponents(): Map>, Component> { + return Collections.unmodifiableMap(components) + } + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt index a9e6df7..bccd5ae 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.kt @@ -1,325 +1,278 @@ -package buttondevteam.lib.chat; +package buttondevteam.lib.chat -import buttondevteam.core.MainPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.commands.CommandArgument; -import buttondevteam.lib.chat.commands.CommandArgumentHelpManager; -import buttondevteam.lib.chat.commands.NumberArg; -import buttondevteam.lib.chat.commands.SubcommandData; -import buttondevteam.lib.player.ChromaGamerBase; -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.*; -import com.mojang.brigadier.builder.ArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.tree.CommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; -import lombok.RequiredArgsConstructor; -import lombok.val; -import org.bukkit.Bukkit; -import org.javatuples.Pair; -import org.javatuples.Triplet; -import org.jetbrains.annotations.NotNull; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; +import buttondevteam.core.MainPlugin +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.chat.Command2.Subcommand +import buttondevteam.lib.chat.commands.* +import buttondevteam.lib.player.ChromaGamerBase +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.* +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.context.ParsedCommandNode +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.CommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import lombok.RequiredArgsConstructor +import org.bukkit.Bukkit +import org.javatuples.Pair +import org.javatuples.Triplet +import java.lang.reflect.Method +import java.util.function.Function +import java.util.function.Predicate +import java.util.function.Supplier +import java.util.stream.Collectors /** * The method name is the subcommand, use underlines (_) to add further subcommands. * The args may be null if the conversion failed and it's optional. */ @RequiredArgsConstructor -public abstract class Command2, TP extends Command2Sender> { +abstract class Command2, TP : Command2Sender> { + /** + * Parameters annotated with this receive all the remaining arguments + */ + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + annotation class TextArg - /** - * Parameters annotated with this receive all the remaining arguments - */ - @Target(ElementType.PARAMETER) - @Retention(RetentionPolicy.RUNTIME) - public @interface TextArg { - } + /** + * Methods annotated with this will be recognised as subcommands + */ + @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) + @Retention(AnnotationRetention.RUNTIME) + annotation class Subcommand( + /** + * Help text to show players. A usage message will be also shown below it. + */ + val helpText: Array = [], + /** + * The main permission which allows using this command (individual access can be still revoked with "chroma.command.X"). + * Used to be "tbmc.admin". The [.MOD_GROUP] is provided to use with this. + */ + val permGroup: String = "", val aliases: Array = []) { + companion object { + /** + * Allowed for OPs only by default + */ + const val MOD_GROUP = "mod" + } + } - /** - * Methods annotated with this will be recognised as subcommands - */ - @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) - public @interface Subcommand { - /** - * Allowed for OPs only by default - */ - String MOD_GROUP = "mod"; + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + annotation class OptionalArg - /** - * Help text to show players. A usage message will be also shown below it. - */ - String[] helpText() default {}; + protected class ParamConverter(val converter: Function, val errormsg: String, val allSupplier: Supplier>) - /** - * The main permission which allows using this command (individual access can be still revoked with "chroma.command.X"). - * Used to be "tbmc.admin". The {@link #MOD_GROUP} is provided to use with this. - */ - String permGroup() default ""; + protected val paramConverters = HashMap, ParamConverter<*>>() + private val commandHelp = ArrayList() //Mainly needed by Discord + private val dispatcher = CommandDispatcher() - String[] aliases() default {}; - } + /** + * The first character in the command line that shows that it's a command. + */ + private val commandChar = 0.toChar() - @Target(ElementType.PARAMETER) - @Retention(RetentionPolicy.RUNTIME) - public @interface OptionalArg { - } + /** + * Whether the command's actual code has to be run on the primary thread. + */ + private val runOnPrimaryThread = false - @RequiredArgsConstructor - protected static class ParamConverter { - public final Function converter; - public final String errormsg; - public final Supplier> allSupplier; - } + /** + * Adds a param converter that obtains a specific object from a string parameter. + * The converter may return null. + * + * @param The type of the result + * @param cl The class of the result object + * @param converter The converter to use + * @param allSupplier The supplier of all possible values (ideally) + */ + open fun addParamConverter(cl: Class, converter: Function, errormsg: String, + allSupplier: Supplier>) { + paramConverters[cl] = ParamConverter(converter, errormsg, allSupplier) + } - protected final HashMap, ParamConverter> paramConverters = new HashMap<>(); - private final ArrayList commandHelp = new ArrayList<>(); //Mainly needed by Discord - private final CommandDispatcher dispatcher = new CommandDispatcher<>(); + open fun handleCommand(sender: TP, commandline: String): Boolean { + val results = dispatcher.parse(commandline, sender) + if (results.reader.canRead()) { + return false // Unknown command + } + //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread + Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance) { + try { + dispatcher.execute(results) + } catch (e: CommandSyntaxException) { + sender.sendMessage(e.message) + } catch (e: Exception) { + TBMCCoreAPI.SendException("Command execution failed for sender " + sender.name + "(" + sender.javaClass.canonicalName + ") and message " + commandline, e, MainPlugin.Instance) + } + } + return true //We found a method + } - /** - * The first character in the command line that shows that it's a command. - */ - private final char commandChar; - /** - * Whether the command's actual code has to be run on the primary thread. - */ - private final boolean runOnPrimaryThread; + //TODO: Add to the help + private fun processSenderType(sender: TP, sd: SubcommandData, params: ArrayList): Boolean { + val sendertype = sd.senderType + val cg: ChromaGamerBase + if (sendertype.isAssignableFrom(sender.javaClass)) params.add(sender) //The command either expects a CommandSender or it is a Player, or some other expected type + else if (sender is Command2MCSender // TODO: This is Minecraft only + && sendertype.isAssignableFrom((sender as Command2MCSender).sender.javaClass)) + params.add((sender as Command2MCSender).sender) + else if ((ChromaGamerBase::class.java.isAssignableFrom(sendertype) && sender is Command2MCSender) + && ChromaGamerBase.getFromSender((sender as Command2MCSender).sender).also { cg = it } != null && cg.javaClass == sendertype) //The command expects a user of our system + params.add(cg) else { + val type = sendertype.simpleName.fold("") { s, ch -> s + if (ch.isUpperCase()) " " + ch.lowercase() else ch } + sender.sendMessage("§cYou need to be a $type to use this command.") + sender.sendMessage(sd.getHelpText(sender)) //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only + return true + } + return false + } - /** - * Adds a param converter that obtains a specific object from a string parameter. - * The converter may return null. - * - * @param The type of the result - * @param cl The class of the result object - * @param converter The converter to use - * @param allSupplier The supplier of all possible values (ideally) - */ - public void addParamConverter(Class cl, Function converter, String errormsg, - Supplier> allSupplier) { - paramConverters.put(cl, new ParamConverter<>(converter, errormsg, allSupplier)); - } + /** + * Register a command in the command system. The way this command gets registered may change depending on the implementation. + * Always invoke [.registerCommandSuper] when implementing this method. + * + * @param command The command to register + */ + abstract fun registerCommand(command: TC) - public boolean handleCommand(TP sender, String commandline) { - var results = dispatcher.parse(commandline, sender); - if (results.getReader().canRead()) { - return false; // Unknown command - } - Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> { - try { - dispatcher.execute(results); - } catch (CommandSyntaxException e) { - sender.sendMessage(e.getMessage()); - } catch (Exception e) { - TBMCCoreAPI.SendException("Command execution failed for sender " + sender.getName() + "(" + sender.getClass().getCanonicalName() + ") and message " + commandline, e, MainPlugin.Instance); - } - }); - return true; //We found a method - } + /** + * Registers a command in the Command2 system, so it can be looked up and executed. + * + * @param command The command to register + * @return The Brigadier command node if you need it for something (like tab completion) + */ + protected fun registerCommandSuper(command: TC): LiteralCommandNode { + var mainCommandNode: LiteralCommandNode? = null + for (meth in command.javaClass.getMethods()) { + val ann = meth.getAnnotation(Subcommand::class.java) ?: continue + val methodPath = CommandUtils.getCommandPath(meth.name, ' ') + val result = registerNodeFromPath(command!!.commandPath + methodPath) + result.value0.addChild(getExecutableNode(meth, command, ann, result.value2, CommandArgumentHelpManager(command))) + if (mainCommandNode == null) mainCommandNode = result.value1 else if (result.value1!!.name != mainCommandNode.name) { + MainPlugin.Instance.logger.warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.javaClass.simpleName) + } + } + if (mainCommandNode == null) { + throw RuntimeException("There are no subcommands defined in the command class " + command.javaClass.getSimpleName() + "!") + } + return mainCommandNode + } - //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread + /** + * Returns the node that can actually execute the given subcommand. + * + * @param method The subcommand method + * @param command The command object + * @param path The command path + * @return The executable node + */ + private fun getExecutableNode(method: Method, command: TC, ann: Subcommand, path: String, argHelpManager: CommandArgumentHelpManager): LiteralCommandNode { + val paramsAndSenderType = getCommandParametersAndSender(method, argHelpManager) // Param order is important + val params = paramsAndSenderType.value0 + val paramMap = HashMap() + for (param in params) { + paramMap[param!!.name] = param + } + val node = CoreCommandBuilder.literal(path, params[0]!!.type, paramMap, params, command) + .helps(command!!.getHelpText(method, ann)).permits { sender: TP -> hasPermission(sender, command, method) } + .executes { context: CommandContext -> executeCommand(context) } + var parent: ArgumentBuilder = node + for (param in params) { // Register parameters in the right order + parent.then(CoreArgumentBuilder.argument(param!!.name, getArgumentType(param), param.optional).also { parent = it }) + } + return node.build() + } - //TODO: Add to the help + /** + * Registers all necessary no-op nodes for the given path. + * + * @param path The full command path + * @return The last no-op node that can be used to register the executable node, + * the main command node and the last part of the command path (that isn't registered yet) + */ + private fun registerNodeFromPath(path: String): Triplet, LiteralCommandNode?, String> { + val split = path.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var parent: CommandNode = dispatcher.root + var mainCommand: LiteralCommandNode? = null + for (i in 0 until split.size - 1) { + val part = split[i] + val child = parent.getChild(part) + if (child == null) parent.addChild(CoreCommandBuilder.literalNoOp(part).executes { context: CommandContext -> executeHelpText(context) }.build().also { parent = it }) else parent = child + if (i == 0) mainCommand = parent as LiteralCommandNode // Has to be a literal, if not, well, error + } + return Triplet(parent, mainCommand, split[split.size - 1]) + } - private boolean processSenderType(TP sender, SubcommandData sd, ArrayList params) { - val sendertype = sd.senderType; - final ChromaGamerBase cg; - if (sendertype.isAssignableFrom(sender.getClass())) - params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type - else if (sender instanceof Command2MCSender // TODO: This is Minecraft only - && sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass())) - params.add(((Command2MCSender) sender).getSender()); - else if (ChromaGamerBase.class.isAssignableFrom(sendertype) - && sender instanceof Command2MCSender - && (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null - && cg.getClass() == sendertype) //The command expects a user of our system - params.add(cg); - else { - String type = sendertype.getSimpleName().chars().mapToObj(ch -> Character.isUpperCase(ch) - ? " " + Character.toLowerCase(ch) - : ch + "").collect(Collectors.joining()); - sender.sendMessage("§cYou need to be a " + type + " to use this command."); - sender.sendMessage(sd.getHelpText(sender)); //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only - return true; - } - return false; - } + /** + * Get parameter data for the given subcommand. Attempts to read it from the commands file, if it fails, it will return generic info. + * The first parameter is always the sender both in the methods themselves and in the returned array. + * + * @param method The method the subcommand is created from + * @return Parameter data objects and the sender type + * @throws RuntimeException If there is no sender parameter declared in the method + */ + private fun getCommandParametersAndSender(method: Method, argHelpManager: CommandArgumentHelpManager): Pair, Class<*>> { + val parameters = method.parameters + if (parameters.size == 0) throw RuntimeException("No sender parameter for method '$method'") + val ret = arrayOfNulls(parameters.size) + val usage = argHelpManager.getParameterHelpForMethod(method) + val paramNames = usage?.split(" ".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + for (i in 1 until parameters.size) { + val numAnn = parameters[i].getAnnotation(NumberArg::class.java) + ret[i - 1] = CommandArgument(paramNames?.get(i) ?: "param$i", parameters[i].type, + parameters[i].isVarArgs || parameters[i].isAnnotationPresent(TextArg::class.java), + if (numAnn == null) null else Pair(numAnn.lowerLimit(), numAnn.upperLimit()), + parameters[i].isAnnotationPresent(OptionalArg::class.java), + paramNames?.get(i) ?: "param$i") // TODO: Description (JavaDoc?) + } + return Pair(ret, parameters[0].type) + } - /** - * Register a command in the command system. The way this command gets registered may change depending on the implementation. - * Always invoke {@link #registerCommandSuper(ICommand2)} when implementing this method. - * - * @param command The command to register - */ - public abstract void registerCommand(TC command); + /** + * Converts the Chroma representation of the argument declaration into Brigadier format. + * It does part of the command argument type processing. + * + * @param arg Our representation of the command argument + * @return The Brigadier representation of the command argument + */ + private fun getArgumentType(arg: CommandArgument?): ArgumentType<*> { + val ptype = arg!!.type + val lowerLimit: Number = arg.limits.value0 + val upperLimit: Number = arg.limits.value1 + return if (arg.greedy) StringArgumentType.greedyString() else if (ptype == String::class.java) StringArgumentType.word() else if (ptype == Int::class.javaPrimitiveType || ptype == Int::class.java || ptype == Byte::class.javaPrimitiveType || ptype == Byte::class.java || ptype == Short::class.javaPrimitiveType || ptype == Short::class.java) IntegerArgumentType.integer(lowerLimit.toInt(), upperLimit.toInt()) else if (ptype == Long::class.javaPrimitiveType || ptype == Long::class.java) LongArgumentType.longArg(lowerLimit.toLong(), upperLimit.toLong()) else if (ptype == Float::class.javaPrimitiveType || ptype == Float::class.java) FloatArgumentType.floatArg(lowerLimit.toFloat(), upperLimit.toFloat()) else if (ptype == Double::class.javaPrimitiveType || ptype == Double::class.java) DoubleArgumentType.doubleArg(lowerLimit.toDouble(), upperLimit.toDouble()) else if (ptype == Char::class.javaPrimitiveType || ptype == Char::class.java) StringArgumentType.word() else if (ptype == Boolean::class.javaPrimitiveType || ptype == Boolean::class.java) BoolArgumentType.bool() else { + StringArgumentType.word() + } + } - /** - * Registers a command in the Command2 system, so it can be looked up and executed. - * - * @param command The command to register - * @return The Brigadier command node if you need it for something (like tab completion) - */ - protected LiteralCommandNode registerCommandSuper(TC command) { - LiteralCommandNode mainCommandNode = null; - for (val meth : command.getClass().getMethods()) { - val ann = meth.getAnnotation(Subcommand.class); - if (ann == null) continue; - String methodPath = getCommandPath(meth.getName(), ' '); - val result = registerNodeFromPath(command.getCommandPath() + methodPath); - result.getValue0().addChild(getExecutableNode(meth, command, ann, result.getValue2(), new CommandArgumentHelpManager<>(command))); - if (mainCommandNode == null) mainCommandNode = result.getValue1(); - else if (!result.getValue1().getName().equals(mainCommandNode.getName())) { - MainPlugin.Instance.getLogger().warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.getClass().getSimpleName()); - } - } - if (mainCommandNode == null) { - throw new RuntimeException("There are no subcommands defined in the command class " + command.getClass().getSimpleName() + "!"); - } - return mainCommandNode; - } + /** + * Displays the help text based on the executed command. Each command node might have a help text stored. + * The help text is displayed either because of incorrect usage or it's explicitly requested. + * + * @param context The command context + * @return Vanilla command success level (0) + */ + private fun executeHelpText(context: CommandContext): Int { + println(""" + Nodes: + ${context.nodes.stream().map { node: ParsedCommandNode -> node.node.name + "@" + node.range }.collect(Collectors.joining("\n"))} + """.trimIndent()) + return 0 + } - /** - * Returns the node that can actually execute the given subcommand. - * - * @param method The subcommand method - * @param command The command object - * @param path The command path - * @return The executable node - */ - private LiteralCommandNode getExecutableNode(Method method, TC command, Subcommand ann, String path, CommandArgumentHelpManager argHelpManager) { - val paramsAndSenderType = getCommandParametersAndSender(method, argHelpManager); // Param order is important - val params = paramsAndSenderType.getValue0(); - val paramMap = new HashMap(); - for (val param : params) { - paramMap.put(param.name, param); - } - val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, params, command) - .helps(command.getHelpText(method, ann)).permits(sender -> hasPermission(sender, command, method)) - .executes(this::executeCommand); - ArgumentBuilder parent = node; - for (val param : params) { // Register parameters in the right order - parent.then(parent = CoreArgumentBuilder.argument(param.name, getArgumentType(param), param.optional)); - } - return node.build(); - } + /** + * Executes the command itself by calling the subcommand method associated with the input command node. + * + * @param context The command context + * @return Vanilla command success level (0) + */ + private fun executeCommand(context: CommandContext): Int { + println("Execute command") + println("Should be running sync: $runOnPrimaryThread") - /** - * Registers all necessary no-op nodes for the given path. - * - * @param path The full command path - * @return The last no-op node that can be used to register the executable node, - * the main command node and the last part of the command path (that isn't registered yet) - */ - private Triplet, LiteralCommandNode, String> registerNodeFromPath(String path) { - String[] split = path.split(" "); - CommandNode parent = dispatcher.getRoot(); - LiteralCommandNode mainCommand = null; - for (int i = 0; i < split.length - 1; i++) { - String part = split[i]; - var child = parent.getChild(part); - if (child == null) - parent.addChild(parent = CoreCommandBuilder.literalNoOp(part).executes(this::executeHelpText).build()); - else parent = child; - if (i == 0) mainCommand = (LiteralCommandNode) parent; // Has to be a literal, if not, well, error - } - return new Triplet<>(parent, mainCommand, split[split.length - 1]); - } - - /** - * Get parameter data for the given subcommand. Attempts to read it from the commands file, if it fails, it will return generic info. - * The first parameter is always the sender both in the methods themselves and in the returned array. - * - * @param method The method the subcommand is created from - * @return Parameter data objects and the sender type - * @throws RuntimeException If there is no sender parameter declared in the method - */ - private Pair> getCommandParametersAndSender(Method method, CommandArgumentHelpManager argHelpManager) { - val parameters = method.getParameters(); - if (parameters.length == 0) - throw new RuntimeException("No sender parameter for method '" + method + "'"); - val ret = new CommandArgument[parameters.length]; - val usage = argHelpManager.getParameterHelpForMethod(method); - val paramNames = usage != null ? usage.split(" ") : null; - for (int i = 1; i < parameters.length; i++) { - val numAnn = parameters[i].getAnnotation(NumberArg.class); - ret[i - 1] = new CommandArgument(paramNames == null ? "param" + i : paramNames[i], parameters[i].getType(), - parameters[i].isVarArgs() || parameters[i].isAnnotationPresent(TextArg.class), - numAnn == null ? null : new Pair<>(numAnn.lowerLimit(), numAnn.upperLimit()), - parameters[i].isAnnotationPresent(OptionalArg.class), - paramNames == null ? "param" + i : paramNames[i]); // TODO: Description (JavaDoc?) - } - return new Pair<>(ret, parameters[0].getType()); - } - - /** - * Converts the Chroma representation of the argument declaration into Brigadier format. - * It does part of the command argument type processing. - * - * @param arg Our representation of the command argument - * @return The Brigadier representation of the command argument - */ - private ArgumentType getArgumentType(CommandArgument arg) { - final Class ptype = arg.type; - Number lowerLimit = arg.limits.getValue0(), upperLimit = arg.limits.getValue1(); - if (arg.greedy) - return StringArgumentType.greedyString(); - else if (ptype == String.class) - return StringArgumentType.word(); - else if (ptype == int.class || ptype == Integer.class - || ptype == byte.class || ptype == Byte.class - || ptype == short.class || ptype == Short.class) - return IntegerArgumentType.integer(lowerLimit.intValue(), upperLimit.intValue()); - else if (ptype == long.class || ptype == Long.class) - return LongArgumentType.longArg(lowerLimit.longValue(), upperLimit.longValue()); - else if (ptype == float.class || ptype == Float.class) - return FloatArgumentType.floatArg(lowerLimit.floatValue(), upperLimit.floatValue()); - else if (ptype == double.class || ptype == Double.class) - return DoubleArgumentType.doubleArg(lowerLimit.doubleValue(), upperLimit.doubleValue()); - else if (ptype == char.class || ptype == Character.class) - return StringArgumentType.word(); - else if (ptype == boolean.class || ptype == Boolean.class) - return BoolArgumentType.bool(); - else { - return StringArgumentType.word(); - } - } - - /** - * Displays the help text based on the executed command. Each command node might have a help text stored. - * The help text is displayed either because of incorrect usage or it's explicitly requested. - * - * @param context The command context - * @return Vanilla command success level (0) - */ - private int executeHelpText(CommandContext context) { - System.out.println("Nodes:\n" + context.getNodes().stream().map(node -> node.getNode().getName() + "@" + node.getRange()).collect(Collectors.joining("\n"))); - return 0; - } - - /** - * Executes the command itself by calling the subcommand method associated with the input command node. - * - * @param context The command context - * @return Vanilla command success level (0) - */ - private int executeCommand(CommandContext context) { - System.out.println("Execute command"); - System.out.println("Should be running sync: " + runOnPrimaryThread); - - /*if (!hasPermission(sender, sd.command, sd.method)) { + /*if (!hasPermission(sender, sd.command, sd.method)) { sender.sendMessage("§cYou don't have permission to use this command"); return; } @@ -327,8 +280,8 @@ public abstract class Command2, TP extends Command2Send if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type val args = parsed.getContext().getArguments(); for (var arg : sd.arguments.entrySet()) {*/ - // TODO: Invoke using custom method - /*if (pj == commandline.length() + 1) { //No param given + // TODO: Invoke using custom method + /*if (pj == commandline.length() + 1) { //No param given if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) { if (cl.isPrimitive()) params.add(Defaults.defaultValue(cl)); @@ -343,13 +296,13 @@ public abstract class Command2, TP extends Command2Send return; } }*/ - /*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?) + /*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?) params.add(commandline.substring(j + 1).split(" +")); continue; }*/ - // TODO: Character handling (strlen) - // TODO: Param converter - /*} + // TODO: Character handling (strlen) + // TODO: Param converter + /*} Runnable invokeCommand = () -> { try { sd.method.setAccessible(true); //It may be part of a private class @@ -368,72 +321,53 @@ public abstract class Command2, TP extends Command2Send if (sync) Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand); else - invokeCommand.run();*/ - return 0; - } + invokeCommand.run();*/return 0 + } - public abstract boolean hasPermission(TP sender, TC command, Method subcommand); + abstract fun hasPermission(sender: TP, command: TC, subcommand: Method?): Boolean + val commandsText: Array + get() = commandHelp.toTypedArray() - public String[] getCommandsText() { - return commandHelp.toArray(new String[0]); - } + /** + * Get all registered command nodes. This returns all registered Chroma commands with all the information about them. + * + * @return A set of command node objects containing the commands + */ + val commandNodes: Set?> + get() = dispatcher.root.children.stream().map { node: CommandNode? -> node as CoreCommandNode? }.collect(Collectors.toUnmodifiableSet()) - /** - * Returns the path of the given subcommand excluding the class' path. It will start with the given replace char. - * - * @param methodName The method's name, method.getName() - * @param replaceChar The character to use between subcommands - * @return The command path starting with the replace char. - */ - @NotNull - public String getCommandPath(String methodName, char replaceChar) { - return methodName.equals("def") ? "" : replaceChar + methodName.replace('_', replaceChar).toLowerCase(); - } + /** + * Get a node that belongs to the given command. + * + * @param command The exact name of the command + * @return A command node + */ + fun getCommandNode(command: String?): CoreCommandNode { + return dispatcher.root.getChild(command) as CoreCommandNode + } - /** - * Get all registered command nodes. This returns all registered Chroma commands with all the information about them. - * - * @return A set of command node objects containing the commands - */ - public Set> getCommandNodes() { - return dispatcher.getRoot().getChildren().stream().map(node -> (CoreCommandNode) node).collect(Collectors.toUnmodifiableSet()); - } + /** + * Unregister all subcommands that were registered with the given command class. + * + * @param command The command class (object) to unregister + */ + fun unregisterCommand(command: ICommand2) { + dispatcher.root.children.removeIf { node: CommandNode -> (node as CoreCommandNode).data.command === command } + } - /** - * Get a node that belongs to the given command. - * - * @param command The exact name of the command - * @return A command node - */ - public CoreCommandNode getCommandNode(String command) { - return (CoreCommandNode) dispatcher.getRoot().getChild(command); - } + /** + * Unregisters all commands that match the given predicate. + * + * @param condition The condition for removing a given command + */ + fun unregisterCommandIf(condition: Predicate?>, nested: Boolean) { + dispatcher.root.children.removeIf { node: CommandNode? -> condition.test(node as CoreCommandNode?) } + if (nested) for (child in dispatcher.root.children) unregisterCommandIf(condition, child as CoreCommandNode) + } - /** - * Unregister all subcommands that were registered with the given command class. - * - * @param command The command class (object) to unregister - */ - public void unregisterCommand(ICommand2 command) { - dispatcher.getRoot().getChildren().removeIf(node -> ((CoreCommandNode) node).getData().command == command); - } - - /** - * Unregisters all commands that match the given predicate. - * - * @param condition The condition for removing a given command - */ - public void unregisterCommandIf(Predicate> condition, boolean nested) { - dispatcher.getRoot().getChildren().removeIf(node -> condition.test((CoreCommandNode) node)); - if (nested) - for (var child : dispatcher.getRoot().getChildren()) - unregisterCommandIf(condition, (CoreCommandNode) child); - } - - private void unregisterCommandIf(Predicate> condition, CoreCommandNode root) { - // Can't use getCoreChildren() here because the collection needs to be modifiable - root.getChildren().removeIf(node -> condition.test((CoreCommandNode) node)); - for (var child : root.getCoreChildren()) - unregisterCommandIf(condition, child); - } -} + private fun unregisterCommandIf(condition: Predicate?>, root: CoreCommandNode) { + // Can't use getCoreChildren() here because the collection needs to be modifiable + root.children.removeIf { node: CommandNode? -> condition.test(node as CoreCommandNode?) } + for (child in root.coreChildren) unregisterCommandIf(condition, child) + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt index ef1395b..278894b 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.kt @@ -1,438 +1,390 @@ -package buttondevteam.lib.chat; +package buttondevteam.lib.chat -import buttondevteam.core.MainPlugin; -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.architecture.ButtonPlugin; -import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.chat.commands.SubcommandData; -import buttondevteam.lib.player.ChromaGamerBase; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.tree.CommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; -import lombok.val; -import me.lucko.commodore.Commodore; -import me.lucko.commodore.CommodoreProvider; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.*; -import org.bukkit.entity.Player; -import org.bukkit.event.Listener; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionDefault; -import org.javatuples.Triplet; +import buttondevteam.core.MainPlugin +import buttondevteam.lib.TBMCCoreAPI +import buttondevteam.lib.architecture.ButtonPlugin +import buttondevteam.lib.architecture.Component +import buttondevteam.lib.chat.commands.CommandUtils +import buttondevteam.lib.chat.commands.SubcommandData +import buttondevteam.lib.player.ChromaGamerBase +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.Suggestion +import com.mojang.brigadier.suggestion.SuggestionProvider +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.CommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import me.lucko.commodore.Commodore +import me.lucko.commodore.CommodoreProvider +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.OfflinePlayer +import org.bukkit.command.* +import org.bukkit.entity.Player +import org.bukkit.event.Listener +import org.bukkit.permissions.Permission +import org.bukkit.permissions.PermissionDefault +import org.javatuples.Triplet +import java.lang.reflect.Method +import java.lang.reflect.Parameter +import java.util.* +import java.util.function.BiConsumer +import java.util.function.Function +import java.util.function.Supplier -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class Command2MC extends Command2 implements Listener { - public Command2MC() { - super('/', true); - } - - /** - * Don't use directly, use the method in Component and ButtonPlugin to automatically unregister the command when needed. - * - * @param command The command to register - */ - @Override - public void registerCommand(ICommand2MC command) { - /*String mainpath; +class Command2MC : Command2('/', true), Listener { + /** + * Don't use directly, use the method in Component and ButtonPlugin to automatically unregister the command when needed. + * + * @param command The command to register + */ + override fun registerCommand(command: ICommand2MC) { + /*String mainpath; var plugin = command.getPlugin(); { String cpath = command.getCommandPath(); int i = cpath.indexOf(' '); mainpath = cpath.substring(0, i == -1 ? cpath.length() : i); }*/ - var commandNode = super.registerCommandSuper(command); - var bcmd = registerOfficially(command, commandNode); - if (bcmd != null) // TODO: Support aliases - super.registerCommandSuper(command); + val commandNode = super.registerCommandSuper(command) + val bcmd = registerOfficially(command, commandNode) + if (bcmd != null) // TODO: Support aliases + super.registerCommandSuper(command) + val perm = "chroma.command." + command.commandPath.replace(' ', '.') + if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset + Bukkit.getPluginManager().addPermission(Permission(perm, + PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only + for (method in command.javaClass.methods) { + if (!method.isAnnotationPresent(Subcommand::class.java)) continue + val path = CommandUtils.getCommandPath(method.name, '.') + if (path.length > 0) { + val subperm = perm + path + if (Bukkit.getPluginManager().getPermission(subperm) == null) //Check needed for plugin reset + Bukkit.getPluginManager().addPermission(Permission(subperm, + PermissionDefault.TRUE)) //Allow commands by default, it will check mod-only + } + val pg = permGroup(command, method) + if (pg.length == 0) continue + val permGroup = "chroma.$pg" + if (Bukkit.getPluginManager().getPermission(permGroup) == null) //It may occur multiple times + Bukkit.getPluginManager().addPermission(Permission(permGroup, + PermissionDefault.OP)) //Do not allow any commands that belong to a group + } + } - var perm = "chroma.command." + command.getCommandPath().replace(' ', '.'); - if (Bukkit.getPluginManager().getPermission(perm) == null) //Check needed for plugin reset - Bukkit.getPluginManager().addPermission(new Permission(perm, - PermissionDefault.TRUE)); //Allow commands by default, it will check mod-only - for (val method : command.getClass().getMethods()) { - if (!method.isAnnotationPresent(Subcommand.class)) continue; - var path = getCommandPath(method.getName(), '.'); - if (path.length() > 0) { - var subperm = perm + path; - if (Bukkit.getPluginManager().getPermission(subperm) == null) //Check needed for plugin reset - Bukkit.getPluginManager().addPermission(new Permission(subperm, - PermissionDefault.TRUE)); //Allow commands by default, it will check mod-only - } - String pg = permGroup(command, method); - if (pg.length() == 0) continue; - String permGroup = "chroma." + pg; - if (Bukkit.getPluginManager().getPermission(permGroup) == null) //It may occur multiple times - Bukkit.getPluginManager().addPermission(new Permission(permGroup, - PermissionDefault.OP)); //Do not allow any commands that belong to a group - } - } + override fun hasPermission(sender: Command2MCSender, command: ICommand2MC, method: Method): Boolean { + return hasPermission(sender.sender, command, method) + } - @Override - public boolean hasPermission(Command2MCSender sender, ICommand2MC command, Method method) { - return hasPermission(sender.getSender(), command, method); - } + fun hasPermission(sender: CommandSender, command: ICommand2MC?, method: Method): Boolean { + if (sender is ConsoleCommandSender) return true //Always allow the console + if (command == null) return true //Allow viewing the command - it doesn't do anything anyway + var pg: String + var p = true + val cmdperm = "chroma.command." + command.commandPath.replace(' ', '.') + val path = CommandUtils.getCommandPath(method.name, '.') + val perms = arrayOf( + if (path.length > 0) cmdperm + path else null, + cmdperm, + if (permGroup(command, method).also { pg = it }.length > 0) "chroma.$pg" else null + ) + for (perm in perms) { + if (perm != null) { + if (p) { //Use OfflinePlayer to avoid fetching player data + p = if (sender is OfflinePlayer) MainPlugin.permission.playerHas(if (sender is Player) sender.location.world.name else null, sender as OfflinePlayer, perm) else false //Use sender's method + if (!p) p = sender.hasPermission(perm) + } else break //If any of the permissions aren't granted then don't allow + } + } + return p + } - public boolean hasPermission(CommandSender sender, ICommand2MC command, Method method) { - if (sender instanceof ConsoleCommandSender) return true; //Always allow the console - if (command == null) return true; //Allow viewing the command - it doesn't do anything anyway - String pg; - boolean p = true; - var cmdperm = "chroma.command." + command.getCommandPath().replace(' ', '.'); - var path = getCommandPath(method.getName(), '.'); - String[] perms = { - path.length() > 0 ? cmdperm + path : null, - cmdperm, - (pg = permGroup(command, method)).length() > 0 ? "chroma." + pg : null - }; - for (String perm : perms) { - if (perm != null) { - if (p) { //Use OfflinePlayer to avoid fetching player data - if (sender instanceof OfflinePlayer) - p = MainPlugin.permission.playerHas(sender instanceof Player ? ((Player) sender).getLocation().getWorld().getName() : null, (OfflinePlayer) sender, perm); - else - p = false; //Use sender's method - if (!p) p = sender.hasPermission(perm); - } else break; //If any of the permissions aren't granted then don't allow - } - } - return p; - } + /** + * Returns the first group found in the hierarchy starting from the command method **or** the mod group if *any* of the superclasses are mod only. + * + * @param method The subcommand to check + * @return The permission group for the subcommand or empty string + */ + private fun permGroup(command: ICommand2MC, method: Method?): String { + if (method != null) { + val sc = method.getAnnotation(Subcommand::class.java) + if (sc != null && sc.permGroup().length > 0) { + return sc.permGroup() + } + } + return if (getAnnForValue(command.javaClass, CommandClass::class.java, Function { obj: CommandClass -> obj.modOnly() }, false)) Subcommand.MOD_GROUP else getAnnForValue(command.javaClass, CommandClass::class.java, Function { obj: CommandClass -> obj.permGroup() }, "") + } - /** - * Returns the first group found in the hierarchy starting from the command method or the mod group if any of the superclasses are mod only. - * - * @param method The subcommand to check - * @return The permission group for the subcommand or empty string - */ - private String permGroup(ICommand2MC command, Method method) { - if (method != null) { - val sc = method.getAnnotation(Subcommand.class); - if (sc != null && sc.permGroup().length() > 0) { - return sc.permGroup(); - } - } - if (getAnnForValue(command.getClass(), CommandClass.class, CommandClass::modOnly, false)) - return Subcommand.MOD_GROUP; - return getAnnForValue(command.getClass(), CommandClass.class, CommandClass::permGroup, ""); - } + /** + * Loops until it finds a value that is **not** the same as def + * + * @param sourceCl The class which has the annotation + * @param annCl The annotation to get + * @param annMethod The annotation method to check + * @param def The value to ignore when looking for the result + * @param The annotation type + * @param The type of the value + * @return The value returned by the first superclass or def + */ + private fun getAnnForValue(sourceCl: Class<*>, annCl: Class, annMethod: Function, def: V): V { + var cl: Class<*>? = sourceCl + while (cl != null) { + val cc = cl.getAnnotation(annCl) + var r: V + if (cc != null && annMethod.apply(cc).also { r = it } !== def) return r + cl = cl.superclass + } + return def + } - /** - * Loops until it finds a value that is not the same as def - * - * @param sourceCl The class which has the annotation - * @param annCl The annotation to get - * @param annMethod The annotation method to check - * @param def The value to ignore when looking for the result - * @param The annotation type - * @param The type of the value - * @return The value returned by the first superclass or def - */ - private V getAnnForValue(Class sourceCl, Class annCl, Function annMethod, V def) { - for (Class cl = sourceCl; cl != null; cl = cl.getSuperclass()) { - val cc = cl.getAnnotation(annCl); - V r; - if (cc != null && (r = annMethod.apply(cc)) != def) return r; - } - return def; - } + /** + * Automatically colors the message red. + * {@see super#addParamConverter} + */ + override fun addParamConverter(cl: Class, converter: Function, errormsg: String, allSupplier: Supplier>) { + super.addParamConverter(cl, converter, "§c$errormsg", allSupplier) + } - /** - * Automatically colors the message red. - * {@see super#addParamConverter} - */ - @Override - public void addParamConverter(Class cl, Function converter, String errormsg, Supplier> allSupplier) { - super.addParamConverter(cl, converter, "§c" + errormsg, allSupplier); - } + fun unregisterCommands(plugin: ButtonPlugin) { + unregisterCommandIf({ node: CoreCommandNode -> Optional.ofNullable(node.data.command).map { obj: ICommand2MC -> obj.plugin }.map { obj: ButtonPlugin? -> plugin.equals(obj) }.orElse(false) }, true) + } - public void unregisterCommands(ButtonPlugin plugin) { - unregisterCommandIf(node -> Optional.ofNullable(node.getData().command).map(ICommand2MC::getPlugin).map(plugin::equals).orElse(false), true); - } + fun unregisterCommands(component: Component<*>) { + unregisterCommandIf({ node: CoreCommandNode -> + Optional.ofNullable(node.data.command).map { obj: ICommand2MC -> obj.plugin } + .map { comp: ButtonPlugin -> component.javaClass.simpleName == comp.javaClass.simpleName }.orElse(false) + }, true) + } - public void unregisterCommands(Component component) { - unregisterCommandIf(node -> Optional.ofNullable(node.getData().command).map(ICommand2MC::getPlugin) - .map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false), true); - } + override fun handleCommand(sender: Command2MCSender, commandline: String): Boolean { + return handleCommand(sender, commandline, true) + } - @Override - public boolean handleCommand(Command2MCSender sender, String commandline) { - return handleCommand(sender, commandline, true); - } + private fun handleCommand(sender: Command2MCSender, commandline: String, checkPlugin: Boolean): Boolean { + val i = commandline.indexOf(' ') + val mainpath = commandline.substring(1, if (i == -1) commandline.length else i) //Without the slash + var pcmd: PluginCommand + return if ((!checkPlugin + || MainPlugin.Instance.prioritizeCustomCommands.get()) || Bukkit.getPluginCommand(mainpath).also { pcmd = it } == null //Our commands aren't PluginCommands + || pcmd.plugin is ButtonPlugin) //Unless it's specified in the plugin.yml + super.handleCommand(sender, commandline) else false + } - private boolean handleCommand(Command2MCSender sender, String commandline, boolean checkPlugin) { - int i = commandline.indexOf(' '); - String mainpath = commandline.substring(1, i == -1 ? commandline.length() : i); //Without the slash - PluginCommand pcmd; - if (!checkPlugin - || MainPlugin.Instance.prioritizeCustomCommands.get() - || (pcmd = Bukkit.getPluginCommand(mainpath)) == null //Our commands aren't PluginCommands - || pcmd.getPlugin() instanceof ButtonPlugin) //Unless it's specified in the plugin.yml - return super.handleCommand(sender, commandline); - else - return false; - } + private var shouldRegisterOfficially = true + private fun registerOfficially(command: ICommand2MC, node: LiteralCommandNode): Command? { + return if (!shouldRegisterOfficially || command.plugin == null) null else try { + val cmdmap = Bukkit.getServer().javaClass.getMethod("getCommandMap").invoke(Bukkit.getServer()) as SimpleCommandMap + val path = command.commandPath + val x = path.indexOf(' ') + val mainPath = path.substring(0, if (x == -1) path.length else x) + var bukkitCommand: Command + run { + //Commands conflicting with Essentials have to be registered in plugin.yml + val oldcmd = cmdmap.getCommand(command.plugin.name + ":" + mainPath) //The label with the fallback prefix is always registered + if (oldcmd == null) { + bukkitCommand = BukkitCommand(mainPath) + cmdmap.register(command.plugin.name, bukkitCommand) + } else { + bukkitCommand = oldcmd + if (bukkitCommand is PluginCommand) (bukkitCommand as PluginCommand).executor = CommandExecutor { sender: CommandSender, command: Command, label: String, args: Array -> this.executeCommand(sender, command, label, args) } + } + bukkitCommand = oldcmd ?: BukkitCommand(mainPath) + } + if (CommodoreProvider.isSupported()) TabcompleteHelper.registerTabcomplete(command, node, bukkitCommand) + bukkitCommand + } catch (e: Exception) { + if (command.component == null) TBMCCoreAPI.SendException("Failed to register command in command map!", e, command.plugin) else TBMCCoreAPI.SendException("Failed to register command in command map!", e, command.component) + shouldRegisterOfficially = false + null + } + } - private boolean shouldRegisterOfficially = true; + private fun executeCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + val user = ChromaGamerBase.getFromSender(sender) + if (user == null) { + TBMCCoreAPI.SendException("Failed to run Bukkit command for user!", Throwable("No Chroma user found"), MainPlugin.Instance) + sender.sendMessage("§cAn internal error occurred.") + return true + } + handleCommand(Command2MCSender(sender, user.channel.get(), sender), + ("/" + command.name + " " + java.lang.String.join(" ", *args)).trim { it <= ' ' }, false) ///trim(): remove space if there are no args + return true + } - private Command registerOfficially(ICommand2MC command, LiteralCommandNode node) { - if (!shouldRegisterOfficially || command.getPlugin() == null) return null; - try { - var cmdmap = (SimpleCommandMap) Bukkit.getServer().getClass().getMethod("getCommandMap").invoke(Bukkit.getServer()); - var path = command.getCommandPath(); - int x = path.indexOf(' '); - var mainPath = path.substring(0, x == -1 ? path.length() : x); - Command bukkitCommand; - { //Commands conflicting with Essentials have to be registered in plugin.yml - var oldcmd = cmdmap.getCommand(command.getPlugin().getName() + ":" + mainPath); //The label with the fallback prefix is always registered - if (oldcmd == null) { - bukkitCommand = new BukkitCommand(mainPath); - cmdmap.register(command.getPlugin().getName(), bukkitCommand); - } else { - bukkitCommand = oldcmd; - if (bukkitCommand instanceof PluginCommand) - ((PluginCommand) bukkitCommand).setExecutor(this::executeCommand); - } - bukkitCommand = oldcmd == null ? new BukkitCommand(mainPath) : oldcmd; - } - if (CommodoreProvider.isSupported()) - TabcompleteHelper.registerTabcomplete(command, node, bukkitCommand); - return bukkitCommand; - } catch (Exception e) { - if (command.getComponent() == null) - TBMCCoreAPI.SendException("Failed to register command in command map!", e, command.getPlugin()); - else - TBMCCoreAPI.SendException("Failed to register command in command map!", e, command.getComponent()); - shouldRegisterOfficially = false; - return null; - } - } + private class BukkitCommand(name: String?) : Command(name) { + override fun execute(sender: CommandSender, commandLabel: String, args: Array): Boolean { + return ButtonPlugin.getCommand2MC().executeCommand(sender, this, commandLabel, args) + } - private boolean executeCommand(CommandSender sender, Command command, String label, String[] args) { - var user = ChromaGamerBase.getFromSender(sender); - if (user == null) { - TBMCCoreAPI.SendException("Failed to run Bukkit command for user!", new Throwable("No Chroma user found"), MainPlugin.Instance); - sender.sendMessage("§cAn internal error occurred."); - return true; - } - handleCommand(new Command2MCSender(sender, user.channel.get(), sender), - ("/" + command.getName() + " " + String.join(" ", args)).trim(), false); ///trim(): remove space if there are no args - return true; - } + @Throws(IllegalArgumentException::class) + override fun tabComplete(sender: CommandSender, alias: String, args: Array): List { + return emptyList() + } - private static class BukkitCommand extends Command { - protected BukkitCommand(String name) { - super(name); - } + @Throws(IllegalArgumentException::class) + override fun tabComplete(sender: CommandSender, alias: String, args: Array, location: Location): List { + return emptyList() + } + } - @Override - public boolean execute(CommandSender sender, String commandLabel, String[] args) { - return ButtonPlugin.getCommand2MC().executeCommand(sender, this, commandLabel, args); - } + private object TabcompleteHelper { + private var commodore: Commodore? = null + private fun appendSubcommand(path: String, parent: CommandNode, + subcommand: SubcommandData?): LiteralCommandNode { + var scmd: LiteralCommandNode + if (parent.getChild(path) as LiteralCommandNode?. also { scmd = it } != null) return scmd + val scmdBuilder = LiteralArgumentBuilder.literal(path) + if (subcommand != null) scmdBuilder.requires { o: Any? -> + val sender = commodore!!.getBukkitSender(o) + subcommand.hasPermission(sender) + } + scmd = scmdBuilder.build() + parent.addChild(scmd) + return scmd + } - @Override - public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { - return Collections.emptyList(); - } + private fun registerTabcomplete(command2MC: ICommand2MC, commandNode: LiteralCommandNode, bukkitCommand: Command) { + if (commodore == null) { + commodore = CommodoreProvider.getCommodore(MainPlugin.Instance) //Register all to the Core, it's easier + commodore.register(LiteralArgumentBuilder.literal("un").redirect(RequiredArgumentBuilder.argument("unsomething", + StringArgumentType.word()).suggests { context: CommandContext?, builder: SuggestionsBuilder -> builder.suggest("untest").buildFuture() }.build())) + } + commodore!!.dispatcher.root.getChild(commandNode.name) // TODO: Probably unnecessary + val customTCmethods = Arrays.stream(command2MC.javaClass.declaredMethods) //val doesn't recognize the type arguments + .flatMap { method: Method -> + Optional.ofNullable(method.getAnnotation(CustomTabCompleteMethod::class.java)).stream() + .flatMap { ctcmAnn: CustomTabCompleteMethod -> + val paths = Optional.of>(ctcmAnn.subcommand()).filter { s: Array -> s.size > 0 } + .orElseGet { + arrayOf( + CommandUtils.getCommandPath(method.name, ' ').trim { it <= ' ' } + ) + } + Arrays.stream(paths).map { name: String? -> Triplet(name, ctcmAnn, method) } + } + }.toList() + for (subcmd in subcmds) { + val subpathAsOne = CommandUtils.getCommandPath(subcmd.method.getName(), ' ').trim { it <= ' ' } + val subpath = subpathAsOne.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + var scmd: CommandNode = cmd + if (subpath[0].length > 0) { //If the method is def, it will contain one empty string + for (s in subpath) { + scmd = appendSubcommand(s, scmd, subcmd) //Add method name part of the path (could_be_multiple()) + } + } + val parameters: Array = subcmd.method.getParameters() + for (i in 1 until parameters.size) { //Skip sender + val parameter = parameters[i] + val customParamType: Boolean + // TODO: Arg type + val param: Any = subcmd.parameters.get(i - 1) + val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete::class.java)) + .map(Function> { obj: CustomTabComplete -> obj.value() }) + val customTCmethod = customTCmethods.stream().filter { t: Triplet -> subpathAsOne.equals(t.value0, ignoreCase = true) } + .filter { t: Triplet -> param.replaceAll("[\\[\\]<>]", "").equalsIgnoreCase(t.value1.param()) } + .findAny() + val argb: RequiredArgumentBuilder = RequiredArgumentBuilder.argument(param, type) + .suggests(SuggestionProvider { context: CommandContext, builder: SuggestionsBuilder -> + if (parameter.isVarArgs) { //Do it before the builder is used + val nextTokenStart = context.getInput().lastIndexOf(' ') + 1 + builder = builder.createOffset(nextTokenStart) + } + if (customTC.isPresent) for (ctc in customTC.get()) builder.suggest(ctc) + var ignoreCustomParamType = false + if (customTCmethod.isPresent) { + val tr = customTCmethod.get() + if (tr.value1.ignoreTypeCompletion()) ignoreCustomParamType = true + val method = tr.value2 + val params = method.parameters + val args = arrayOfNulls(params.size) + var j = 0 + var k = 0 + while (j < args.size && k < subcmd.parameters.length) { + val paramObj = params[j] + if (CommandSender::class.java.isAssignableFrom(paramObj.type)) { + args[j] = commodore!!.getBukkitSender(context.getSource()) + j++ + continue + } + val paramValueString = context.getArgument(subcmd.parameters.get(k), String::class.java) + if (paramObj.type == String::class.java) { + args[j] = paramValueString + j++ + continue + } + val converter = getParamConverter(params[j].type, command2MC) ?: break + val paramValue = converter.converter.apply(paramValueString) + ?: //For example, the player provided an invalid plugin name + break + args[j] = paramValue + k++ //Only increment if not CommandSender + j++ + } + if (args.size == 0 || args[args.size - 1] != null) { //Arguments filled entirely + try { + val suggestions = method.invoke(command2MC, *args) + if (suggestions is Iterable<*>) { + for (suggestion in suggestions) if (suggestion is String) builder.suggest(suggestion as String?) else throw ClassCastException("Bad return type! It should return an Iterable or a String[].") + } else if (suggestions is Array) for (suggestion in suggestions as Array) builder.suggest(suggestion) else throw ClassCastException("Bad return type! It should return a String[] or an Iterable.") + } catch (e: Exception) { + val msg = "Failed to run tabcomplete method " + method.name + " for command " + command2MC.javaClass.simpleName + if (command2MC.component == null) TBMCCoreAPI.SendException(msg, e, command2MC.plugin) else TBMCCoreAPI.SendException(msg, e, command2MC.component) + } + } + } + if (!ignoreCustomParamType && customParamType) { + val converter = getParamConverter(ptype, command2MC) + if (converter != null) { + val suggestions = converter.allSupplier.get() + for (suggestion in suggestions) builder.suggest(suggestion) + } + } + if (ptype === Boolean::class.javaPrimitiveType || ptype === Boolean::class.java) builder.suggest("true").suggest("false") + val loweredInput = builder.remaining.lowercase(Locale.getDefault()) + builder.suggest(param).buildFuture().whenComplete(BiConsumer { s: Suggestions, e: Throwable? -> //The list is automatically ordered + s.list.add(s.list.removeAt(0)) + }) //So we need to put the at the end after that + .whenComplete(BiConsumer { ss: Suggestions, e: Throwable? -> + ss.list.removeIf { s: Suggestion -> + val text = s.text + !text.startsWith("<") && !text.startsWith("[") && !text.lowercase(Locale.getDefault()).startsWith(loweredInput) + } + }) + }) + val arg: ArgumentCommandNode = argb.build() + scmd.addChild(arg) + scmd = arg + } + } + if (shouldRegister.get()) { + commodore.register(maincmd) + //MinecraftArgumentTypes.getByKey(NamespacedKey.minecraft("")) + val pluginName = command2MC.plugin.name.lowercase(Locale.getDefault()) + val prefixedcmd = LiteralArgumentBuilder.literal(pluginName + ":" + path.get(0)) + .redirect(maincmd).build() + commodore!!.register(prefixedcmd) + for (alias in bukkitCommand.aliases) { + commodore!!.register(LiteralArgumentBuilder.literal(alias).redirect(maincmd).build()) + commodore!!.register(LiteralArgumentBuilder.literal("$pluginName:$alias").redirect(maincmd).build()) + } + } + } + } - @Override - public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { - return Collections.emptyList(); - } - } - - private static class TabcompleteHelper { - private static Commodore commodore; - - private static LiteralCommandNode appendSubcommand(String path, CommandNode parent, - SubcommandData subcommand) { - LiteralCommandNode scmd; - if ((scmd = (LiteralCommandNode) parent.getChild(path)) != null) - return scmd; - var scmdBuilder = LiteralArgumentBuilder.literal(path); - if (subcommand != null) - scmdBuilder.requires(o -> { - var sender = commodore.getBukkitSender(o); - return subcommand.hasPermission(sender); - }); - scmd = scmdBuilder.build(); - parent.addChild(scmd); - return scmd; - } - - private static void registerTabcomplete(ICommand2MC command2MC, LiteralCommandNode commandNode, Command bukkitCommand) { - if (commodore == null) { - commodore = CommodoreProvider.getCommodore(MainPlugin.Instance); //Register all to the Core, it's easier - commodore.register(LiteralArgumentBuilder.literal("un").redirect(RequiredArgumentBuilder.argument("unsomething", - StringArgumentType.word()).suggests((context, builder) -> builder.suggest("untest").buildFuture()).build())); - } - String[] path = command2MC.getCommandPath().split(" "); - var shouldRegister = new AtomicBoolean(true); - @SuppressWarnings("unchecked") var maincmd = commodore.getRegisteredNodes().stream() - .filter(node -> node.getLiteral().equalsIgnoreCase(path[0])) - .filter(node -> {shouldRegister.set(false); return true;}) - .map(node -> (LiteralCommandNode) node).findAny() - .orElseGet(() -> LiteralArgumentBuilder.literal(path[0]).build()); //Commodore 1.8 removes previous nodes - var cmd = maincmd; - for (int i = 1; i < path.length; i++) { - var scmd = subcmds.stream().filter(sd -> sd.method.getName().equals("def")).findAny().orElse(null); - cmd = appendSubcommand(path[i], cmd, scmd); //Add each part of the path as a child of the previous one - } - final var customTCmethods = Arrays.stream(command2MC.getClass().getDeclaredMethods()) //val doesn't recognize the type arguments - .flatMap(method -> Stream.of(Optional.ofNullable(method.getAnnotation(CustomTabCompleteMethod.class))) - .filter(Optional::isPresent).map(Optional::get) // Java 9 has .stream() - .flatMap(ctcm -> { - var paths = Optional.of(ctcm.subcommand()).filter(s -> s.length > 0) - .orElseGet(() -> new String[]{ - ButtonPlugin.getCommand2MC().getCommandPath(method.getName(), ' ').trim() - }); - return Arrays.stream(paths).map(name -> new Triplet<>(name, ctcm, method)); - })).collect(Collectors.toList()); - for (SubcommandData subcmd : subcmds) { - String subpathAsOne = ButtonPlugin.getCommand2MC().getCommandPath(subcmd.method.getName(), ' ').trim(); - String[] subpath = subpathAsOne.split(" "); - CommandNode scmd = cmd; - if (subpath[0].length() > 0) { //If the method is def, it will contain one empty string - for (String s : subpath) { - scmd = appendSubcommand(s, scmd, subcmd); //Add method name part of the path (could_be_multiple()) - } - } - Parameter[] parameters = subcmd.method.getParameters(); - for (int i = 1; i < parameters.length; i++) { //Skip sender - Parameter parameter = parameters[i]; - final boolean customParamType; - // TODO: Arg type - val param = subcmd.parameters[i - 1]; - val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete.class)) - .map(CustomTabComplete::value); - var customTCmethod = customTCmethods.stream().filter(t -> subpathAsOne.equalsIgnoreCase(t.getValue0())) - .filter(t -> param.replaceAll("[\\[\\]<>]", "").equalsIgnoreCase(t.getValue1().param())) - .findAny(); - var argb = RequiredArgumentBuilder.argument(param, type) - .suggests((context, builder) -> { - if (parameter.isVarArgs()) { //Do it before the builder is used - int nextTokenStart = context.getInput().lastIndexOf(' ') + 1; - builder = builder.createOffset(nextTokenStart); - } - if (customTC.isPresent()) - for (val ctc : customTC.get()) - builder.suggest(ctc); - boolean ignoreCustomParamType = false; - if (customTCmethod.isPresent()) { - var tr = customTCmethod.get(); - if (tr.getValue1().ignoreTypeCompletion()) - ignoreCustomParamType = true; - final var method = tr.getValue2(); - val params = method.getParameters(); - val args = new Object[params.length]; - for (int j = 0, k = 0; j < args.length && k < subcmd.parameters.length; j++) { - val paramObj = params[j]; - if (CommandSender.class.isAssignableFrom(paramObj.getType())) { - args[j] = commodore.getBukkitSender(context.getSource()); - continue; - } - val paramValueString = context.getArgument(subcmd.parameters[k], String.class); - if (paramObj.getType() == String.class) { - args[j] = paramValueString; - continue; - } - val converter = getParamConverter(params[j].getType(), command2MC); - if (converter == null) - break; - val paramValue = converter.converter.apply(paramValueString); - if (paramValue == null) //For example, the player provided an invalid plugin name - break; - args[j] = paramValue; - k++; //Only increment if not CommandSender - } - if (args.length == 0 || args[args.length - 1] != null) { //Arguments filled entirely - try { - val suggestions = method.invoke(command2MC, args); - if (suggestions instanceof Iterable) { - //noinspection unchecked - for (Object suggestion : (Iterable) suggestions) - if (suggestion instanceof String) - builder.suggest((String) suggestion); - else - throw new ClassCastException("Bad return type! It should return an Iterable or a String[]."); - } else if (suggestions instanceof String[]) - for (String suggestion : (String[]) suggestions) - builder.suggest(suggestion); - else - throw new ClassCastException("Bad return type! It should return a String[] or an Iterable."); - } catch (Exception e) { - String msg = "Failed to run tabcomplete method " + method.getName() + " for command " + command2MC.getClass().getSimpleName(); - if (command2MC.getComponent() == null) - TBMCCoreAPI.SendException(msg, e, command2MC.getPlugin()); - else - TBMCCoreAPI.SendException(msg, e, command2MC.getComponent()); - } - } - } - if (!ignoreCustomParamType && customParamType) { - val converter = getParamConverter(ptype, command2MC); - if (converter != null) { - var suggestions = converter.allSupplier.get(); - for (String suggestion : suggestions) - builder.suggest(suggestion); - } - } - if (ptype == boolean.class || ptype == Boolean.class) - builder.suggest("true").suggest("false"); - final String loweredInput = builder.getRemaining().toLowerCase(); - return builder.suggest(param).buildFuture().whenComplete((s, e) -> //The list is automatically ordered - s.getList().add(s.getList().remove(0))) //So we need to put the at the end after that - .whenComplete((ss, e) -> ss.getList().removeIf(s -> { - String text = s.getText(); - return !text.startsWith("<") && !text.startsWith("[") && !text.toLowerCase().startsWith(loweredInput); - })); - }); - var arg = argb.build(); - scmd.addChild(arg); - scmd = arg; - } - } - if (shouldRegister.get()) { - commodore.register(maincmd); - //MinecraftArgumentTypes.getByKey(NamespacedKey.minecraft("")) - String pluginName = command2MC.getPlugin().getName().toLowerCase(); - var prefixedcmd = LiteralArgumentBuilder.literal(pluginName + ":" + path[0]) - .redirect(maincmd).build(); - commodore.register(prefixedcmd); - for (String alias : bukkitCommand.getAliases()) { - commodore.register(LiteralArgumentBuilder.literal(alias).redirect(maincmd).build()); - commodore.register(LiteralArgumentBuilder.literal(pluginName + ":" + alias).redirect(maincmd).build()); - } - } - } - } - - private static ParamConverter getParamConverter(Class cl, ICommand2MC command2MC) { - val converter = ButtonPlugin.getCommand2MC().paramConverters.get(cl); - if (converter == null) { - String msg = "Could not find a suitable converter for type " + cl.getSimpleName(); - Exception exception = new NullPointerException("converter is null"); - if (command2MC.getComponent() == null) - TBMCCoreAPI.SendException(msg, exception, command2MC.getPlugin()); - else - TBMCCoreAPI.SendException(msg, exception, command2MC.getComponent()); - return null; - } - return converter; - } -} + companion object { + private fun getParamConverter(cl: Class<*>, command2MC: ICommand2MC): ParamConverter<*>? { + val converter = ButtonPlugin.getCommand2MC().paramConverters[cl] + if (converter == null) { + val msg = "Could not find a suitable converter for type " + cl.simpleName + val exception: Exception = NullPointerException("converter is null") + if (command2MC.component == null) TBMCCoreAPI.SendException(msg, exception, command2MC.plugin) else TBMCCoreAPI.SendException(msg, exception, command2MC.component) + return null + } + return converter + } + } +} \ No newline at end of file diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.java new file mode 100644 index 0000000..fd3db08 --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandUtils.java @@ -0,0 +1,19 @@ +package buttondevteam.lib.chat.commands; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class CommandUtils { + /** + * Returns the path of the given subcommand excluding the class' path. It will start with the given replace char. + * + * @param methodName The method's name, method.getName() + * @param replaceChar The character to use between subcommands + * @return The command path starting with the replacement char. + */ + @NotNull + public static String getCommandPath(String methodName, char replaceChar) { + return methodName.equals("def") ? "" : replaceChar + methodName.replace('_', replaceChar).toLowerCase(); + } +} diff --git a/CorePOM/pom.xml b/CorePOM/pom.xml index 3c4e49e..6227fa5 100644 --- a/CorePOM/pom.xml +++ b/CorePOM/pom.xml @@ -9,7 +9,7 @@ pom master-SNAPSHOT - 1.18.10 + 1.18.26 Core POM for Chroma @@ -21,30 +21,14 @@ maven-compiler-plugin 3.8.1 - 8 + 17 - - com.github.bsideup.jabel - jabel-javac-plugin - 0.2.0 - - + org.projectlombok lombok - ${lombok.version} - - - com.github.TBMCPlugins.ChromaCore - ButtonProcessor - master-SNAPSHOT - + 1.18.26 + - - com.github.bsideup.jabel.JabelJavacProcessor - lombok.launch.AnnotationProcessorHider$AnnotationProcessor - - buttondevteam.buttonproc.ButtonProcessor - @@ -97,31 +81,4 @@ provided - - - - intellij-idea-only - - - idea.maven.embedder.version - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - - - - - - - diff --git a/pom.xml b/pom.xml index eb8cfde..af71e1c 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,8 @@ pom master-SNAPSHOT - 1.18.12 - + 1.18.26 + Chroma Parent @@ -27,7 +27,7 @@ maven-compiler-plugin 3.8.1 - 8 + 17