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.
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.
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.
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.
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.
@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.
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#loadModulesregisterModule("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.
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 oninfo("Broadcast sent"); // alwayswarning("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.