Skip to content

v5 Config System

Jake Moore edited this page Aug 30, 2026 · 3 revisions

Configuration System

⚠️ Usage ⚠️

The standalone half lives in standalone-utils; the Spigot half (KamiConfig) lives in spigot-utils. Both sit on the same abstractions, so using one is much like using the other.

KamiCommon's configs are YAML (snakeyaml, YAML 1.1) and aim to be a drop-in replacement for Bukkit's YamlConfiguration: the same getString / getInt / getStringList / getConfigurationSection surface, plus getItemStack and friends on the Spigot side.

The thing Bukkit does not do is preserve comments. Comments in your bundled resource file are copied into the server-side config and kept across reloads, and comments an admin writes against keys of their own survive too.

Creating a config

KamiPlugin creates config.yml for you; reach it with getKamiConfig(). For anything else:

KamiConfig lang = new KamiConfig(this, new File(getDataFolder(), "lang.yml"));

That form loads defaults from the jar resource with the same name. To supply them from somewhere else, or to disable them:

// defaults from a differently-named resource
new KamiConfig(this, file, () -> getResource("defaults/lang.yml"));

// no defaults at all
new KamiConfig(this, file, null);

KamiConfigExt is the same thing with extra helpers, and takes the same constructors.

⚠️ Migrating from v4: v4 had two three-argument forms, one taking a boolean addDefaults and one already taking a Supplier<InputStream>. If you passed a supplier, nothing changes. The boolean is now a @Nullable Supplier<InputStream>, and "add defaults" now means "the supplier is not null". new KamiConfig(plugin, file, false) becomes new KamiConfig(plugin, file, null), and new KamiConfig(plugin, file, true) becomes new KamiConfig(plugin, file).

Standalone

StandaloneConfig config = new StandaloneConfig(logger, new File("config.yml"));

A LoggerService is required. Create one if you have none.

⚠️ The logger you pass decides which jar your defaults come from. Bundled defaults are resolved through logger.getClass().getResourceAsStream(...), so a LoggerService from a different jar reads that jar's resources, or none. Pass a logger from the jar that carries your default config.

Saving and reloading

config.save();     // writes current data to the source
config.reload();   // re-reads, discarding in-memory changes, and notifies observers

save() buffers the whole document and only marks the config clean if the write succeeds, so a failed write no longer leaves a dirty config looking saved.

Config sources

A config no longer has to be a file. ConfigSource is the abstraction:

implementation writable use
FileConfigSource yes the normal case, a file on disk
StringConfigSource no a YAML document held in memory
ConfigSource source = new StringConfigSource("greeting: hello", "in-memory");
StandaloneConfig config = new StandaloneConfig(logger, source, null);

save() on a read-only source is a no-op returning false rather than an exception. A source with no content starts from an empty document rather than failing.

Reacting to reloads

Implement ConfigObserver and register it against anything that is an ObservableConfig, which includes KamiConfig, KamiConfigExt, KamiPlugin, AbstractSubsystem and StandaloneConfig.

config.registerConfigObserver(cfg -> {
    // called immediately on registration, and on every reload afterwards
});

CachedConfig is the framework version of that pattern: subclass it, put your parsing in loadCache(T config), and call register(). Your cached fields are then refreshed on every reload.

public class Lang extends CachedConfig<KamiConfig> {
    public static String prefix;

    public Lang(KamiConfig config) { super(config); }

    @Override
    public void loadCache(KamiConfig config) {
        prefix = config.getString("prefix");
    }
}

⚠️ Migrating from v4: registerObserver() is now register(), and loadConfig(T) is now loadCache(T). ICachedConfig and CachedModuleConfig were removed; CachedConfig is an abstract class implementing ConfigObserver.

Comment behaviour

defaultCommentsOverwrite on AbstractConfig defaults to true, meaning a comment already in the server-side file wins over the one in your bundled resource. Set it to false and the bundled comment takes priority instead.

The flag is read while defaults are applied, which happens inside the constructor, so setting it on a config you already built only affects later reload() calls.

Sequences

Support for YAML sequences is early. Saving is unaffected, but reading a sequence whose entries are mappings through get, getString and friends throws IllegalStateException. Use getConfigurationSequence for those. Sequences of plain scalars read normally.

Because defaults are applied in the constructor, building a config with defaults enabled over a file containing a sequence of mappings throws out of the constructor.

Migrating a custom YAML handler

If you subclassed AbstractYamlHandler or YamlHandlerStandalone, several things moved. The two that break quietly:

  • AbstractYamlHandler gained an abstract warn(String), so every existing subclass stops compiling until it implements it.
  • AbstractConfig#getFile() became getSource(), returning a ConfigSource, and MemorySectionMethods#save(File) became save(ConfigSource). Neither is on the handler itself; what breaks a handler subclass is newConfig's second parameter changing type, and the constructor arity.

loadConfig(boolean, Supplier) is now loadConfig() with no arguments, the defaults supplier having moved to the constructor, and getIS() is gone. loadConfig also no longer calls System.exit(0) when it cannot create the file.

Clone this wiki locally