Skip to content

These docs describe Bloom 0.1.0, a pre-release. APIs may change before 1.0, and Bloom is not on Maven Central yet (see Installation).

Configuration

YAML files with bundled defaults, typed getters, object mapping, validation, migrations and saves that keep comments.

bloom-config reads and writes YAML with its own SnakeYAML copy, so it behaves the same on every server. It has no Bukkit dependency.

Loading a file

JavaView source
YamlConfig config = YamlConfig.builder(plugin.getDataFolder().toPath().resolve("config.yml"))
        .defaults(() -> plugin.getResource("config.yml")) // bundled in src/main/resources
        .logger(plugin.getLogger())
        .build(); // creates the file from defaults if missing, then loads it

If the file does not exist, Bloom copies your bundled config.yml byte for byte, so your comments and layout reach the server owner unchanged.

Reading values

JavaView source
String prefix = config.getString("messages.prefix"); // required: throws if missing everywhere
int maxHomes = config.getInt("homes.max", 3); // with a fallback
Duration delay = config.getDuration("homes.teleport-delay"); // "3s", "1m30s" or a number of seconds
List<String> worlds = config.getStringList("homes.disabled-worlds");
ConfigSection kits = config.section("kits");
for (String kit : kits.keys()) {
    int cost = kits.getInt(kit + ".cost", 0);
    // ...
    Integer.valueOf(cost).hashCode();
}

A value missing from the file falls back to the bundled default. Getters without a fallback argument throw if the value is missing everywhere. When a section exists in the file, keys() returns only the file's keys, so a kit a server owner deleted does not come back from the defaults.

Mistakes in the file produce messages that point at the line:

Text
config.yml line 7: 'homes.max-homes' expected a whole number but found 'ten'
Invalid YAML in config.yml at line 12, column 3: mapping values are not allowed here.
Check indentation (spaces only, no tabs) and quoting.

Mapping to classes

JavaView source
static final class HomeSettings {
    int maxHomes = 3; // key "max-homes"; the value here is the default when the key is missing
    Duration teleportDelay = Duration.ofSeconds(3);
    List<String> disabledWorlds = new ArrayList<String>();

    @ConfigKey("allow-in-combat")
    boolean combat = false;
}

static HomeSettings homes(YamlConfig config) {
    return config.get("homes", HomeSettings.class);
}

Fields map to kebab-case keys (maxHomes becomes max-homes) unless @ConfigKey says otherwise. Missing keys keep the field's default value. Supported field types: strings, numbers, booleans, characters, enums (case-insensitive), UUID, Duration, List, Set, Map<String, ...>, nested classes with a no-argument constructor, and anything with a registered serializer.

Validation

JavaView source
config.validate()
        .require("storage.type")
        .oneOf("storage.type", "sqlite", "mysql", "mariadb")
        .range("homes.max-homes", 1, 100)
        .type("homes.teleport-delay", Duration.class)
        .check(); // throws ConfigException listing every problem with its line number

All problems are reported at once, each with its line, so a server owner can fix everything in one edit.

Migrations

JavaView source
return YamlConfig.builder(plugin.getDataFolder().toPath().resolve("config.yml"))
        .defaults(() -> plugin.getResource("config.yml"))
        .version(3) // files without "config-version" count as version 1
        .migration(1, m -> m.move("prefix", "messages.prefix")) // 1 -> 2
        .migration(2, m -> m.set("storage.type", m.has("mysql") ? "mysql" : "sqlite")) // 2 -> 3
        .logger(plugin.getLogger())
        .build(); // old files are backed up as config.yml.v1.<timestamp>.bak before migrating

Before migrating, Bloom copies the file to config.yml.v1.<timestamp>.bak. If a migration throws, the file is left untouched. A file with a higher version than your plugin understands is refused rather than downgraded.

Reloading

JavaView source
try {
    config.reload();
    sender.sendMessage("Reloaded.");
} catch (ConfigException e) {
    sender.sendMessage("Reload failed, keeping the previous settings: " + e.getMessage());
}

reload() swaps in the new values only if the whole file loads and migrates successfully.

Saving

JavaView source
config.set("homes.max-homes", 5);
config.set("spawn.world", "world");
config.save(); // atomic write; comments and untouched values keep their formatting

save() edits the file's YAML structure instead of rewriting it from scratch: comments, blank lines and the formatting of untouched values stay as the server owner left them. The write is atomic (a temporary file replaces the original), so a crash never leaves half a file.

New settings in updates

.addMissingDefaults(true) writes settings that exist in your bundled defaults but not in the server's file, with their comments, after backing the file up. It is off by default; values that are only missing are read from the defaults anyway.

Custom types

JavaView source
TypeSerializer<Location> locations = new TypeSerializer<Location>() {
    @Override
    public Location deserialize(Object raw) {
        String[] p = raw.toString().split(",");
        World world = Bukkit.getWorld(p[0]);
        if (world == null) {
            throw new IllegalArgumentException("unknown world '" + p[0] + "'");
        }
        return new Location(
                world, Double.parseDouble(p[1]), Double.parseDouble(p[2]), Double.parseDouble(p[3]));
    }

    @Override
    public Object serialize(Location value) {
        return value.getWorld().getName() + "," + value.getX() + "," + value.getY() + "," + value.getZ();
    }
};
YamlConfig config = YamlConfig.builder(plugin.getDataFolder().toPath().resolve("spawns.yml"))
        .serializer(Location.class, locations)
        .build();
Location spawn = config.get("spawn", Location.class);

Safety

Values are loaded with SnakeYAML's safe constructor (no arbitrary Java objects), duplicate keys are rejected, and alias expansion is limited to block "billion laughs" files.