-
Notifications
You must be signed in to change notification settings - Fork 1
Configuration
PowerLib gives every platform the same configuration API. You write your config code once against ConfigManager and Configuration, and it behaves identically on Bukkit/Spigot/Paper, BungeeCord and Velocity — no FileConfiguration on one side and ConfigurationNode on the other.
Two things make it worth using over each platform's native config layer:
- Auto-backfill on update. When you ship a new plugin version that adds keys to your bundled default config, those keys are merged into every user's existing file automatically — without touching the values they already changed. No "delete your config to get the new options" notes in your changelog.
-
YAML and JSON behind one interface. The same
Configurationobject is produced and consumed whether it is backed by a.ymlor a.jsonfile.
If you have not set the library up yet, start with Getting Started; everything below assumes PowerLib is on the classpath.
| Type | Role |
|---|---|
ConfigManager |
Per-plugin manager. Creates files from packaged defaults, caches them, saves and reloads them. The base class is platform-agnostic; you instantiate a platform subclass. |
BukkitConfigManager / BungeeConfigManager / VelocityConfigManager
|
~15-line subclasses that extract your plugin's data folder and jar so the base class can do its work. |
Configuration |
The config object itself — a nested key/value tree with typed accessors (getString, getInt, getList, getSection, set, …) addressed by dotted paths. |
ConfigMigration |
The backfill engine. backfill(target, defaults) recursively copies missing default keys into a user file, preserving existing values. Called for you by create(...). |
ConfigurationProvider / YamlConfiguration / JsonConfiguration
|
The serialization layer. YamlConfiguration (SnakeYAML) and JsonConfiguration (Gson) read/write a Configuration to/from disk. ConfigManager uses YAML; you can use the providers directly for JSON. |
The packages are:
it.mycraft.powerlib.common.configuration.* // ConfigManager, Configuration, ConfigMigration, providers
it.mycraft.powerlib.bukkit.config.BukkitConfigManager
it.mycraft.powerlib.bungee.config.BungeeConfigManager
it.mycraft.powerlib.velocity.config.VelocityConfigManager
You never construct the base ConfigManager directly (its constructor is protected). Instead you construct the subclass for your platform — each one knows how to find your data folder and jar:
| Platform | Constructor | Source argument |
|---|---|---|
| Bukkit / Spigot / Paper | new BukkitConfigManager(JavaPlugin plugin) |
your JavaPlugin
|
| BungeeCord | new BungeeConfigManager(Plugin plugin) |
your Bungee Plugin
|
| Velocity | new VelocityConfigManager(PluginDescription description) |
the plugin's PluginDescription
|
Construct the manager once per plugin and keep it in a field. From there, create(String fileName):
- copies the packaged default out of your jar into the data folder if the file does not exist yet,
- loads it into a
Configuration, - backfills any keys present in the packaged default but missing from the user's file (and saves if anything was added),
- caches it and returns it.
fileName is both the path inside the data folder and the resource name read from the jar root. So create("config.yml") reads config.yml from your jar and writes plugins/<YourPlugin>/config.yml.
package com.example.myplugin;
import it.mycraft.powerlib.bukkit.config.BukkitConfigManager;
import it.mycraft.powerlib.common.configuration.ConfigManager;
import it.mycraft.powerlib.common.configuration.Configuration;
import org.bukkit.plugin.java.JavaPlugin;
public final class MyPlugin extends JavaPlugin {
private ConfigManager configs;
@Override
public void onEnable() {
// One manager for the whole plugin.
this.configs = new BukkitConfigManager(this);
// Copies config.yml from the jar on first run, backfills new keys on update, then loads it.
Configuration config = configs.create("config.yml");
getLogger().info("Loaded prefix: " + config.getString("prefix"));
}
public Configuration getMainConfig() {
return configs.get("config.yml"); // already cached — no disk hit
}
}BungeeConfigManager extracts the data folder and jar straight from the Bungee Plugin:
package com.example.myplugin;
import it.mycraft.powerlib.bungee.config.BungeeConfigManager;
import it.mycraft.powerlib.common.configuration.ConfigManager;
import it.mycraft.powerlib.common.configuration.Configuration;
import net.md_5.bungee.api.plugin.Plugin;
public final class MyPlugin extends Plugin {
private ConfigManager configs;
@Override
public void onEnable() {
this.configs = new BungeeConfigManager(this);
Configuration config = configs.create("config.yml");
getLogger().info("Servers configured: " + config.getStringList("servers"));
}
}Velocity plugins have no data folder on the plugin object, so the manager is built from the PluginDescription. The proxy injects everything you need into your plugin constructor; create the manager once ProxyInitializeEvent fires:
package com.example.myplugin;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.proxy.ProxyServer;
import it.mycraft.powerlib.common.configuration.ConfigManager;
import it.mycraft.powerlib.common.configuration.Configuration;
import it.mycraft.powerlib.velocity.config.VelocityConfigManager;
@Plugin(id = "myplugin", name = "MyPlugin", version = "1.0.0")
public final class MyPlugin {
private final PluginManager pluginManager;
private ConfigManager configs;
@Inject
public MyPlugin(ProxyServer proxy) {
this.pluginManager = proxy.getPluginManager();
}
@Subscribe
public void onInit(ProxyInitializeEvent event) {
PluginContainer self = pluginManager.fromInstance(this).orElseThrow();
// VelocityConfigManager resolves the data folder under <velocity>/plugins/<id>/ from the description.
this.configs = new VelocityConfigManager(self.getDescription());
Configuration config = configs.create("config.yml");
}
}Loading a config whose name differs from its resource. Use the two-arg
create(String file, String source)when the on-disk name should differ from the packaged resource — e.g.configs.create("messages_en.yml", "messages.yml")writesmessages_en.ymlbut seeds and backfills it from themessages.ymlbundled in your jar.create(file)is shorthand forcreate(file, file).
Configuration addresses values by dotted path — "database.host" walks into the database section and reads host. Every typed getter has a no-default form and a (path, default) form:
Configuration config = configs.get("config.yml");
String prefix = config.getString("prefix"); // "" if unset
String host = config.getString("database.host", "127.0.0.1");
int port = config.getInt("database.port", 3306);
boolean debug = config.getBoolean("debug"); // false if unset
double timeout = config.getDouble("network.timeout", 5.0);
List<String> worlds = config.getStringList("enabled-worlds"); // empty list if unset
List<Integer> tiers = config.getIntList("reward-tiers");
boolean hasKey = config.contains("database.password");The no-default getters fall back to the packaged default when the file was loaded with one, and otherwise to a type-appropriate zero value: getString → "", getInt/getDouble/… → 0, getBoolean → false, getChar → '�', getList → empty list. Type mismatches degrade gracefully — asking for getInt on a string returns the default rather than throwing.
A nested block is itself a Configuration. Use getSection(path) to descend, and getKeys() to list the immediate child keys of a section (not deep):
# config.yml
kits:
starter:
cost: 0
items: [WOODEN_SWORD, BREAD]
vip:
cost: 500
items: [DIAMOND_SWORD, GOLDEN_APPLE]Configuration kits = config.getSection("kits");
for (String kitName : kits.getKeys()) { // -> "starter", "vip"
Configuration kit = kits.getSection(kitName);
int cost = kit.getInt("cost");
List<String> items = kit.getStringList("items");
getLogger().info(kitName + " costs " + cost + " and grants " + items);
}| Method | Returns |
|---|---|
Object get(String path) |
Raw value at the path, or the default config's value, else null. |
<T> T get(String path, T def) |
Raw value cast to T, or def when unset. |
boolean contains(String path) |
true if a non-null value is set at the path. |
Object getDefault(String path) |
The value at this path in the backing default config, or null. |
Configuration getSection(String path) |
The sub-section at the path (an empty one is created if absent). |
Collection<String> getKeys() |
Immediate (top-level) keys of this section. |
String getString(String path) / getString(path, def)
|
Value as a string ("" / def fallback). |
int getInt(String path) / getInt(path, def)
|
Value as an int (also getByte, getShort, getLong, getFloat, getDouble). |
boolean getBoolean(String path) / getBoolean(path, def)
|
Value as a boolean. |
char getChar(String path) / getChar(path, def)
|
Value as a char. |
List<?> getList(String path) / getList(path, def)
|
Raw list at the path. |
getStringList / getIntList / getBooleanList / getDoubleList / getLongList / getByteList / getShortList / getFloatList / getCharList
|
The list at the path filtered to that element type. |
set(String path, Object value) updates the in-memory tree, creating intermediate sections as needed. Passing null removes the key. Changes are not persisted until you call save(fileName) on the manager:
Configuration config = configs.get("config.yml");
config.set("database.host", "db.internal");
config.set("database.port", 3306);
config.set("maintenance", true);
config.set("legacy-option", null); // remove a key
configs.save("config.yml"); // write the cached config back to disk (YAML)save serializes the cached Configuration for that file name back to the data folder and re-caches it. It throws IllegalArgumentException if you ask it to save a file name that was never loaded.
If a file was edited out-of-band (an admin changed it, or another tool wrote to it), re-read it:
configs.reload("config.yml"); // re-load this one file from disk
Configuration fresh = configs.get("config.yml");
configs.reloadAll(); // re-load every file this manager has cachedHold the file name, re-fetch the object.
reload/saveswap the cachedConfigurationinstance for a file. After a reload, aConfigurationreference you grabbed earlier is stale — callconfigs.get("config.yml")again rather than caching the object across reloads.
| Method | Description |
|---|---|
Configuration create(String file) |
Copy from jar if missing, load, backfill from the packaged default, cache, return. |
Configuration create(String file, String source) |
As above, but seed/backfill from a differently-named jar resource. |
Configuration get(String file) |
The cached Configuration for file, or null if not loaded. |
void save(String file) |
Serialize the cached config back to disk as YAML and re-cache it. |
void reload(String file) |
Re-load a single file from disk into the cache. |
void reloadAll() |
Re-load every cached file from disk. |
This is the part most config layers leave to you. When you release a plugin update that adds new options to your bundled default config, existing users keep their old file — which is now missing your new keys. The usual workarounds are ugly: ship the new default and overwrite the user's edits, or ask users to regenerate the file.
PowerLib does neither. Every create(...) runs ConfigMigration.backfill(userConfig, packagedDefault), which recursively walks the packaged default and copies into the user's file only the keys they are missing — never overwriting a value they already set. If anything was added, the file is saved back to disk immediately. Nested sections are merged key-by-key, not replaced wholesale.
Say version 1.0 of your plugin shipped this default, and a user customized it:
# plugins/MyPlugin/config.yml (the user's file, from v1.0 — they changed the prefix)
prefix: "&a[MyServer] "Version 1.1 of your jar bundles a new default that adds a database section and a debug flag:
# config.yml inside MyPlugin-1.1.jar
prefix: "&7[MyPlugin] "
debug: false
database:
host: "127.0.0.1"
port: 3306On first start with 1.1, your code runs exactly as before:
Configuration config = configs.create("config.yml");PowerLib backfills the missing keys and rewrites the file. The user's prefix is untouched; the new keys arrive with their default values:
# plugins/MyPlugin/config.yml (after the 1.1 update — saved automatically)
prefix: "&a[MyServer] "
debug: false
database:
host: "127.0.0.1"
port: 3306No code change between versions, no migration script, no lost edits.
For each key in the packaged default:
- Key is a section → if the user has a section there too, recurse into it (merge child-by-child); otherwise copy the whole section in.
-
Key is a scalar/list → if the user does not already have that key (
!contains(key)), copy the default value; otherwise leave the user's value alone.
public static boolean backfill(Configuration target, Configuration defaults) {
if (target == null || defaults == null) {
return false;
}
boolean changed = false;
for (String key : defaults.getKeys()) {
Object defaultValue = defaults.get(key);
if (defaultValue instanceof Configuration) {
Object targetValue = target.get(key);
if (targetValue instanceof Configuration) {
changed |= backfill((Configuration) targetValue, (Configuration) defaultValue);
} else {
target.set(key, defaultValue);
changed = true;
}
} else if (!target.contains(key)) {
target.set(key, defaultValue);
changed = true;
}
}
return changed;
}The method returns true if it added at least one key — that is the signal ConfigManager uses to decide whether to re-save the file. Because backfill only ever adds keys, it is safe to run on every startup; a config already up to date is left byte-for-byte unchanged on disk.
Scope. Backfill adds missing keys; it does not rename, move, or delete keys, and it does not change a value the user has set (even if your default for it changed). For a key whose meaning changed between versions, handle it explicitly with
set/getaftercreatereturns.
You rarely need to, since create already does — but the engine is public if you load configs yourself (e.g. when working with JSON via the providers):
import it.mycraft.powerlib.common.configuration.ConfigMigration;
import it.mycraft.powerlib.common.configuration.Configuration;
import it.mycraft.powerlib.common.configuration.ConfigurationProvider;
import it.mycraft.powerlib.common.configuration.YamlConfiguration;
Configuration user = ConfigurationProvider.getProvider(YamlConfiguration.class).load(userFile);
Configuration defaults = ConfigurationProvider.getProvider(YamlConfiguration.class).load(getResourceAsStream("config.yml"));
if (ConfigMigration.backfill(user, defaults)) {
ConfigurationProvider.getProvider(YamlConfiguration.class).save(user, userFile);
}ConfigManager always reads and writes YAML — that is the right default for human-edited Minecraft configs. But the underlying serialization is pluggable through ConfigurationProvider, and PowerLib ships two implementations:
| Provider | Backing library | Pick for |
|---|---|---|
YamlConfiguration |
SnakeYAML | Human-edited config files. Comments-friendly format, block style, what server owners expect. This is what ConfigManager uses. |
JsonConfiguration |
Gson | Machine-written data — caches, state dumps, anything consumed by another program. Pretty-printed, strict structure, no YAML ambiguity. |
Both produce and consume the same Configuration type, so your reading/writing code does not change between them — only the load/save call does.
You obtain a provider via ConfigurationProvider.getProvider(...) and call load / save directly. (The providers are registered only when their library is on the classpath: SnakeYAML ships with every platform module; Gson is present on the supported platforms. getProvider returns null if the backing library is absent.)
import it.mycraft.powerlib.common.configuration.Configuration;
import it.mycraft.powerlib.common.configuration.ConfigurationProvider;
import it.mycraft.powerlib.common.configuration.JsonConfiguration;
import java.io.File;
import java.io.IOException;
ConfigurationProvider json = ConfigurationProvider.getProvider(JsonConfiguration.class);
File stateFile = new File(getDataFolder(), "state.json");
// Write some state as JSON
Configuration state = new Configuration();
state.set("lastRun", System.currentTimeMillis());
state.set("processed", 42);
try {
json.save(state, stateFile);
} catch (IOException ex) {
getLogger().warning("Could not write state.json: " + ex.getMessage());
}
// Read it back
try {
Configuration loaded = json.load(stateFile);
long lastRun = loaded.getLong("lastRun");
} catch (IOException ex) {
getLogger().warning("Could not read state.json: " + ex.getMessage());
}Both providers expose the full load/save surface — from a File, Reader, InputStream or raw String, each with an optional defaults overload so the loaded config falls back to a defaults tree for unset paths:
| Method | Notes |
|---|---|
load(File) / load(File, Configuration defaults)
|
IOException on read failure. |
load(Reader) / load(Reader, Configuration)
|
|
load(InputStream) / load(InputStream, Configuration)
|
Handy for reading a packaged default straight from the jar. |
load(String) / load(String, Configuration)
|
Parse from an in-memory string. |
save(Configuration, File) |
IOException on write failure. Writes UTF-8. |
save(Configuration, Writer) |
- Getting Started — adding the dependency and bootstrapping PowerLib.
-
Messages —
Messagereads its colour/placeholder strings straight from aConfiguration.
Core (all platforms)
Bukkit / Paper
Commands