Configuration binds Kotlin properties to YAML config paths with an annotation —
the same idea as Spring Boot's @Value, but built for Bukkit plugins and, most
importantly, reloadable at runtime. When an admin edits the config file and runs
your /reload command, a single reload() call re-reads the file and updates every
bound property in place.
class Settings(plugin: JavaPlugin) : Configuration(plugin, "settings.yml") {
@Value("prefix", comments = ["Prefix shown before plugin messages"])
var prefix: String by config("&7[MyPlugin] ")
@Value("economy.starting-balance")
var startingBalance: Double by config(100.0)
init { load() } // must come after the property declarations (see §2)
}val settings = Settings(this)
player.sendMessage(settings.prefix + "Balance: " + settings.startingBalance)This is the typed, declarative option. For untyped key/value access (or JSON), see Files & Config (
KFile/Kson).Configurationis best when you have a fixed, known set of settings you want as real, typed fields.
Extend Configuration, choosing where the file lives:
class Settings(plugin: JavaPlugin) : Configuration(plugin, "settings.yml") // -> plugins/MyPlugin/settings.yml
// or: Configuration(filePath, fileName) // a specific directory
// or: Configuration(File(...)) // an exact fileEach setting is a property that combines:
@Value("path", comments = [...])— the YAML path (dot-separated for nesting) and optional comment lines.by config(default)— the delegate, carrying the default value and binding the property's type.
@Value("economy.starting-balance", comments = ["Money new players start with"])
var startingBalance: Double by config(100.0)The path, type, and default all live in one place — no manual getDouble("...", 100.0).
The parent directory and file are created automatically on construction.
You must call load() after the properties are declared — put an init { load() }
block below them, or call settings.load() right after constructing the object.
Why: a subclass's delegate fields don't exist until after the Configuration base
constructor returns, so the base class can't load them for you. load():
- reads each bound path from disk into its property (or writes the default if the key is missing), and
- saves the file back — so first run produces a fully-populated, commented config.
class Settings(plugin: JavaPlugin) : Configuration(plugin, "settings.yml") {
@Value("prefix") var prefix: String by config("&7[MyPlugin] ")
@Value("debug") var debug: Boolean by config(false)
init { load() } // ✅ after the two properties above
}Forgetting load() leaves every property at its compile-time default and never reads
the file.
Bound properties are normal Kotlin properties:
if (settings.debug) logger.info("debug on") // read
settings.prefix = "&a[MyPlugin] " // write (in-memory)
settings.save() // persist all properties to diskReading returns the current in-memory value. Assigning updates it (and the in-memory
config), but isn't written to disk until you call save().
For one-off settings you don't want to declare as a property, getOrSetDefault reads
a path — and if it's absent, writes the given default, persists, and returns it:
val cooldown = settings.getOrSetDefault("limits.cooldown-seconds", 30)The default's type drives coercion (see §6), so the result is correctly typed.
When an operator edits settings.yml by hand and runs your reload command, call
reload() — it re-reads the file from disk and refreshes every bound property:
@CommandAlias("myplugin")
class MyPluginCommand(private val settings: Settings) : MCommand(
Precondition.Builder().hasPermission("myplugin.admin").build()
) {
@Subcommand("reload")
fun reload(sender: CommandSender) {
settings.reload() // re-reads the file into the live object
Chat.tell(sender, "${settings.prefix}&aConfig reloaded!")
}
}After reload(), code holding the same settings instance immediately sees the new
values — no restart, no re-wiring. Keys removed from the file fall back to their
defaults (and are written back), so a partial/edited file can't break you.
Keep a single, shared
Settingsinstance (e.g. a property on your plugin or a Kotlinobject) so areload()is visible everywhere. If different parts of your plugin construct their own copies, only the reloaded one updates.
Comment lines from @Value(..., comments = [...]) are written above their key:
@Value("prefix", comments = ["Prefix shown before plugin messages", "Supports & color codes"])
var prefix: String by config("&7[MyPlugin] ")
@Value("debug", comments = ["Enable verbose logging"])
var debug: Boolean by config(false)produces:
# Prefix shown before plugin messages
# Supports & color codes
prefix: '&7[MyPlugin] '
# Enable verbose logging
debug: falseComments work for nested keys too — the writer tracks the full dotted path by
indentation, so @Value("economy.start-balance", comments = [...]) is placed correctly:
economy:
# Money new players start with
start-balance: 100.0(It assumes the 2-space indentation Bukkit writes; if you hand-edit with a different
indent, the next save()/reload() rewrites the file back to normal form anyway.)
The property type drives how the value is read back from YAML. Use types Bukkit's
YamlConfiguration round-trips cleanly:
| Type | Notes |
|---|---|
String |
|
Int, Long, Double |
the property type must match how the value is stored (don't read an Int value as Double) |
Boolean |
|
List<String> |
and other YAML-native lists |
ConfigurationSerializable |
e.g. Location, or your own (see Selections.md Region) |
On load the stored value is matched against the property's type:
- Numbers are coerced between forms, so an operator writing
start-balance: 100(an int) into aDoublefield works — it loads as100.0. - A genuine mismatch raises a
ConfigurationExceptionwith a clear message instead of a cryptic cast failure, e.g.:Could not load 'economy.max-homes' from 'settings.yml': expected Integer but found String (value: notanumber). Fix the value, or remove the line to regenerate its default.
- A missing (or
null) key falls back to the declared default and is written back, so an operator deleting a line just regenerates it.
import dev.mrshawn.mlib.files.configuration.Configuration
import dev.mrshawn.mlib.files.configuration.annotations.Value
import org.bukkit.plugin.java.JavaPlugin
class Settings(plugin: JavaPlugin) : Configuration(plugin, "settings.yml") {
@Value("prefix", comments = ["Prefix for all plugin messages"])
var prefix: String by config("&7[MyPlugin] ")
@Value("max-homes", comments = ["How many homes a player may set"])
var maxHomes: Int by config(3)
@Value("welcome-message", comments = ["Lines shown on join"])
var welcomeMessage: List<String> by config(listOf("&aWelcome!", "&7Enjoy your stay."))
init { load() }
}
class MyPlugin : JavaPlugin() {
lateinit var settings: Settings
private set
override fun onEnable() {
settings = Settings(this) // creates + fills settings.yml on first run
}
fun reloadConfigFile() = settings.reload() // call from your /reload command
}- Always
load()after the properties (aninit { load() }block is the idiom). @Valueis required on everyconfig()property — a missing annotation throws aConfigurationExceptionnaming the offending property.- Writes are in-memory until
save();load()/reload()both persist defaults and comments as a side effect (so the on-disk file stays complete). - Type mismatches throw
ConfigurationException(numbers are coerced first); a missing key regenerates its default. - Share one instance so
reload()propagates everywhere. - For untyped or JSON configuration, use
KFile/Ksoninstead.