Skip to main content

Messages

Package: dev.anchorlight.stonelib.message

Player-facing text belongs in a file server owners can edit, not in code. MessageService loads that file, resolves keys through MiniMessage, and substitutes placeholder values in a way that cannot inject markup.

The messages file

Bundle a messages.yml in your resources. A top-level prefix key, if present, is prepended to chat messages:

prefix: "<gray>[<gold>Rally</gold>]</gray> "

opted_in: "<green>You will now be brought along to rallies."
countdown: "<yellow>Rallying to <white>{0}</white> in {1}..."
payout: "<green><player></green> earned <gold><points></gold> points (balance <balance>)"

The file is kept up to date with the bundled copy through ConfigUpdater on every load and reload. Without that, a plugin update that adds a message would show [Missing message: ...] on every server that already had the old file.

MessageService

MethodPrefixPlaceholdersMeaning
new MessageService(JavaPlugin, String fileName)Updates and loads the file.
reload()Updates and re-reads the file.
get(key, Object... values)yespositionalThe message as a Component.
send(CommandSender, key, Object... values)yespositionalSends it as chat.
sendActionBar(Player, key, Object... values)yespositionalSends it as an action bar.
getNamed(key, Object... keyValuePairs)yesnamedThe message as a Component.
getNamedUnprefixed(key, Object... keyValuePairs)nonamedFor titles, action bars, sidebars, and item names.
sendNamed(Audience, key, Object... keyValuePairs)yesnamedSends it as chat to any audience, including the whole server.
sendNamedActionBar(Audience, key, Object... keyValuePairs)nonamedSends it as an action bar.
showTitle(Audience, titleKey, subtitleKey, Title.Times, Object... keyValuePairs)nonamedShows a title and subtitle from two keys.
raw(key)nononeThe unparsed template text, for parsing later.
has(key)Whether the key exists and is not blank.

Positional placeholders

{0}, {1}, and so on are replaced by the values in call order:

messages.send(player, "countdown", waypoint.name(), 5);

Fine for short messages. Once a template carries three or four values, positional placeholders get hard to write and to re-order, which is what named placeholders are for.

Named placeholders

Pass the placeholder name, then its value, repeated. The template reads the way the message does:

messages.sendNamed(player, "payout", "player", name, "points", 12, "balance", balance);

A trailing name with no value is ignored, and a null value becomes empty text.

Missing keys

MethodWhen the key is missing
get, send, sendActionBar, getNamed, getNamedUnprefixed, rawRenders [Missing message: <key>], with the prefix where the method applies one.
sendNamed, sendNamedActionBarSends nothing. Also sends nothing when the value is blank, which makes a message optional by design: a server owner can blank it out to turn it off.
showTitleRenders [Missing message: <key>] for whichever of the two keys is missing.

:::caution An absent subtitle key is shown, not skipped showTitle renders the missing-message text for an absent key rather than leaving that line blank. For a title with no subtitle, add a subtitle key with an empty value, such as rally_title_sub: "", rather than leaving it out. :::

:::note sendActionBar keeps the prefix sendActionBar goes through get, so the chat prefix appears in the action bar too. Use sendNamedActionBar for an unprefixed one. :::

Placeholder values are always inert

Every placeholder value, positional or named, is substituted with MiniMessage's Placeholder.unparsed. The template is parsed as markup. The value is inserted as plain text afterwards. So a player named <click:run_command:/op me> shows up as that literal text and never becomes a clickable command. This is covered by StoneLib's tests.

MiniMessages

Named-placeholder parsing without a messages file, for scoreboard lines, item names, and log output.

MethodMeaning
parse(String template, Object... keyValuePairs)Parses a template into a Component. A null template is treated as empty.
plain(String template, Object... keyValuePairs)As parse, flattened to plain text.
plain(Component)Strips markup from a component. null becomes "".
resolvers(Object... keyValuePairs)The unparsed TagResolvers, for your own MiniMessage.deserialize calls.
Component line = MiniMessages.parse("<gray>Round <white><round></white> of <total>", "round", 3, "total", 5);

UntrustedText

Placeholders are safe on their own. The risk is building a MiniMessage string yourself by concatenating text you did not write: chat input, a name from an external API, a dialog text field. Sanitise that text first:

String safeName = UntrustedText.forDisplay(apiResponse.displayName(), 40);
Component message = MiniMessage.miniMessage().deserialize("<yellow>" + safeName + " joined!");

forDisplay(String raw, int maxLength):

  1. Escapes every \ to \\, then every < to \<. The order matters: escaping < first would let an input like \<bold> combine with the inserted backslash into \\<bold>, which MiniMessage reads as a literal backslash followed by a live tag.
  2. Turns every whitespace character, including newlines and tabs, into a single space.
  3. Drops other control characters, and the legacy formatting marker §, so untrusted text cannot apply old-style colour or obfuscation codes.
  4. Trims the ends.
  5. Truncates to maxLength characters, ending in ... when it had to cut, and never splits an emoji or other surrogate pair.

null becomes "".

It neutralises every < rather than looking for known tag names, so it cannot be bypassed by a tag it does not recognise. Use it as defence in depth even for values you pass as placeholders, and always for anything you concatenate.

:::caution Sanitised text is for display only The output is made safe to concatenate into a template. Do not parse it on its own and then treat the result as trusted. :::

LegacyColorConverter

For configs or data still written with & colour codes.

MethodMeaning
parseLegacy(String)Parses &-coded text into a Component.
toPlainText(String)Parses &-coded text and returns only the plain text, discarding formatting.