Skip to main content

Running a server? This topic in the server admin guide

Messages

Versatia reads every YAML file under messages/ in your plugin's data folder into a bank of messages, keeps it up to date with /versatia reload, and sends them with runtime arguments. The text is MiniMessage: angle brackets are formatting, braces are data. The code hands over values, the text decides what to show of them.

Nothing to install or declare: every plugin may inject two beans VersatiaCore provides, each in a package of its own:

InjectPackageWhat it is
Messagesdev.versatia.messagesyour plugin's bank of messages, read from its data folder
Textdev.versatia.textthe parser behind the messages, for text that is not in a file and for describing your types; arguments (arg) and presentations live here too
import dev.versatia.messages.Messages
import dev.versatia.text.arg

@LocalBean
class Shop(private val messages: Messages) {
fun sell(player: Player, item: ItemStack, price: Int) =
messages["shop.bought"].send(player, arg("player", player), arg("item", item), arg("price", price))
}

The files​

Ship the files under src/main/resources/resources/messages/; resource extraction puts them into plugins/<Name>/messages/ on the first start, where server owners edit them. Any .yml or .yaml there, in any subdirectory, is read:

messages/shop.yml
shop:
open: "<green>The shop is open: {offers} offers today."
bought: "<green>{player} bought {item} for <gold>{price}</gold> coins."
closed:
- "<red>The shop is closed."
- "<gray>Come back tomorrow."
In the fileBecomes
nested sectionsa dotted key: shop.bought
a listone message of several lines, each parsed on its own; every line must be text
any other value, Yes, off, 1.10, 010 includedits text exactly as written, never a boolean or a number
an empty value, closed:an empty message
the file name and directorynothing: files only organise messages, a key may move between them
the same key twice, in one file or in twoan error naming the file or both files
a key that is a message in one place and a group of messages in anotheran error naming the files

Files are read as UTF-8; a file in another encoding is an error that says to save it as UTF-8. Invalid YAML is reported with the file and the place: Plugin Shop: messages/shop.yml is not valid YAML at line 3, column 12: mapping values are not allowed here.

Every standard MiniMessage tag works in angle brackets; the arguments are in braces, as below.

For server admins

Server admins edit the texts without the code; Editing messages in the server admin guide is the page to send them to.

Legacy colour codes​

MiniMessage shows &a and §l as text. A message using them is reported once per load, through your plugin's logger, with the tags to write instead:

[Shop] Message "shop.sale" in messages/shop.yml uses legacy colour codes, which MiniMessage shows as text: &a -> <green>, &l -> <bold>; write the tags instead, or turn this warning off with "legacy-color-warnings: false" in messages/.versatia.yml

A code is & or § followed by 0-9, a-f, k-o or r, in either case, with no letter or digit right before it, unless that character ends another code: &a&lSale has two codes, R&D and B&B have none. To keep codes on purpose, such as a help message explaining them, turn the warning off for your plugin in messages/.versatia.yml, which you can ship with the other files:

messages/.versatia.yml
legacy-color-warnings: false

This file holds settings, not messages: a file whose name starts with a dot is never read for messages. An unknown setting, or a value other than true or false, fails the load.

Sending​

messages[key] is a Message; a key that does not exist throws MissingMessageException with the keys that start the same way (Plugin Shop has no message "shop.byu"; the messages under "shop" are: shop.open, shop.bought, shop.closed). getOrNull and contains are there for optional messages, keys for all of them.

On a MessageDoes
get()the component; a message that uses arguments fails here with the first missing one
get(arg("item", item), ...)the component with the arguments
lines(...arguments)one component per line, for item lore and other places that take lines
send(audience, ...arguments)sends it to a player, the console, any Audience
broadcast(...arguments)sends it to the console and every online player with bukkit.broadcast.user (everyone, unless a permission plugin revokes it); fires a cancellable BroadcastMessageEvent
key, raw, filethe key, the text as written, the file relative to the data folder

A message is parsed when it is used, never when the file is read, so a tag registered after the files were read applies at once. A text without braces and without registered tags gives the same component every time, so it is parsed on its first use and reused; this holds for each constant line of a message even when the call passes arguments. A text with a registered tag is parsed at every use, because the tag may answer differently each time.

Lines​

A YAML list is a message of several lines. get() joins them with line breaks; lines() gives them apart, which is what item lore takes:

sword:
lore:
- "<gray>Damage: <red>{damage}"
- "<gray>Forged by {smith}"
meta.lore(messages["sword.lore"].lines(arg("damage", 7), arg("smith", player)))

Each line is parsed on its own, so formatting never carries over: a <red> left open on the first line does not colour the second. A message that is a single text is one line.

Arguments and properties​

{name} inserts the argument. {name.property} inserts one of its properties, and properties chain: {player.location.x} is the x coordinate of the player's location. The text, not the code, picks what to show, so a message file can switch from {player} to {player.name} without a rebuild. An argument is never a tag:

Wrong: MiniMessage leaves the unknown tags as text
joined: "<yellow><player> joined in <world>"
Right
joined: "<yellow>{player} joined in {player.world}"

What a value offers depends on its type:

Value{name} insertsProperties
Stringthe text, verbatim: tags in it are not parsed, so player input is safenone
a number, Boolean, Charits textnone
nullnothingnone
Componentthe componentplain
Playerthe display namename, displayName, uuid, level, health, foodLevel, gameMode, ping, location, world
any other Entityits namename, type, uuid, location, world
OfflinePlayerthe name, or the UUIDname, uuid
ItemStackthe item namename, hoverable (the name with the item's hover), type, amount, maxStackSize
Locationx, y, z as block coordinatesx, y, z, blockX, blockY, blockZ, yaw, pitch, world
Worldits namename, environment, time, players
Material, EntityType and any other translatable valueits name in the viewer's language (Component.translatable)none
a List, Set, any other Iterable or an arraythe elements joined with , , each shown as an argument would besize
a Presentablewhat its presentation sayswhat its presentation says
anything elseits toString()none, unless registered

A value of a more specific type keeps its own row: a Player or an ItemStack is shown as above, not as a translatable. Properties are evaluated only when the text uses them, and once per call. Argument names consist of letters, digits, _ and -; braces around anything else ({}, { x }, {1,2}) are plain text, so JSON or code in a message is left alone.

A backslash escapes a brace, as MiniMessage's \< escapes a tag, in the text and inside tag arguments alike. Without it, a brace meant literally is read as an argument and the call fails with No argument "nick"; the arguments are: player:

Wrong: {nick} is read as an argument
help: "Write {nick} in the template to insert your name, {player}."
Right
help: "Write \\{nick} in the template to insert your name, {player}." # shows {nick}
copy: "<click:suggest_command:'/template \\{nick}'>Copy</click>" # suggests /template {nick}

In a double-quoted YAML string the backslash itself is written twice, as above; in a plain or single-quoted one it is written once: help: Write \{nick} ....

Braces inside a tag​

In the text a brace inserts the value's component. Inside the argument of a tag, where MiniMessage reads a plain string, it inserts the value's plain text instead, escaped so that it can neither end the argument nor open a tag:

whisper: "<click:suggest_command:'/msg {player.name} '>Click to answer {player}</click>"
copy: "<insert:{player.name}>{player}</insert>"

An unquoted argument holding a brace is quoted for you. The text of <hover:show_text:...>, which MiniMessage parses again, keeps the component.

Your own values​

A class implements Presentable and says how it appears; presentation { } is a small builder where everything is lazy:

import dev.versatia.text.Presentable
import dev.versatia.text.presentation

class Offer(val item: String, val price: Price) : Presentable {
override fun presentation() = presentation {
component { text(item) } // {offer}
property("item") { item } // {offer.item}
property("price") { price } // {offer.price}, and {offer.price.coins} if Price has that property
}
}

A property may return anything an argument may be, which is how they chain. A presentation without component { } can only be used through its properties; using the bare brace fails with {offer} cannot be inserted as text; use one of its properties: item, price.

Types you do not own​

For a type you cannot make Presentable, register a presentation on your Text, once, for example in an @AutoInvoke(Phase.PLUGIN_START_ONLY) function:

@AutoInvoke(Phase.PLUGIN_START_ONLY)
fun describe(text: Text) = text.present<Duration> { duration ->
presentation {
component { text("${duration.toMinutes()} min") }
property("seconds") { duration.toSeconds() }
}
}

The registration applies to everything your plugin parses, its messages included, and to nothing else: Text is a prototype of each plugin. Among registered types, the one nearest to the value's class wins, so a presentation for Entity applies to a villager and a more specific one for Player to players; your registrations take precedence over the built-in ones, String, numbers and collections included, registering a type again replaces its presentation, and a Presentable value always describes itself.

Text without a file​

text.parse(input, ...arguments) parses a string with the same rules and returns a Component, for text that is built in code rather than read from a file:

import dev.versatia.text.Text
import dev.versatia.text.arg

class Welcome(private val text: Text) : Listener {
@EventHandler
fun onJoin(event: PlayerJoinEvent) =
event.player.sendMessage(text.parse("<gold>Welcome {player}, you are at {player.location}.", arg("player", event.player)))
}

text.argumentNames(input) lists the arguments a text refers to, and text.miniMessage is the plain MiniMessage instance behind it.

Tags with access to the arguments​

text.tag(name) { arguments, context, values -> ... } adds a MiniMessage tag, in angle brackets like any tag, that resolves with the call's arguments in hand, in messages and in parse alike. values gives the arguments by name, values.require<Player>("who") with a type check. This is the hook for bridges such as a PlaceholderAPI tag that reads the target player from an argument:

text.tag("upper") { arguments, _, values -> Tag.selfClosingInserting(text(values.require<String>(arguments.pop().value()).uppercase())) }

text.parse("<upper:who> joined", arg("who", "Steve")) // STEVE joined
Insert outside strings as text

A value from elsewhere, such as the result of PlaceholderAPI.setPlaceholders, may contain what a player typed: a nickname, a prefix, a clan tag. Insert it as a text component, as upper does with text(...), never through Tag.preProcessParsed or context.deserialize: those parse it as MiniMessage, and a clan tag <click:run_command:'/op x'> becomes a working click.

Tag names follow MiniMessage's rules (lower-case letters, digits, _, -) and cannot shadow a standard tag; names starting with versatia-argument- are reserved for the braces.

Reloading​

/versatia reload <name> re-reads the files before your reload-phase functions run; nothing in your plugin knows about it, because the Messages instance made for your plugin is a Reloadable bean of your plugin. A file that fails to read (invalid YAML, a duplicate key, a value that is not text) fails the reload with Plugin X: ... failed to reload and a cause naming the file, and the previous messages stay in use; at start-up the plugin does not enable.

One instance per plugin​

Messages and Text are exposed prototypes of VersatiaCore: the instances you inject are your plugin's own. Your Messages reads your data folder and parses with your Text, the same instance your beans inject, so the presentations and tags you register apply to your messages. Two plugins never see each other's messages or registrations, and neither needs the other's keys to be unique.

Errors​

A missing or unreadable message is a MessagesException (package messages):

ExceptionWhen
MissingMessageExceptionmessages["shop.byu"]: no such key; lists the keys starting the same way
MessagesLoadExceptiona file cannot be read, is not valid YAML or not UTF-8, a value is not text, a key is defined twice or is both a message and a group of messages, messages/.versatia.yml holds an unknown setting; names the file
MessageRenderExceptiona message cannot be shown; names the key and the file, with the TextException below as its cause: Plugin Shop: message "shop.bought" in messages/shop.yml cannot be shown: No argument "nmae"; the arguments are: player, item, price

A text that uses an argument wrongly fails the call with a TextException (package text) naming what is wrong and what is available, wrapped in a MessageRenderException for a message, so a broken message file is found at the first use rather than by a player reading a stray {player.nmae}:

ExceptionWhen
UnknownArgumentException{nmae}: No argument "nmae"; the arguments are: name, item; also a tag asking values for an argument the call did not pass
UnknownPropertyException{player.nmae}: {player} has no property "nmae"; it has: name, displayName, ...
NullValueException{player.location.world.name} while the world is null: {player.location.world} is null, so it has no property "name"
NoTextFormExceptionthe bare brace of a value whose presentation has no component
PresentationExceptiona presentation, or the code it calls, threw; names the brace and has the exception as its cause: {item.price} cannot be shown: java.lang.IllegalStateException: ...
InvalidArgumentNameExceptionarg("Player Name", x): a name with anything but letters, digits, _ and -; thrown by arg() itself
DuplicateArgumentExceptionthe same argument name passed twice in one call
ReservedTagNameExceptiona tag named like a MiniMessage tag (b, color, click...), starting with versatia-argument-, or not a tag name
ArgumentTypeExceptionvalues.require<T> on an argument of another type

MiniMessage tags that do not exist are not an error: MiniMessage leaves them as text, as it always does.

Corner cases​

  • A key that is a message in one file and a group in another fails the load: Plugin Shop: "shop" is a message in messages/general.yml and a group of messages in messages/shop.yml; rename one of them. Within one file the message reads "shop" is both a message and a group of messages in messages/shop.yml; rename one of them.
  • An empty value is an empty message, not an error and not a missing key.
  • YAML types do not apply. enabled: Yes is the text Yes, version: 1.10 is 1.10, and code: 010 is 010, exactly as written.
  • A brace inside <pre> is not replaced: the message shows an internal placeholder instead of the value. Keep arguments out of <pre>.
  • A tag of your own is called twice per parse, because MiniMessage reads a text in two passes. Keep it free of side effects.
  • An Inventory is an Iterable, so {inventory} lists its items; an empty slot shows as nothing between two commas.
  • Threads. Text and Messages may be used, and present and tag called, from any thread, such as an AsyncChatEvent renderer. The built-in presentations read Bukkit state ({player.location}, {world.players}), which is safe on the main thread only.
  • A missing key never falls back to a copy of the file in the plugin JAR: the bank is what the data folder holds.
  • Backslashes before a brace pair up. An odd number escapes it; in the text and in a quoted tag argument, where MiniMessage reads \\ as one backslash, \\{nick} shows \Steve (with nick = Steve) and \\\{nick} shows \{nick}. A backslash before anything else is left to MiniMessage. argumentNames skips escaped braces, and a text whose only braces are escaped is parsed once like any constant text.
  • Inside an unquoted tag argument the brace escape is the only backslash rule: MiniMessage reads every other backslash there literally, so <insert:\{nick}> inserts {nick} and <insert:\\{nick}> inserts \\Steve.
  • Legacy codes are found by the character before them. x&a and 5&l are not reported, since a letter or digit precedes the code; &a&l is two codes. Hex forms (&#55ff55, &x&5&5...) are not detected. The warning comes again at every reload until the message or the setting changes.
  • Files starting with a dot (messages/.versatia.yml, messages/.draft.yml) hold no messages; only messages/.versatia.yml is read, for settings.