A module is registered at startup in ModuleManager.registerModules(). It becomes active only when:
modules.<key>.enabledistrueinconfig.yml(seeModule.isEnabledByConfig), and- At least one online player has that module enabled in their personal toggles.
When no players want the module, disable() runs and it leaves the active set. Design enable() / disable() so they can run repeatedly and leave no leaked listeners or tasks.
Implement ir.buddy.mint.module.Module:
| Method | Purpose |
|---|---|
getName() |
Human-readable title (shown in GUI and /mint admin modules). |
getConfigPath() |
Must be modules.<kebab-key> (e.g. modules.door-knock). Used for config and for the internal module key. |
getDescription() |
Short line for GUI and command output. |
enable() |
Register listeners, start region-safe tasks, etc. |
disable() |
Unregister listeners (HandlerList.unregisterAll(this) for Listener impls), cancel repeating tasks, clear state. |
Optional overrides:
isEnabledByConfig— only if the defaultgetBoolean(getConfigPath() + ".enabled", true)is wrong.supportsAsyncPreparation/prepareEnable— use when heavy IO or CPU work must finish beforeenable()runs on the main thread (seeModuleManager.requestModuleEnable).
- Config / YAML key:
getModuleKey(module)strips themodules.prefix. That key must match:- The block under
modules:insrc/main/resources/config.yml(e.g.door-knock:). - An entry under
module-icons:insrc/main/resources/gui.ymlif you want a custom icon (otherwise the GUI uses a default).
- The block under
- Normalization: CLI and PlaceholderAPI matching use
ModuleManager.normalizeModuleInput(lowercase, alphanumeric only). Display names like"Door Knock"still matchdoorknockanddoor-knock. - Class name:
SomethingModulein packageir.buddy.mint.module.impl.
Keep one module class per feature unless sub-components are clearly private helpers in the same file.
- Class —
src/main/java/ir/buddy/mint/module/impl/<Name>Module.javaimplementingModule(andListenerwhen using events). ModuleManager.registerModules()—modules.add(new YourModule(plugin));
UseMintPluginin the constructor only when you need APIs beyondJavaPlugin(e.g.getModuleManager(),getGuiConfig()). OtherwiseJavaPluginkeeps modules easy to test.config.yml— addmodules.<key>:with at leastenabled: true/falseand any module-specific options. Defaults should match whatPluginConfigValidatorand your code expect.gui.yml— addmodule-icons.<key>: <MATERIAL>for a sensible inventory icon (comment ingui.ymllists the pattern).- Config validation —
PluginConfigValidatorlogs unknownmodules.*keys and missing registered keys; after adding a module, keys should line up so the log stays clean on startup.
-
Per-player: At the start of any player-visible behavior (events, schedulers tied to a player), guard with:
ModuleAccess.isEnabledForPlayer(plugin, this, player)Use the same
pluginreference you registered the listener with (the Mint plugin instance). -
Building / breaking blocks: Before changing the world on behalf of a player, call:
ModuleAccess.canBuild(plugin, player, location)so WorldGuard / GriefPrevention rules and
mint.bypass.protectionare respected.
Mint targets Folia-compatible scheduling. Do not use Bukkit.getScheduler() for game-state or entity work.
Use ir.buddy.mint.util.FoliaScheduler:
| Situation | API |
|---|---|
| Work tied to a block/chunk region | runRegion, runRegionLater, runRegionAtFixedRate |
| Work tied to an entity | runEntity, runEntityLater, runEntityAtFixedRate |
| Global server tick work | runGlobal, runGlobalLater, runGlobalAtFixedRate |
| Off-thread prep (no world/entity access) | runAsync + then hop back with region/entity/global as appropriate |
If you repeat work every tick, store a ScheduledTaskHandle (or equivalent) and cancel in disable().
- Implement
Listener, register inenable()withplugin.getServer().getPluginManager().registerEvents(this, plugin). - In
disable(), callHandlerList.unregisterAll(this). - Prefer
@EventHandler(priority = ..., ignoreCancelled = true)when you should not override vanilla after another plugin cancels. - Respect
EquipmentSlot.HAND(and off-hand rules) for interact events when relevant, to avoid double firing.
- Prefer
plugin.getConfig()and paths under yourgetConfigPath()(e.g.getConfigPath() + ".some-option"). - After
/mint reload, changed keys under your module are handled byModuleManager.reloadChangedModules; avoid caching config values in static fields without reload hooks.
If the module registers Bukkit recipes, follow the CarpetGeometryModule pattern: expose a method called from ModuleManager.registerRecipes() during onEnable before modules are enabled, so recipe registration order stays consistent.
- Match existing code: imports, brace style, minimal comments (explain non-obvious invariants only).
- No new mandatory dependencies in
pom.xmlwithout discussion; prefer Paper / Bukkit APIs. - Avoid long-lived static mutable state tied to a world; use instance fields on the module or per-player maps keyed by
UUIDwith cleanup on quit if needed.
- New class under
module.impl, implementsModule(+Listenerif needed). -
ModuleManager.registerModules()updated. -
config.ymlsection undermodules.<key>withenabledand documented options. -
gui.ymlmodule-iconsentry for<key>(unless intentionally defaulting). -
ModuleAccess.isEnabledForPlayer(andcanBuildif mutating the world) on all relevant paths. - No raw
Bukkit.getScheduler()for gameplay; useFoliaScheduler. -
disable()cleans listeners and repeating tasks. -
mvn -q packagesucceeds.