Running a server? This topic in the server admin guide
Configuration values
A value a plugin reads from its configuration is a property marked @Value: the runtime sets it
from a YAML file in the data folder when the plugin starts and again at every
reload. No file handling, no getter, no reload function.
import dev.versatia.config.Value
@LocalBean
class Shop {
@Value("shop.tax")
var tax = Percent.ZERO
@Value("shop.hours")
private var hours: List<Int> = emptyList()
@Value("max-players", file = "limits.yml")
var maxPlayers = 0
}
key is the dotted path inside the file, file the path relative to the data folder,
config.yml unless given (Value.DEFAULT_FILE). The file is one the plugin ships under
resources/, so the defaults are the file itself and the administrator edits the
extracted copy.
The server admin edits the values in plugins/<Name>/ and applies them with /versatia reload <name>;
Configuration files in the server admin guide explains how values are
written and what a mistake does.
Where a property may live
| Owner | Set on |
|---|---|
| the plugin's main class | the plugin instance |
a Kotlin object | the object |
| a bean | the bean instance of that plugin |
a plain Listener or BackgroundTask, or any class a subclass enhancement creates | the one instance the plugin creates for it, which stays no bean |
The same places an auto-invoked function may
live, for the same reason: the runtime has an instance there. On an @ExposePrototype the
property is set on every instance, the owner's own and each one made for another plugin, from the
owner's files, and a reload of the owner updates them all and then reloads the plugins holding
them; see @Value on a prototype. On a plain listener or
task the property is set right after its constructor, before the listener is registered or the
task first runs, and again at every reload, before the task restarts. In a bean or a plain
listener or task the property may also be declared in the primary constructor. The property must be a var,
or a val holding a PreviousValueTracking. Visibility does not
matter: a private property, a private set or a private class gets a reflective setter in the
descriptor, a visible one a direct assignment. Anything else is a compile error: a val of any
other type, a plain constructor parameter, a parameter of a function, a top-level property, a
property of a plain class that nothing creates.
The initializer is only a placeholder until the runtime sets the value, so pick what reads best:
lateinit var motd: String for a reference type, var maxPlayers = 0,
var tax = Percent.ZERO, var period = 30.seconds or var token = Secret.EMPTY for a primitive or
a value class such as Int, UInt, Percent, Duration or Secret, which Kotlin does not allow
as lateinit.
Supported types
| Type | Read as |
|---|---|
Boolean, Int, Long, Float, Double, UInt, ULong, String, Percent, Duration, Secret | one value, with the strict getters: 3000000000 is not an Int, -1 is not a UInt, an unquoted 01234 is not a String, 12.5% is a Percent, 30s is a Duration, and a Secret is text, ${env:NAME}, ${file:PATH} or ${pluginfile:PATH} |
| any enum | one value, matched by constant name ignoring case; java.time.DayOfWeek and Material included |
Color, Location, OfflinePlayer, Vector, ItemStack, ConfigurationSection | one value, with Bukkit's own deserialisation |
List<T>, Set<T> | a YAML list of T, for T among the scalars and enums above, every element checked like a single value; a single value where the list belongs is an error, not a list of one |
Map<String, T> | a YAML section whose children are T, for the same T |
A nullable type makes the value optional: var closedOn: DayOfWeek? = null stays null when the
key is absent. Every other type, a MutableList, a nullable element, a data class, is rejected
at compile time with the list of what is supported, unless a mapper reads it.
Durations
A kotlin.time.Duration is written with its unit, in Kotlin's notation or in ISO-8601, so the file
says what it means and the code needs no conversion:
@Value("auto-broadcast.period")
var period = 30.seconds
@Value("cooldowns")
var cooldowns: List<Duration> = emptyList()
A bare number is not a duration: period: 300 fails with Value at "auto-broadcast.period" is not a duration such as "30s", "5m" or "1h 30m" (units d, h, m, s, ms; must not be negative): "300". The value goes
straight into every(period) or delay(period), so a negative
duration such as -5s and Infinity are rejected the same way.
Secrets
A password, a token or an API key is a Secret (dev.versatia.util.type). It
prints as ****, so a log line, a message or an exception never shows it, and the file may name
where the secret is instead of holding it:
import dev.versatia.util.type.Secret
@LocalBean
class DiscordBridge(private val logger: Logger) {
@Value("discord.token")
var token = Secret.EMPTY
@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
fun connect() {
logger.info("Connecting with token $token") // Connecting with token ****
client.login(token.reveal()) // the secret itself, only where it is needed
}
}
| Written | Becomes |
|---|---|
| text | the text, as written |
${env:NAME} | the environment variable NAME of the server process |
${file:PATH} | the content of the file, read as UTF-8 without its trailing line breaks; a relative path starts at the server folder, the one holding server.properties, and an absolute one (/etc/minecraft/token.txt, C:/secrets/token.txt) is taken as it is |
${pluginfile:PATH} | the same for a file in the plugin's data folder: ${pluginfile:api-key.txt} in plugins/Shop/config.yml reads plugins/Shop/api-key.txt |
| both inside text | each one replaced: Bot ${env:SHOP_TOKEN} |
The references are resolved at every start and reload, so a secret rotated in its file or
variable is picked up by /versatia reload <name>. A variable that is not set or a file that is
missing fails like any wrong value, naming the key and the reference, never the secret:
Plugin Shop: com.example.DiscordBridge.token could not be read from config.yml: Value at "discord.token" uses ${env:SHOP_DISCORD_TOKEN}, which is not set; set that environment variable for the server, or write the value instead
An empty secret, the default of a shipped file, switches its feature off by
convention: check token.isEmpty(). Docker and Kubernetes
hand secrets to a container as files under /run/secrets/, which is what ${file:PATH} is for. List<Secret> and Map<String, Secret> read every element the same way.
How the server admin hands over a secret, with ${env:...}, ${file:...} or ${pluginfile:...}, is
explained in Secrets of the server admin guide.
Mappers
A type of your own, or a value that is not what the file says, is read by a ValueMapper named
in the annotation:
import dev.versatia.config.SectionMapper
import dev.versatia.config.ValueMapper
object TicksFromSeconds : ValueMapper<Long> {
override fun map(section: ConfigurationSection, key: String) = section.getLongOrThrow(key) * 20
}
data class PriceRange(val min: Int, val max: Int) {
object Mapper : SectionMapper<PriceRange>() {
override fun map(section: ConfigurationSection): PriceRange {
val min = section.getIntOrThrow("min")
return PriceRange(min, section.getIntOrThrow("max").validateAtLeast(min))
}
}
}
data class Ore(val name: String, val veins: Int)
object Ores : SectionMapper<List<Ore>>() {
override fun map(section: ConfigurationSection) = section.mapChildren { name, ore -> Ore(name, ore.getIntOrThrow("veins")) }
}
@LocalBean
class Shop {
@Value("shop.restock-seconds", mapper = TicksFromSeconds::class)
var restockTicks = 0L
@Value("shop.price-range", mapper = PriceRange.Mapper::class)
var priceRange = PriceRange(0, 0)
@Value("ores", mapper = Ores::class)
var ores: List<Ore> = emptyList()
}
The mapper receives the section holding the key and the key, so it reads with the
strict getters, reports with their messages (the full key path
included) and validates with the guards; whatever it throws fails
the event like a wrong value would. It is an object or a class with a no-argument constructor,
and it is called only when the key is present: a missing key follows the nullability of the
property, as for any value. The compile-time step checks that what the mapper maps to fits the
property (mapper TicksFromSeconds maps to Long, which Int cannot hold), so a mismatch never
reaches a server. With a mapper, the property may have any type.
A value written as a section of its own, the usual shape of a data class, takes a SectionMapper,
as PriceRange.Mapper above: its map(section) receives that section, and a key holding
anything else fails with Value at "shop.price-range" is not a section: "5".
Reacting to a change
A reload sets the property and the old value is gone. Code that must know what changed, to
reschedule a task only when its interval did, say, declares the property as a
PreviousValueTracking<T>, a val holding a cell the runtime fills:
import dev.versatia.config.PreviousValueTracking
@LocalBean
class Announcer(private val plugin: JavaPlugin) {
@Value("announce.interval-seconds")
val interval = PreviousValueTracking<Int>()
@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
fun schedule() {
if (!interval.changed) return // true at start, then only when the file changed it
plugin.logger.info("Announcing every ${interval.value}s" + (interval.previous?.let { ", was ${it}s" } ?: ""))
// cancel the old task, start a new one ...
}
}
| Member | Meaning |
|---|---|
value | the value last set; reading it before the runtime set one throws |
previous | the value the last assignment replaced; null until the second assignment |
hasPrevious | whether there is one, which tells a replaced null from none |
changed | whether the last assignment changed the value; true after the first one |
The held type follows the same rules as a plain property: any supported type,
a mapper, nullable for an optional value (PreviousValueTracking<DayOfWeek?>); the
cell itself is never nullable and the property is initialised with an empty one. The runtime
stores into value, so the property may stay a val and even be private. A reload that fails
assigns nothing, so previous and changed still describe the last reload that succeeded.
Missing and invalid values
A non-nullable property whose key is absent is an error, and so is a value of the wrong shape or a file that does not exist:
Plugin Shop: com.example.Shop.tax could not be read from config.yml: No value present at "shop.tax"
Plugin Shop: com.example.Shop.tax could not be read from config.yml: Value at "shop.tax" is not a percent string such as "12.5%" (0-100): "8"
Plugin Shop: config.yml could not be read
Plugin Shop: limits.yml is missing from the data folder; put it back, or reload the plugin to restore the default copy from the plugin JAR
The third line comes with the YAML parser's message as its cause, pointing at the line. When the
JAR has no resources/limits.yml either, the last message says so and tells the developer to
ship one. There is no default value in the annotation on purpose: the default is in the shipped
file, where the administrator sees it, and a key that disappears is a mistake worth reporting
rather than a silent fallback. At start such an error keeps the plugin disabled; at a reload it
reaches the sender of /versatia reload and is logged as one warning, without a stack trace.
Every value of the plugin is read before any property is set, so a broken file leaves the plugin
running with the values it had.
In a constructor
A bean, or a plain listener or task, may declare a value as a property of its primary constructor:
@LocalBean
class Shop(
private val plugin: JavaPlugin,
@Value("shop.tax") var tax: Percent,
@Value("announce.interval-seconds") val interval: PreviousValueTracking<Int>,
)
The value is read before the bean is created and passed to the constructor, so the constructor
already sees it, and it needs no placeholder initializer. At every reload the property is set
again like any other. The parameter must be a property: var, or a val holding a
PreviousValueTracking, which receives a cell already holding the value. A val of another type
or a plain parameter is a compile error,
Shop.tax must be a var to be enhanced; declare the constructor parameter as a property.
Values that derive from others
A reload sets the @Value properties and nothing else. A value computed from them is kept
current in one of three ways, from the simplest:
-
Compute it where it is used. A getter costs nothing and is never stale:
@LocalBeanclass Shop {@Value("shop.price")var price = 0@Value("shop.tax")var tax = Percent.ZEROval priceWithTax get() = price * (1 + tax.normalizedValue)} -
Recompute it in a
PLUGIN_RELOAD_OR_STARTfunction, when it is expensive or built once per configuration. The function runs after the values are set, at start and at every reload:@LocalBeanclass Shop {@Value("shop.name")lateinit var name: Stringvar label = text("")private set@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)fun recompute() {label = MiniMessage.miniMessage().deserialize("<gold>$name")}} -
React to a change with a
PreviousValueTracking, when the work should happen only if the value changed, such as rescheduling a task: theAnnouncerabove checksinterval.changed.
When the values are set
Values are set at PLUGIN_RELOAD_OR_START. At start the files are extracted first; a value
declared in a bean's constructor is read before that bean is created, and a value of a bean's
body is set once the bean exists. At a reload the values are set before anything else. The
plugin's Reloadable beans are reloaded afterwards, so a
reload() may read a value, and the auto-invoked functions of the
event run last, so a function reading a @Value property always sees the current file. Why this
order and what follows from it, with examples, is under
The order within an event:
class ShopPlugin : VersatiaPlugin() {
@Value("motd")
lateinit var motd: String
@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
fun announce() = logger.info(motd)
}
Listeners and tasks read the property when they need it rather than copying it when they are created, like any other reloadable state.
Common mistakes
-
@Valueon avalor a plain constructor parameter. The runtime assigns the property at start and at every reload, so the build fails withShop.name must be a var to be enhanced, and for the parameter withShop.tax must be a var to be enhanced; declare the constructor parameter as a property:Wrong@LocalBeanclass Shop(@Value("shop.tax") tax: Percent) {@Value("shop.name")val name = ""}Right@LocalBeanclass Shop(@Value("shop.tax") var tax: Percent) {@Value("shop.name")lateinit var name: String}A
valis right only when it holds aPreviousValueTracking. -
Implementing
Reloadableto read the plugin's own files.Reloadableexists for the prototypes of modules, which read the consuming plugin's files and have no other way to follow that plugin's reloads. A plugin's own configuration is@Valueproperties, with a mapper for anything the built-in types do not cover; what derives from them is rebuilt in aPLUGIN_RELOAD_OR_STARTfunction. A bean that loads a file inreload()is a module's job done by hand. -
Reading a file in a constructor. Constructors run once; the value never changes on reload.
-
A default in the code. The initializer of a
@Valueproperty is overwritten at start and never consulted again, so it is a placeholder, not a default:lateinitwhere Kotlin allows it,0orPercent.ZEROwhere it does not. The default belongs in the shipped file, where the administrator sees it; an optional value is a nullable property. -
Copying a value into a listener or task. Read the property when the event or tick happens; a reload changes the property, not the copy.
-
A password or a token as a
String. Any log line or message that prints it shows it, and the file has to hold it in plain text. Make it aSecret, which prints as****and can come from an environment variable or a file, and callreveal()only where it is handed over:Wrong@Value("discord.token")lateinit var token: Stringfun connect() {logger.info("Connecting with token $token") // the token is in the server logclient.login(token)}Right@Value("discord.token")var token = Secret.EMPTYfun connect() {logger.info("Connecting with token $token") // Connecting with token ****client.login(token.reveal())}
Corner cases
-
A copy made in a constructor is not updated. With
class Shop(@Value("shop.tax") var tax: Percent) { val rate = tax.normalizedValue }a reload setstax, butratekeeps the value of the start. Derive it with one of the three patterns. -
A constructor sees the values of the main class and of
objects. At start they are set before any bean is created, soclass Shop(plugin: ShopPlugin) { val title = plugin.title }reads the configuredtitle, never the placeholder. A property declared in the body of a bean is set right after that bean's constructor, before the bean is injected anywhere, soclass Shop(prices: PriceList)sees every value ofprices, but the placeholders of its own body; a bean that needs its own value at construction declares it in its constructor. -
A prototype's values follow the owner. An
@ExposePrototypeinstance made for another plugin takes its values from the owner's files: its constructor receives the values the owner read last (an instance made after a reload gets that reload's), its body values are set right after, and every reload of the owner sets them again and then reloads the consumer. A tracked constructor value is a cell of each instance's own. -
The file stays inside the data folder.
fileis a relative path to a.ymlor.yamlfile; an absolute path or a..segment is a compile error, and the runtime refuses any path that leaves the data folder once normalised. -
Values come before functions. At start and at every reload all values are set before any
@AutoInvokefunction of the event runs, and a failed read leaves every value as it was and runs no function. -
A duration needs its unit.
300is an error, not 300 seconds or ticks; write300sor5m. -
A
Stringor aSecretis text. An unquoted01234,1.10,yesor2024-01-01is something else to YAML (668, 1.1,true, a date), and Bukkit'sgetStringwould quietly return"668". Such a value is an error asking for quotes instead:Value at "code" is read as a number (668), not text; quote it, ...; writecode: '01234'. A secret's message never shows the value. -
A secret's references are resolved once. What a variable or a file holds is taken as written, even when it contains
${env:...}. Any other${...}in a secret is an error naming the reference (${DB_PASSWORD}gets "did you mean${env:DB_PASSWORD}?"), so a secret that really contains${goes into a file or a variable. An unclosed${is plain text. -
A secret's file is read whole. Its content without a byte order mark and without the line breaks at its end is the secret, so a multi-line key keeps its inner line breaks; a file that is not UTF-8 is an error.
-
An empty secret prints as
'', so a log line tells an unset password from a set one, which prints as****. -
A duration is finite and not negative.
-5s,-InfinityandInfinityare errors: the value is a wait or a period, so0sis the shortest one. -
A single value where a list belongs (
hours: 9for aList<Int>) is an error, not a list of one. An absent key is never an empty list: it is a missing value,nullfor a nullableList<Int>?and an error otherwise; writehours: []for an empty one. -
An override is annotated once. When a base class declares
@Value("limit") open var limit, a subclass overrides it without@Value: the value set through the base property already reaches the override, and annotating both is a compile error. -
Both annotation targets work on a constructor property.
@Value("k") var x: Intand@property:Value("k") var x: Intin a primary constructor are the same: the value is passed to the constructor and updated at every reload.
Under the hood
@Value is an enhancement like any other: the descriptor records
each property as a PropertyTarget with its type and a setter, and
ValueEnhancementProcessor (ConfigProcessors.VALUE) in VersatiaCore reads the files and
assigns the values, or lets the ValueMapper read them. The supported types are listed in
ValueTypes, which the compile-time step checks against, and read with the getters of
util.config; see
The descriptor and The runtime.