Configuration
Packages: dev.anchorlight.stonelib.config, dev.anchorlight.stonelib
The usual approach to a plugin config, "copy the bundled file if it is missing", has a quiet flaw: it never updates an existing file. When a plugin update adds a key, every server that already had the file silently reads the default from code instead of a value it can see and edit. And once a key is renamed, every existing server keeps the old name forever, so the plugin has to read both.
StoneLib's config classes fix both. Every load brings the server's file up to date with the copy bundled in the jar, and a file that declares a version can have keys renamed or removed.
ConfigManager
For the common case: one config.yml.
| Method | Meaning |
|---|---|
new ConfigManager(JavaPlugin) | Updates config.yml against the bundled resource, then loads it. |
reload() | Updates again and re-reads it. |
getConfig() | The plugin's FileConfiguration, the same object as plugin.getConfig(). |
ConfigManager config = new ConfigManager(this);
int radius = config.getConfig().getInt("scatter.radius", 4);
MultiConfigManager
For plugins with a lot of tunable surface, split across several files so a balance pass does not mean scrolling through one enormous config.
| Method | Meaning |
|---|---|
new MultiConfigManager(JavaPlugin, List<String> fileNames) | Updates and loads every file. Each must exist as a bundled resource. |
reload() | Updates and re-reads every file. |
get(String fileName) | The loaded config for that file. Throws IllegalArgumentException for a name this manager was not built with. |
fileNames() | The managed names, in declaration order. |
MultiConfigManager configs = new MultiConfigManager(this,
List.of("config.yml", "loot.yml", "arenas.yml"));
int size = configs.get("arenas.yml").getInt("default_size");
File names may include a folder, such as "arenas/overworld.yml". The folder is created in the data folder as needed.
ConfigUpdater
Both managers, and MessageService, use ConfigUpdater underneath. Call it directly when you need relocations or a file outside those classes.
| Method | Meaning |
|---|---|
update(JavaPlugin, String fileName) | Brings the file up to date and returns it as a Bukkit FileConfiguration. |
update(JavaPlugin, String fileName, UpdaterSettings) | As above, with your own BoostedYAML updater settings, for declaring relocations. |
document(JavaPlugin, String fileName) | The updated file as a BoostedYAML YamlDocument, for writing values back with comments preserved. |
document(JavaPlugin, String fileName, UpdaterSettings) | As above, with your own settings. |
resourceDeclaresVersion(JavaPlugin, String fileName) | Whether the bundled copy has a config-version value. |
VERSION_ROUTE | The constant "config-version". |
What an update does
- If the bundled resource is missing, logs
Missing resource '<name>'and stops.updatethen returns whatever is on disk, which may be an empty config.documentreturnsnull. - If the server has no copy yet, copies the bundled resource into the data folder.
- Loads the server's copy with the bundled copy as defaults, merges in any missing keys, applies relocations if the file is versioned, and saves it back.
updatethen loads the saved file as a normal Bukkit config.
The merge runs through BoostedYAML, so comments and key order in the server's file are preserved. Values the server has set are never overwritten.
If anything fails, the error is logged as Could not update '<name>'; the server copy is left as it is, and update still loads what is on disk, so the plugin starts on a stale config rather than not at all.
Versioned migration
Put a config-version key at the top of the bundled resource, and increase it whenever you restructure the file:
# src/main/resources/config.yml
config-version: 2
scatter:
radius: 4
With no relocations to declare, nothing else is needed: update(plugin, "config.yml") detects the version key and turns versioning on.
When a key moves, declare the relocation for the version that moved it:
// "teleport.radius" became "scatter.radius" in version 2
ConfigUpdater.update(this, "config.yml", UpdaterSettings.builder()
.setVersioning(new BasicVersioning(ConfigUpdater.VERSION_ROUTE))
.addRelocation("2", Route.fromString("teleport.radius"), Route.fromString("scatter.radius"))
.build());
A server on version 1 has its value moved to the new key, keeping what it set. A server already on version 2 is untouched.
ConfigManager and MultiConfigManager always use the automatic settings, so they pick up versioning but cannot declare relocations. For a file that needs relocations, call ConfigUpdater.update with your settings yourself, then read the config through Bukkit as usual.
:::note Adopting versioning is per file
BoostedYAML throws if versioning is on and the bundled defaults carry no version, so a resource without config-version falls back to a plain merge of missing keys. Add the key to a file when you are ready, and only that file starts being versioned.
:::
Writing values back
The FileConfiguration that update returns is loaded through Bukkit, and saving it through Bukkit strips comments. To change a value and keep the file's comments, take the document instead:
YamlDocument document = ConfigUpdater.document(this, "config.yml");
if (document != null) {
document.set("scatter.radius", 6);
document.save();
}
Shading BoostedYAML
BoostedYAML arrives transitively with StoneLib. Relocate it in your shade configuration so two plugins bundling different versions cannot collide. See Installation.
ConfigValidator
Class: dev.anchorlight.stonelib.ConfigValidator
Checks individual fields and swaps in a fallback when one is wrong, logging why.
FileConfiguration cfg = config.getConfig();
ConfigValidator.validateConfig(this, cfg, "scatter.radius", ConfigValidator.isValidIntPos, 4);
ConfigValidator.validateConfig(this, cfg, "spawn.world", ConfigValidator.isValidWorld, "world");
| Method | Meaning |
|---|---|
validateConfig(plugin, config, field, Validator, Object fallback) | Runs the validator. On failure, sets field to fallback in memory and logs two warnings: which field was replaced, and the reason. Returns the ValidationResult. |
validateConfig(plugin, config, field, ValidatorWithSetter<T>, Object fallback, Consumer<T> setter) | As above, for validators that also hand back a parsed value through setter. |
The fallback is only set on the in-memory config. The file on disk is not changed.
Built-in validators
| Validator | Passes when the value is |
|---|---|
isValidBoolean | A YAML boolean: true, false, yes, no, on, off, y, n, in any of their usual casings. |
isValidInt | An integer. |
isValidIntPos | An integer of zero or more. |
isValidIntNeg | An integer below zero. |
isValidDouble | A decimal. |
isValidPercent | A decimal from 0.0 to 1.0. |
isValidPitch | A decimal from -90.0 to 90.0. |
isValidYaw | A decimal from -180.0 to 180.0. |
isValidHotbarSlot | An integer from 0 to 8. |
isValidString | A string. |
isValidList | A list. |
isValidSection | A configuration section. |
isValidWorld | The name of a world that is currently loaded. |
isValidPlayerString | A legacy &-coded string containing exactly one %p% placeholder. Hands the parsed TextComponent to the setter. |
:::caution Decimal validators reject whole numbers
YAML reads 1 as an integer and 1.0 as a decimal, and the decimal validators check the type. So isValidPercent rejects 1 and accepts 1.0. Write decimal settings with a decimal point in your bundled config, and say so in its comments.
:::
isValidWorld only sees worlds that are loaded at the moment it runs, so validate world names after worlds load, not during an early onLoad.
Custom validators
A Validator is a function of the config and the field name that returns a ValidationResult:
ConfigValidator.Validator isPort = (cfg, field) -> {
if (!cfg.isInt(field)) {
return ConfigValidator.ValidationResult.failure("'" + field + "' wasn't an integer");
}
int port = cfg.getInt(field);
return port > 0 && port <= 65535
? ConfigValidator.ValidationResult.success()
: ConfigValidator.ValidationResult.failure("'" + field + "' wasn't a valid port");
};
CopyResources (deprecated)
CopyResources.mirror(plugin, filepath) is the older way to copy a resource into the data folder. It now simply calls ConfigUpdater.update, so existing callers keep working and gain versioning as soon as their resource declares config-version. Use ConfigUpdater in new code.