Configuration
Fand provides two configuration entry points:
context.config(): the default plugin configuration atconfig.ymlin the plugin data directory.context.configurations(): a generic loader for YAML, JSON, TOML, properties, and other supported files.
Default Configuration
On first access to context.config(), if the data directory does not contain config.yml, the runtime tries to copy the default config.yml from the plugin jar root. If the jar does not contain one either, an empty document is created.
var config = context.config();
var enabled = config.getBoolean("features.enabled", true);
var message = config.getString("messages.welcome", "Welcome");Example src/main/resources/config.yml:
features:
enabled: true
messages:
welcome: "Welcome to Fand"Reading and Writing Values
Configuration paths are dot-separated. Typed getters return their default value on type mismatch instead of throwing.
var host = config.getString("database.host", "localhost");
var port = config.getInt("database.port", 3306);
var debug = config.getBoolean("debug", false);
var worlds = config.getStringList("enabled-worlds");After changing values, call save() on the root Configuration to persist them.
var config = context.config();
config.set("features.enabled", true);
config.set("messages.welcome", "Hello");
config.save();Passing null removes a path:
config.set("temporary.value", null);
config.save();Sections
getSection returns a sub-section. If it does not exist, an empty section is created. Mutating a sub-section changes the root configuration, but the root still needs save().
var database = config.getSection("database");
database.set("host", "127.0.0.1");
database.set("port", 3306);
config.save();Reloading
reloadConfig() reads config.yml from disk again and discards unsaved in-memory changes.
context.reloadConfig();Loading Other Files
ConfigurationService is useful for extra files in the plugin data directory.
var file = context.dataDirectory().resolve("messages.json");
var messages = context.configurations().load(file);You can also specify a format explicitly:
var config = context.configurations().load(file, ConfigurationFormat.JSON);Design Philosophy
Fand separates the default plugin configuration from arbitrary configuration file loading so common plugins stay simple, while larger plugins can split messages.yml, database.json, or other data files when needed.
Typed getters return defaults on type mismatch so each read site declares a fallback strategy. Critical configuration should still be validated by plugin code with clear feedback for server owners.
ConfigurationSection is a view over the same in-memory document, not an independent copy. Mutating a section affects the root document, while persistence is still controlled by root Configuration.save().
Best Practices
- Use lowercase dot-separated keys, such as
database.host. - Give typed getters reasonable defaults to avoid startup failures from user mistakes.
- Call
save()only when persisting user-facing changes. Configurationis not thread-safe; synchronize access yourself if multiple threads touch the same instance.- Put a default
config.ymlat the plugin jar root so first startup generates readable configuration. - Keep keys stable. When renaming keys, include migration or compatibility reads.
- Use typed getters for normal toggles, messages, and numeric thresholds; validate critical values such as database endpoints and external tokens.
- Store editable server-owner settings in configuration; store player data, caches, and runtime statistics elsewhere.
Common Pitfalls
getSection("path")creates an empty section when missing. Usecontains("path")if you only want to check existence.- Mutating a child section without calling
save()on the root loses changes after restart. reloadConfig()discards unsaved in-memory changes.- A typed getter returning the default does not prove the file is valid; critical fields still need validation.
- Concurrent reads and writes to the same
Configurationare not synchronized by the API.
Complete Example: Config-Driven Welcome Message
This example loads the default configuration and sends a configured welcome message on join. The string template is plugin logic; Fand does not prescribe a message format.
package com.example;
import io.fand.api.event.player.PlayerJoinEvent;
import io.fand.api.plugin.Plugin;
import io.fand.api.plugin.PluginContext;
import net.kyori.adventure.text.Component;
public final class ExamplePlugin implements Plugin {
private String welcomeMessage = "Welcome, {player}";
private boolean enabled = true;
@Override
public void onEnable(PluginContext context) {
loadSettings(context);
context.events().subscribe(PlayerJoinEvent.class, event -> {
if (!enabled) {
return;
}
var text = welcomeMessage.replace("{player}", event.player().name());
event.player().sendMessage(Component.text(text));
});
}
private void loadSettings(PluginContext context) {
var config = context.config();
enabled = config.getBoolean("welcome.enabled", true);
welcomeMessage = config.getString("welcome.message", "Welcome, {player}");
if (!config.contains("welcome.enabled")) {
config.set("welcome.enabled", enabled);
config.set("welcome.message", welcomeMessage);
config.save();
}
}
}