Skip to content

v5 Subsystems

Jake Moore edited this page Aug 31, 2026 · 6 revisions

Subsystems: Modules and Features

⚠️ Usage ⚠️

Available in spigot-utils and its inheritors (spigot-jar).

A subsystem is a mini-plugin inside your plugin: its own config, its own commands, its own lifecycle. There are two kinds, and they differ in exactly one way.

Module Feature
can an admin turn it off? yes, via modules.yml no, always enabled
config file modules.yml features.yml
register with registerModule(Module...) registerFeature(Feature...)
data folder getModuleDataFolder() getFeatureDataFolder()

Both extend AbstractSubsystem, so everything below applies to either.

Migrating from v4: the package moved. com.kamikazejam.kamicommon.modules is now com.kamikazejam.kamicommon.subsystem.module, and Feature is new.

Writing one

public class CustomEnchantsModule extends Module {
    private final MyPlugin plugin;
    public CustomEnchantsModule(MyPlugin plugin) { this.plugin = plugin; }

    @Override public @NotNull KamiPlugin getPlugin() { return plugin; }
    @Override public String getName() { return "CustomEnchants"; }
    @Override public boolean isEnabledByDefault() { return true; }

    @Override
    public @NotNull VersionedComponent defaultPrefix() {
        return NmsAPI.getVersionedComponentSerializer().fromMiniMessage("<gold>[Enchants] ");
    }

    @Override
    public void onEnable() {
        registerListeners(new EnchantListener());
        registerCommands(new CmdEnchant());
    }

    @Override
    public void onDisable() { }
}

Registration happens in your plugin's enable, and takes instances:

registerModule(new CustomEnchantsModule(this));
registerFeature(new ScoreboardFeature(this));

Both are varargs, so several can be registered in one call. Keep your own reference to the instance if you need it later; there is no lookup by class.

Lifecycle

  1. config is loaded
  2. onEnable(), where you register your listeners, tasks and commands
  3. onDisable()

A module can be disabled and re-enabled, and goes through the whole cycle again. Config reloads do not re-run onEnable, so parse config values in a ConfigObserver rather than in onEnable.

Migrating from v4: Module#onConfigLoaded(ModuleConfig) was removed. Register a ConfigObserver instead. See Configuration System.

Registration helpers

AbstractSubsystem carries the same helpers as KamiPlugin, and everything registered is cleaned up when the subsystem disables:

registerListeners(Listener...);      unregisterListeners(Listener...);   unregisterListeners();
registerTasks(BukkitTask...);        unregisterTasks(BukkitTask...);     unregisterTasks();
registerDisableables(Disableable...); unregisterDisableables(...);       unregisterDisableables();
registerCommands(KamiCommand...);    unregisterCommands(KamiCommand...); unregisterCommands();

Logging goes through getLogger(), which returns a ComponentLogger:

getLogger().info(serializer.fromMiniMessage("<green>Loaded " + arenas.size() + " arenas"));

Migrating from v4: Module#log(String) is gone; use getLogger().info(...). warnWithTrace and errorWithTrace were removed; ComponentLogger takes a Throwable and a component instead.

Config layout

Each subsystem needs a default config in your jar's resources. The path is built as getModuleYmlPath() + "/" + getName() + "Module.yml", so tell the framework the folder:

@Override public String getModuleYmlPath()  { return "com/myplugin/modules"; }
@Override public String getFeatureYmlPath() { return "com/myplugin/features"; }

With getName() returning CustomEnchants, the resource must be at exactly:

com/myplugin/modules/CustomEnchantsModule.yml

All of a plugin's module resources live in that one folder, flat. The subsystem's own Java package is not part of the path. Features work the same way with getFeatureYmlPath() and a Feature.yml suffix.

⚠️ You must override these two methods. They return null by default, and a subsystem resolving its config fails a null check naming the method to override. A missing resource file fails the same way, naming the path it looked for. Both are caught and logged by the subsystem manager, as Can not register the module: <name> or Can not register the feature: <name> with a stack trace, so the plugin still enables and the subsystem never runs. Check your console if a subsystem is silently absent.

On the server, the config lands at plugins/<plugin>/modules/<name>.yml. Override getConfigFileDestination() or getConfigResourcePath() on an individual subsystem to change either side.

modules.yml

Appears in your plugin's data folder once you register a module. It turns modules on and off and holds per-module properties, including modulePrefix, which fills {prefix} and %prefix% in messages built through buildMessage(String) or buildMiniMessage(String). A module disabled here stays disabled until the next restart.

features.yml is the same file for features, minus the enable toggle.

Spaces in a subsystem name become underscores in its config key. A module named My Module is configured under modules.My_Module, and a feature named My Feature under features.My_Feature. The name itself is unchanged; only the key is normalised, and every reader of it normalises the same way, so enabled, modulePrefix and featurePrefix all sit under one spelling.

Supplemental configs

A subsystem can own further configs with defaults from its own resources:

KamiConfig arenas = new KamiConfig(this, new File(getModuleDataFolder(), "arenas.yml"));

Using the AbstractSubsystem constructors requires you to override AbstractSubsystem#getSupplementalConfigResource, which tells the constructor where to find the resource in the jar. If you do not want defaults, pass null as the supplier instead.

Clone this wiki locally