Skip to main content

Writing a Custom Module

XDiscordUltimate features are built on a single abstraction: com.xreatlabs.xdiscordultimate.modules.Module. A module owns its lifecycle, reads its own features.<name> config section, and optionally contributes slash commands and text commands to the Discord bot.

The Module Contract

Subclass Module and pass the plugin into the constructor. Implement the four abstract members; override the lifecycle and command hooks you need.
import com.xreatlabs.xdiscordultimate.modules.Module;
import com.xreatlabs.xdiscordultimate.XDiscordUltimate;

public class BroadcastModule extends Module {

    public BroadcastModule(XDiscordUltimate plugin) {
        super(plugin);
    }

    @Override
    public String getName() {
        return "Broadcast";          // reads config from features.broadcast
    }

    @Override
    public String getDescription() {
        return "Broadcasts a configurable message to a Discord channel on demand.";
    }

    @Override
    protected void onEnable() {
        info("Broadcast module enabled");
    }

    @Override
    protected void onDisable() {
        info("Broadcast module disabled");
    }
}
The public enable() / disable() methods on Module wrap your onEnable() / onDisable() in try/catch and track the enabled flag — the ModuleManager calls those, never your lifecycle methods directly.

Methods You Can Override

MethodCalled WhenDefault
getName() / getDescription()alwaysabstract — must implement
onEnable() / onDisable()module enabled/disabledabstract — must implement
onDiscordReady()JDA gateway is readyno-op
getSlashCommands()commands are consolidated at Discord-readyempty list
handleSlashCommand(SlashCommandInteractionEvent)a slash command is receivedfalse
handleTextCommand(MessageReceivedEvent, String, String)a text command is receivedfalse
reloadConfig()reloadModuleConfigs() hot reloadno-op

Configuration

getConfig() returns the module’s own features.<name> ConfigurationSection, where <name> is getName() lowercased with spaces replaced by hyphens. "Broadcast" therefore resolves to features.broadcast. Use isSubFeatureEnabled(String) to check nested toggles under that section.
@Override
protected void onEnable() {
    String message = getConfig().getString("message", "Server is live!");
    long channelId  = getConfig().getLong("channel-id", 0L);
    this.message    = message;
    this.channelId  = channelId;
}
The matching config.yml section:
features:
  broadcast:
    enabled: true
    message: "🚀 The server just started — come join!"
    channel-id: 123456789012345678
The master feature flag (features.broadcast.enabled) is read by ModuleManager via ConfigManager.isFeatureEnabled("broadcast"); it controls whether the module is enabled at all.

A Complete, Compilable Module

This BroadcastModule registers a /broadcast slash command and, when invoked, posts the configured message to the configured channel. It reads its message on enable and on reloadConfig().
package com.xreatlabs.xdiscordultimate.modules.broadcast;

import com.xreatlabs.xdiscordultimate.XDiscordUltimate;
import com.xreatlabs.xdiscordultimate.modules.DependsOn;
import com.xreatlabs.xdiscordultimate.modules.Module;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;

import java.util.Collections;
import java.util.List;

@DependsOn({"verification"})
public class BroadcastModule extends Module {

    private String message;
    private long channelId;

    public BroadcastModule(XDiscordUltimate plugin) {
        super(plugin);
    }

    @Override
    public String getName() {
        return "Broadcast";
    }

    @Override
    public String getDescription() {
        return "Broadcasts a configured message to a Discord channel.";
    }

    @Override
    protected void onEnable() {
        loadConfig();
    }

    @Override
    protected void onDisable() {
        // nothing to clean up
    }

    @Override
    public void reloadConfig() {
        loadConfig();
    }

    private void loadConfig() {
        message   = getConfig().getString("message", "Server is live!");
        channelId = getConfig().getLong("channel-id", 0L);
    }

    @Override
    public List<SlashCommandData> getSlashCommands() {
        return List.of(
            Commands.slash("broadcast", "Post the configured broadcast message")
                     .setDefaultPermissions(
                         DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER))
        );
    }

    @Override
    public boolean handleSlashCommand(SlashCommandInteractionEvent event) {
        if (!"broadcast".equals(event.getName())) {
            return false;
        }
        if (channelId == 0L) {
            event.reply("No channel configured for features.broadcast.channel-id.")
                 .setEphemeral(true).queue();
            return true;
        }

        TextChannel channel = plugin.getDiscordManager()
                                    .getJda()
                                    .getTextChannelById(channelId);
        if (channel == null) {
            event.reply("Configured channel not found.").setEphemeral(true).queue();
            return true;
        }

        channel.sendMessage(message).queue(
            success -> event.reply("Broadcast sent.").queue(),
            error   -> event.reply("Failed: " + error.getMessage()).setEphemeral(true).queue()
        );
        return true;
    }
}
Slash commands from every enabled module are consolidated and upserted to Discord once, during ModuleManager.onDiscordReady(). Duplicate command names across modules are skipped — name your commands uniquely.

Declaring Dependencies

@DependsOn lists the registration keys of modules that must be enabled before this one. ModuleManager.checkDependencies() reads the annotation reflectively; if any listed dependency is disabled in config, this module is skipped with a warning.
@DependsOn({"verification", "chat-bridge"})
public class MyModule extends Module { /* ... */ }
The annotation targets the type and is retained at runtime:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DependsOn {
    String[] value();
}

Registering a Custom Module

There is currently no public runtime API to register a module from an external plugin. ModuleManager.registerModule(...) is private, and loadModules() hard-codes the 19 built-in modules.
To wire a new module in today, it must be registered in source — either inside the XDiscordUltimate codebase or via a fork:
// com.xreatlabs.xdiscordultimate.modules.ModuleManager#loadModules
registerModule("broadcast", new com.xreatlabs.xdiscordultimate.modules.broadcast.BroadcastModule(plugin));
Add the registration alongside the other registerModule(...) calls, before the enable loop. The first argument is both the map key and the config path (features.broadcast); pass the same key to @DependsOn when another module depends on it. For an out-of-tree plugin that needs module-style integration without modifying the source, use the alternatives the public API already exposes:
  • XDiscordEventBus for reactive behavior (see API Overview).
  • getDatabaseManager() for direct reads/writes of verified-account and stats tables.
  • getDiscordManager() to attach your own JDA ListenerAdapter if you expose JDA on your classpath.
A future release is expected to open module registration to third-party plugins; until then, treat the module list as part of the core build.

Logging and Feature Flags

Use the inherited helpers rather than the raw plugin logger — they prefix every line with the module name and respect the debug flag:
debug("Handled broadcast for channel " + channelId);  // only when debug is on
info("Broadcast sent");                                // always
warning("Channel not found: " + channelId);
error("JDA rejected the message", throwable);
Always gate side effects behind the feature flag and isEnabled() so a disabled module does no work.

Next Steps

Events

The event bus your module can publish to and listen on.

Architecture

How modules, the manager, and the event bus interact at runtime.