Skip to content
This repository has been archived by the owner on Nov 25, 2022. It is now read-only.

Writing a command module

Cole Bryant edited this page Jan 31, 2020 · 7 revisions
Basic module example
public class ModulePing extends Module {
    public ModulePing() {
        super(new ModuleInfo.Builder(ModulePing.class)
                .withName("ping")
                .withDescription("Check for bot responsiveness"));
    }

    @Override
    public void invoke(@NotNull final CommandContext ctx) {
        ctx.reply("Pong!");
    }
}

Creating a command module is relatively easy, which makes doing so a good beginner task that doesn't require deep knowledge of the bot. Every command in ghost2 extends Module, and so will any modules you write. Make sure you precisely adhere to the rules on writing a Module class or you're guaranteed to get an InvalidModuleException somewhere down the line.

Requirements

Two things are contractually required from a Module subclass:

  1. A call to super() in the constructor with a ModuleInfo.Builder
  2. An invoke() implementation

Also required, although not contractually, is a publicly accessible no-args constructor.

ModuleInfo

Finding your way around ModuleInfo is the part of writing a Module that's not obvious at first glance. ModuleInfo contains all the metadata for a module, i.e. its name, description, category, permissions, and aliases. Name, description, and type are the three fields you should be most concerned with, as they're required for every ModuleInfo instance. Name and description, specifically, should be not-blank, meaning they're both not null and contain at least one non-whitespace character.

To construct a ModuleInfo object, you need to use the built-in ModuleInfo.Builder class. When you look at the arguments for the builder's constructor, you'll notice that it takes a Class<? extends Module>. Pass in the actual class of the Module you're creating here. After that, you can do what you want with the builder. Its parameters are set in the typical builder-pattern fashion via the with... methods.

There are a few things that are done automatically for you, as well:

  • The CommandType for your module is assigned automatically based on what package it's in.
  • build() is called automatically by Module's constructor.

invoke

After writing your constructor, you can move on to what your command actually does. invoke() does exactly what you'd think: it gets called when your method is invoked. You can block for a reasonable amount of time, since each command instance receives its own thread; however, you should always try to give the user some feedback as soon as possible, and free the thread back to the executor's pool eventually.

CommandContext

CommandContext offers basically everything you could possibly need when your command is invoked. Upon invocation, CommandDispatcher will construct a CommandContext instance for you and pass it in to invoke(). From there, you can use all the getter methods that CommandContext offers to create rich commands that respond fluently to most situations.

CommandContext also gives you some quickfire reply methods that just take a message string if you want to send a plaintext message:

  • CommandContext#reply() sends a plain-text message to the channel the command was invoked in.
  • CommandContext#replyEmbed() sends an embed message to the channel the command was invoked in.
  • CommandContext#replyDirect() sends a plain-text message directly to the user who invoked the command. There are blocking and non-blocking variants of each reply method. All of them allow you to access the sent message, either directly through a Mono.

Notes and best practices

  • Inadvertently extending another Module subclass will generate a runtime warning and make your module be ignored by the command registry. Don't do it. You can mark your Module classes final to help avoid this across the project.

  • Use the @ReflectiveAccess annotation where necessary. Reflection is utilized extensively throughout ghost2, both by internal classes and Spring. In most cases, your IDE won't detect that a member is accessed reflectively; for convenience, you can mark it with @ReflectiveAccess and configure your IDE to suppress unused warnings for members marked with the annotation.