To add an entirely new command (e.g. /fly), follow the ExampleCommand pattern:
- Create a class extending
BaseCommand(from cw-commons); build the fullCommandAPICommandtree in the constructor and pass it tosuper(...) - Call
.register()on an instance of it inExamplePlugin.onEnable(), alongside the existingnew ExampleCommand(config, creeperKillsManager, this).register() - Add its permissions to
plugin.yml. Do not add the command itself. CommandAPI registers it programmatically - Write unit tests for each executor class following the
PingTest/GreetTestpattern
To add a subcommand to /example, follow the Ping/Greet pattern:
- Create a class implementing CommandAPI's
CommandExecutor; inject any config values via the constructor - Add a
.withSubcommand(...)call inExampleCommand's constructor - Write a unit test following
PingTestorGreetTest— mockCommandSenderandCommandArguments, callrun(), verifysendRichMessage() - If the subcommand needs a config value, add it to
PluginConfigandconfig.yml
This project provides an example of loading config files using Jackson with Hibernate Validator,
via cw-commons' Config interface and BukkitConfigManagerBuilder
(com.crimsonwarpedcraft.cwcommons.config.bukkit).
To define your own config, add fields annotated with a Bean Validation constraint and a @JsonProperty YAML key to PluginConfig (config/PluginConfig.java):
@NotBlank
@JsonProperty("my-message")
private String myMessage = "default";Then add a getter. The config is validated upfront on every startup. If any constraint fails, the plugin logs the offending fields and disables itself cleanly.
Add a matching entry to src/main/resources/config.yml for every field you add, with a comment if desired:
# Description of what the setting controls.
# Supports MiniMessage formatting: https://docs.advntr.dev/minimessage/format.html
my-message: "default"Comments are preserved because saveDefaultConfig() writes this file once on first startup and never overwrites it. Schema migrations rewrite the file, but those are rare, and require custom code to handle.
cw-commons' BukkitDataStoreBuilder/Repository/PlayerDataManager stores any JSON-shaped data per player, not just counters. To add a new field, follow the PlayerData/CreeperKillListener pattern:
- Add a field with a getter/setter to
PlayerData(data/PlayerData.java) - Reuse the existing
Repository<UUID, PlayerData>/PlayerDataManager<PlayerData>pair built once from theBukkitDataStoreBuilderstore inExamplePlugin.onEnable()— one repository backs every field onPlayerData, so adding a field never requires a new repository - Read and write it with
PlayerDataManager#get/#save, e.g. from aListenerlikeCreeperKillListeneror a command executor likeCreepersKilled - Write unit tests mocking
PlayerDataManager, followingCreeperKillListenerTest/CreepersKilledTest - (Optional) Periodically flush data stores using
AutoFlushTask.builder(...).build().start()to prevent session data loss from an unexpected shutdown.
Note: PlayerDataManager is just a Player-keyed convenience wrapper. For data that isn't tied to a specific player, call DataStore#repository directly with a different KeySerializer (e.g. KeySerializers.forString()) to get a standalone Repository for that data.