DUXPLIMA Documentation

Configuration

HOCON config with atomic reloads, live properties, migrations, validation, lenient lists and typed record snapshots.

Configuration over Configurate and HOCON, in two shapes: query individual nodes with HoconConfig, or map a whole file onto one record with RecordConfig. Both reload atomically.

HoconConfig

HoconConfig config = HoconConfig.load(dataFolder.resolve("config.conf"));

int limit = config.getInt("homes.limit", 3);
String name = config.getString("server.name", "Server");
boolean flag = config.getBoolean("features.pvp", true);

A missing file loads as an empty tree, so every getter falls back to the default you pass and the first run does not crash.

The parsed tree lives behind an atomic reference swapped whole on reload, so a reader always sees the entire old tree or the entire new one. Never half.

Shipping defaults

HoconConfig config = HoconConfig.loadOrExtract(
        dataFolder.resolve("config.conf"), "config.conf", getClassLoader());

config.mergeDefaults("config.conf", getClassLoader());   // add keys a new release introduced
config.commentIfAbsent("homes.limit", "How many homes a player may set");
config.save();

loadOrExtract writes the bundled resource out on first run. mergeDefaults adds keys the user's file does not have yet without touching the ones they edited, which is what an upgrade needs.

Typed reads

List<String> worlds = config.getList("worlds", String.class);
Map<String, Warp> warps = config.getSection("warps", Warp.class);
Settings settings = config.getNode("settings", Settings.class, Settings.DEFAULT);
Optional<Root> whole = config.get(Root.class);

ConfigCodecs.bukkit() adds scalar serializers for Material, NamespacedKey and Color, so those map straight from a string:

HoconConfig config = HoconConfig.load(path, ConfigCodecs.bukkit());

Lenient reads

One malformed entry in a list of fifty should not lose the other forty-nine.

LenientResult<List<Warp>> result = config.getListLenient("warps", Warp.class);

result.value();        // everything that parsed
result.skipped();      // a ConfigViolation per entry that did not
result.allParsed();    // whether anything was skipped

This is the right default for operator-edited collections. Log the skips and carry on with what worked, rather than refusing to load.

Live properties

ConfigProperty<Integer> limit = config.intProperty("homes.limit", 3);
limit.onChange(value -> rebuildLimits(value));

config.reload();       // fires the listener only if the value actually changed

A property re-reads itself on reload and fires its listeners only when the value moved, so nothing has to poll the file to find out something changed. There are intProperty, boolProperty and stringProperty variants.

config.onReload(runnable) is the coarser hook for work that is not tied to one key.

Watching the file

config.watch(scheduler, Duration.ofSeconds(5));
config.unwatch();

Polls the file on a period and reloads when it changed, so an operator editing the file gets it applied without a command.

Migrations

ConfigMigration migration = ConfigMigration.builder()
        .version(2, step -> step.rename("homes.max", "limit"))
        .version(3, step -> step.remove("legacy.section"))
        .build();

int applied = config.migrate(migration);

Each version's step runs once, in order, and the file records where it got to. rename and remove are the two operations.

Validation

ValidationResult result = new ConfigRules()
        .require("database.url")
        .range("homes.limit", 1, 100)
        .matches("server.id", "[a-z0-9-]+")
        .oneOf("storage.type", "sqlite", "mysql", "postgres")
        .validate(config);

result.ok();
result.violations();          // a ConfigViolation per failure: path and message
result.throwIfInvalid();      // or fail loudly on enable

Validating on enable turns a config typo into one clear line at startup instead of a NumberFormatException an hour later.

Interpolation

config.interpolate(Map.of("server", "lobby-1"));
String motd = config.getInterpolated("motd", "Welcome", Map.of("server", "lobby-1"));

Substitutes variables into string values, so one file can be shared across nodes with the node name injected.

Composition

config.include(otherConfig);
config.include(dataFolder.resolve("extra.conf"));

Merges another tree in, for splitting a large config across files.

Saving

config.save();
config.saveAsync(executor);

Writes are atomic: the file is written beside the target and moved into place, so a crash mid-write cannot leave a half-written config behind.

Raw access

config.nodeAt("menus", "shop");     // a ConfigurationNode
config.root();                       // the whole commented tree
config.keys("warps");                // the child keys under a path

nodeAt is what you hand to MenuConfig.load or any other node-consuming API.

RecordConfig

When the whole file is one shape, map it onto one @ConfigSerializable record.

RecordConfig<Settings> settings = new RecordConfig<>(
        dataFolder.resolve("settings.conf"), Settings.class, Settings::new);

Settings current = settings.current();     // a cached field read, cheap on a hot path
settings.reload();
settings.dryRun();                          // parse without swapping, to validate a pending edit
settings.isModifiedSinceLoad();             // has the file changed on disk

Reload is fail-safe: on a parse error it throws and keeps the prior snapshot, so current() always returns a valid value. A bad edit does not take the server down with it.

RecordConfig.loadFrom(file, type, default) is the one-shot static when you do not need the reloadable wrapper.

Literal or placeholder

A config field that may be either a fixed value or a placeholder template resolved later.

ValueOrPlaceholder<String> owner = ValueOrPlaceholder.placeholder("%player_name%");

String resolved = owner.resolve(template -> Placeholders.apply(player, template));

ValueOrPlaceholder.literal("Steve") ignores the resolver entirely. A placeholder calls it on every resolve, never caching, so live values stay live. The resolver is a plain function, which is why this type does not depend on any placeholder engine.

Merging user edits

Map<String, Value> merged = MapMerge.userWins(managedDefaults, userEdits);

For the case where you own a set of entries and the operator may override any of them.