How to add a new module
Prerequisites
- Know the kebab-case ID your module will use (e.g.
my-feature). - The module class lives under
src/main/java/de/kreiscraft/modules/<name>/.
Steps
1. Create the module class
Create src/main/java/de/kreiscraft/modules/<name>/<Name>Module.java implementing Module.
package de.kreiscraft.modules.myfeature;
import de.kreiscraft.core.module.Module;import de.kreiscraft.core.module.ModuleContext;import de.kreiscraft.core.module.ModuleException;import org.spongepowered.configurate.ConfigurationNode;import org.spongepowered.configurate.serialize.SerializationException;
public final class MyFeatureModule implements Module {
private MyFeatureConfig config;
@Override public String id() { return "my-feature"; // kebab-case, matches config key }
// Override only if this module depends on another module being enabled first. // @Override // public List<String> dependsOn() { return List.of("other-module"); }
@Override public void enable(ModuleContext ctx) { try { this.config = ctx.moduleConfig().get(MyFeatureConfig.class); } catch (SerializationException e) { throw new ModuleException("failed to load config for module " + id(), e); } if (config == null) config = new MyFeatureConfig(/* defaults */);
// Register listeners, commands, services here using ctx.* ctx.events().register(new MyFeatureListener(config, ctx.messages())); ctx.commands().register(MyFeatureCommand.build(ctx.messages()), "My feature command"); }
@Override public void disable() { // Clean up resources that EventBus.unregisterAll() won't handle. // Listeners registered via ctx.events().register() are unregistered automatically. }
@Override public void onConfigReload(ConfigurationNode moduleConfig) { try { this.config = moduleConfig.get(MyFeatureConfig.class); } catch (SerializationException e) { throw new ModuleException("failed to reload config for module " + id(), e); } if (config == null) config = new MyFeatureConfig(/* defaults */); // Push new config to any stateful objects that hold a reference. }}ModuleContext fields available in enable():
| Field | Type | Purpose |
|---|---|---|
ctx.plugin() |
JavaPlugin |
Raw plugin handle |
ctx.moduleConfig() |
ConfigurationNode |
Pre-scoped to modules.<id> |
ctx.messages() |
MessageService |
Send/render i18n messages |
ctx.services() |
ServiceRegistry |
Publish or consume services |
ctx.scheduler() |
Scheduler |
Schedule tasks |
ctx.commands() |
CommandRegistrar |
Register Brigadier commands |
ctx.events() |
EventBus |
Register Bukkit listeners |
2. Create a @ConfigSerializable config class
package de.kreiscraft.modules.myfeature;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializablepublic record MyFeatureConfig(boolean someFlag, int someValue) {
public MyFeatureConfig { // Apply safe defaults when keys are absent (Configurate initialises // primitive int to 0 and boolean to false when the key is missing). if (someValue == 0) someValue = 42; }}Supported custom serializer types: Duration (e.g. "5m"), Location, Component,
Material, Vector. These are registered automatically via KreiscraftSerializers.
3. Add the module entry to config.yml
In src/main/resources/config.yml, add under modules::
modules: my-feature: enabled: true some-flag: true some-value: 42A module is disabled by default if the enabled key is absent.
4. Add message keys to both lang files
Add identical key sets to both src/main/resources/lang/de.yml and lang/en.yml.
# lang/en.yml and lang/de.yml (keys must be identical in both files)my-feature: action-done: "<prefix> <green>Done: <result>" error-foo: "<prefix> <red>Something went wrong."LangParityTest enforces that both files share the same key set. The build will fail if they diverge.
5. Register the module in KreiscraftBootstrap.java
In src/main/java/de/kreiscraft/KreiscraftBootstrap.java, add one line inside start():
modules.register(new de.kreiscraft.modules.myfeature.MyFeatureModule());Insert it in alphabetical order with the existing registrations.
6. Add permissions to plugin.yml (if needed)
If the module registers commands or gates features behind a permission node, declare it in
src/main/resources/plugin.yml:
permissions: kreiscraft.myfeature.use: description: "Allows using the my-feature command" default: op7. Write at least one test
Create src/test/java/de/kreiscraft/modules/myfeature/MyFeatureModuleTest.java.
Minimal pattern using MockBukkit:
@ExtendWith(MockitoExtension.class)class MyFeatureModuleTest {
private ServerMock server;
@BeforeEach void setUp() { server = MockBukkit.mock(); }
@AfterEach void tearDown() { MockBukkit.unmock(); }
@Test void enable_withValidConfig_doesNotThrow() { // Build a ModuleContext with a real or mocked config node and assert behaviour. }}Run all tests (including LangParityTest) with:
./gradlew test