Skip to main content

Architecture

XDiscordUltimate is a modular Bukkit plugin that bridges a Minecraft server and a Discord bot. A small core orchestrates a ModuleManager, a JDA-backed DiscordManager, an async DatabaseManager, and a reflection-based event bus. Every feature is implemented as a Module.

System Architecture

The main class XDiscordUltimate extends JavaPlugin owns every subsystem as a field and exposes it through getters. It is a singleton: XDiscordUltimate.getInstance().

Lifecycle

Enable

Order matters: the LibraryManager (Libby) must finish before anything that touches JDA or JDBC, because those libraries are downloaded at runtime rather than shaded. If loadAllLibraries() throws, the plugin disables itself. The same is true for DatabaseManager.initialize() — a SQL failure disables the plugin.

Disable

onDisable() reverses the order: detach the ConsoleAppender, let the chat-bridge and server-logging modules send their shutdown messages, ModuleManager.disableModules(), DiscordManager.shutdown(), then DatabaseManager.close().

Module System

Every feature is a Module. The ModuleManager registers the 19 built-in modules in loadModules(), resolves their @DependsOn dependencies, checks each module’s feature flag, and calls enable(). The abstract base class defines the contract:
package com.xreatlabs.xdiscordultimate.modules;

import com.xreatlabs.xdiscordultimate.XDiscordUltimate;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
import org.bukkit.configuration.ConfigurationSection;

public abstract class Module {

    protected final XDiscordUltimate plugin;

    public Module(XDiscordUltimate plugin) { /* store plugin */ }

    public abstract String getName();
    public abstract String getDescription();

    protected abstract void onEnable();
    protected abstract void onDisable();

    public void onDiscordReady() {}
    public List<SlashCommandData> getSlashCommands() { return Collections.emptyList(); }
    public boolean handleSlashCommand(SlashCommandInteractionEvent event) { return false; }
    public boolean handleTextCommand(MessageReceivedEvent event, String command, String args) { return false; }
    public void reload() {}      // disable() then enable()
    public void reloadConfig() {}
    public boolean isEnabled() {}

    protected ConfigurationSection getConfig() {
        // returns the features.<name> section from config.yml
    }
    protected boolean isSubFeatureEnabled(String subFeature) {}

    protected void debug(String message) {}
    protected void info(String message) {}
    protected void warning(String message) {}
    protected void error(String message) {}
    protected void error(String message, Throwable throwable) {}
}
enable() and disable() are the public entry points — they guard onEnable()/onDisable() with try/catch and flip the enabled flag. Modules must implement only the four abstract members; everything else has a no-op default.

Configuration

getConfig() resolves the module’s features.<name> ConfigurationSection, where <name> is getName() lowercased with spaces replaced by hyphens. A module named "Chat Bridge" reads from features.chat-bridge. The feature flag itself is ConfigManager.isFeatureEnabled(key) — the key matches the registration key in ModuleManager.loadModules() (for example "chat-bridge").

Dependencies

A module may declare required modules with @DependsOn. ModuleManager.checkDependencies() reads the annotation reflectively and refuses to enable the module unless every dependency is enabled in config.
@DependsOn({"verification"})
public class MyModule extends Module { /* ... */ }

ModuleManager API

MethodReturns
getModule(String key)Module by registration key, or null
<T extends Module> getModule(Class<T>)module of the given class, or null
getModules()defensive copy Map<String, Module>
isModuleActive(String key)true if loaded and enabled
reloadModules()full restart — disable all, clear map, reload
reloadModuleConfigs()hot reload cached config in each enabled module
onDiscordReady()fan-out to every enabled module

Data Layer

DatabaseManager wraps a HikariCP HikariDataSource. SQLite is the default; MySQL and PostgreSQL are selected by database.type in config.yml. Pool sizing is fixed: max 10 connections, min idle 2, 30 s connection timeout, 60 s leak detection.

Async by Default

Every read and write goes through executeAsync, which runs a DatabaseOperation<T> on the common ForkJoinPool:
@FunctionalInterface
public interface DatabaseOperation<T> {
    T execute() throws SQLException;
}

public <T> CompletableFuture<T> executeAsync(DatabaseOperation<T> operation);
A SQLException inside the operation is logged and rethrown wrapped in a RuntimeException, so the CompletableFuture completes exceptionally. Typical call sites:
CompletableFuture<String>   discordId   = db.getDiscordId(uuid);
CompletableFuture<UUID>     mcUuid      = db.getMinecraftUuid(discordId);
CompletableFuture<Boolean>  linked      = db.isDiscordLinked(discordId);
CompletableFuture<Boolean>  linked      = db.linkAccount(uuid, discordId, mcName, discordName);
CompletableFuture<Boolean>  unlinked    = db.unlinkAccount(uuid);
CompletableFuture<Integer>  ticketId    = db.createTicket(uuid, subject);
CompletableFuture<Void>     statDelta   = db.updatePlayerStats(uuid, "joins", 1);
Synchronous variants exist for contexts already running off the main thread: getDiscordIdSync(UUID), getMinecraftUuidSync(String), isDiscordLinkedSync(String), and getLinkedAccountInfoSync(String) (returns a LinkedAccountInfo snapshot). Never call the sync variants on the main server thread.

Schema and Migrations

Tables are created idempotently on initialize(): verified_users, verification_codes, tickets, ticket_messages, moderation_logs, player_stats, activity_rewards, schema_versions. Versioning is tracked in schema_versions. runMigrations() compares the stored version against CURRENT_SCHEMA_VERSION and applies each missing migration in order. Each migration step updates the stored version only after its statements succeed.

Inner Data Classes

  • LinkedAccountInfo — immutable snapshot with getDiscordId(), getDiscordName(), getMinecraftName(), getMinecraftUuid(), getLinkedAt().
  • VerificationCodegetDiscordId(), getCode(), getUsername(), getCreatedAt(), getExpiresAt(), isExpired().

Event Bus

XDiscordEventBus is a static, reflection-based dispatcher. It is not a Bukkit event bus, and events are plain POJOs — do not annotate handlers with @EventHandler and do not implement Listener.
public static void register(Object listener);
public static void unregister(Object listener);
public static void publish(Object event);
public static void clear();
public static int getListenerCount();
On publish(event) the bus iterates every registered listener and, for each declared method with exactly one parameter whose type isInstance(event), calls setAccessible(true) and invoke(). That means a listener can name its handler method anything and can handle multiple event types by overloading the parameter. Exceptions inside a handler are swallowed so one failing listener cannot break dispatch. Example event:
public class PlayerVerifiedEvent {
    public PlayerVerifiedEvent(UUID playerUuid, String playerName,
                               String discordId, String discordUsername);
    public UUID   getPlayerUuid();
    public String getPlayerName();
    public String getDiscordId();
    public String getDiscordUsername();
    public long   getTimestamp();
}
PlayerUnlinkedEvent follows the same shape. The core publishes these after the database write for linking/unlinking succeeds.

Technology Stack

Platform

Spigot/Paper 1.16.5+ API, Java 16 source/target, Java 17+ runtime.

Discord

JDA 5.0.0-beta.18 (fetched at runtime by Libby).

Persistence

HikariCP 5.0.1 pool, SQLite/MySQL/PostgreSQL JDBC drivers (runtime-fetched).

Optional Hooks

LuckPerms 5.4, PlaceholderAPI 2.11.3, Vault 1.7 (all compileOnly).

Next Steps

Writing Modules

Implement a feature on the Module abstraction.

API Overview

Database, event, and manager APIs in detail.