Skip to main content

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.

MethodMeaning
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.

MethodMeaning
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.

MethodMeaning
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_ROUTEThe constant "config-version".

What an update does

  1. If the bundled resource is missing, logs Missing resource '<name>' and stops. update then returns whatever is on disk, which may be an empty config. document returns null.
  2. If the server has no copy yet, copies the bundled resource into the data folder.
  3. 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.
  4. update then 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");
MethodMeaning
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

ValidatorPasses when the value is
isValidBooleanA YAML boolean: true, false, yes, no, on, off, y, n, in any of their usual casings.
isValidIntAn integer.
isValidIntPosAn integer of zero or more.
isValidIntNegAn integer below zero.
isValidDoubleA decimal.
isValidPercentA decimal from 0.0 to 1.0.
isValidPitchA decimal from -90.0 to 90.0.
isValidYawA decimal from -180.0 to 180.0.
isValidHotbarSlotAn integer from 0 to 8.
isValidStringA string.
isValidListA list.
isValidSectionA configuration section.
isValidWorldThe name of a world that is currently loaded.
isValidPlayerStringA 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.