Skip to content

Placeholders

Leo edited this page Jan 27, 2024 · 18 revisions

Messages might contain embedded values, like integers in "Page 1/2". These values are represented by placeholders.

Administrator Usage

Administrators use placeholders wherever they need them. Placeholders, like tags, can have attributes.

Page {page}/{pages:some:attributes}

would be one example of how to use them. Placeholders might be number values that can be formatted. This must be specified by the plugin developers. Examples:

msg={balance:%.2f} #  --> 1.23 or 0.00

For more information see https://docs.oracle.com/javase/tutorial/java/data/numberformat.html

Choices

You might at some point want to render a plural depending on the variable content, like 'if 0 or multiple minutes remaining, render "Minutes", if 1 minute remains render "Minute"'.

In MiniMessage, this would be done with a choice placeholder, like so:

<minutes_remaining/> <minutes_remaining_choice:'Minute':'Minutes'/>

This is a rather confusing approach because we must register a choice placeholder for every other placeholder we register.

TinyTranslations and the NanoMessage syntax solves this with a choice placeholder

{minutes_remaining} {minutes_remaining ? 'Minute' : 'Minutes'} or {minutes_remaining} Minute{minutes_remaining ? '' : 's'}

We simply use the actual placeholder before the choice symbol ? (all attributes are valid) and append options after the choice symbol. If we add more than two options, the first option represents 0 occurances. The last one will always resemble all not explicitly defined occurances.

{a:b:c ? '1' : '0 or many'}
{a:b:c ? '0' : '1' : 'many'}
{a:b:c ? '0' : '1' : '2' : 'many'}
and so on

Internally when converting to basic MiniMessage, the choice placeholder becomes <choice:'<a:b:c/>':'1':'0 or many'/>

Plugin Developer Usage

To apply placeholders to your Message instance, look at the following code:

public static final Message MY_MESSAGE = new MessageBuilder("page").withDefault("Page <page>/<pages>").build();

// and wherever you need it:
sendMessage(Messages.MY_MESSAGE
        .insertNumber("page", page)
	.insertBool("last_page", page == pages)
	.insertObject("player", player)
	.insertObject("desc", plugin.getDescription())
	.insertString("lazy", StringUtil::generateLoremIpsum)
);

Messages are formatted with placeholders by calling the #formatted(TagResolver...resolvers) method. The method does not modify the original Message instance but creates a copy (the contract is pure, like with any Adventure Component). To reduce verbosity, there are the insert methods that all create TagResolvers and then call the #formatted method on the Message.

TagResolvers are the way to resolve Tags within a MiniMessage text, for more information, take a look at the Adventure wiki.

Insert Primitives

There is a series of primitives that can be inserted and you should use the according methods when inserting types. Don't convert your integers to strings and insert them as string. use insertNumber("my_int", myInt) instead. This allows users to use number formatting, which is not the developers choice to make.

Primitives that can be inserted are:

  • Strings with insertString
  • ComponentLikes with insertComponent
  • int, float, double, short, long and corresponding Object wrappers with insertNumber
  • booleans with insertBool
  • Temporals (with some restrictions) with insertTemporal
  • Lists with insertList

If you would like the list to include more methods suggest them in an issue or contribute via pull request :)

Insert Objects

You might want to render objects that are not part of the above primitive list. The whole concept of this framework is to create a standardization for how chat messages are being created. Therefore I recommend using the following concepts.

I will explain the situation with the Player object first, because it is a pretty common situation. You can simply insert a player name via insertObject(player), which is your best option. There is a default way of how the server renders players, and again, administrators can configure it.

By default, display names are being rendered with a hover text that shows the players UUID. This is configured with the global 'format.player' message. If you use insertString(player.getDisplayName()) (respectively Component on paper) instead, you enforce a chat design that administrators can not configure.

Most typical bukkit classes are renderable, like ItemStack, Player, PluginDescription, Material and so on. Most primitive classes like Material directly become their according translatable component (Material.Diamond -> <tr:item.minecraft.diamond>)

If you want to render a custom object, you first have to declare a way of how the object becomes a component. You do so by defining a set of productions, like shown below for the player class.

        ObjectTagResolverMap map;
        map.put(
                // What class should be rendered
                Player.class,
                // Which attributes are valid
                Map.of(
                        "name", Player::getName, // {player:name}
                        "uuid", Entity::getUniqueId, // {player:uuid}
                        "type", Entity::getType,
                        "display", Player::getDisplayName,
                        "location", Player::getLocation // {player:location} will return location, which then triggers its own resolver on the remaining attributes
                ),
                // What to do with {player} and no attributes.
                p -> BukkitGlobalMessages.FORMAT_PLAYER.insertObject("player", p)
        );

As of v4.4.0, register types in TinyTranslations.NM.getObjectTypeResolverMap(). This will change in future versions.

Lazy Placeholders

Have a more detailed look at TagResolvers and the way tags are being rendered if you want to boost performance of your application. For example, if you want to add a specific element as placeholder that takes quite long to prepare or render, you would only want to render it if the placeholder is really used by administrators.

Methods that accept a Supplier instead of the actual type are lazy formatters, that only call the Supplier method if the Tag is actually being used.

player.sendMessage(Messages.MY_MESSAGE.insertString("a_placeholder_admins_would_probably_not_use_anyways", () -> {
        // with a lot of things going on here to produce the string.
        return ...;
}));

NanoMessage Default Resolvers

NanoMessage - the message format of TinyTranslations that extends the default MiniMessage - introduces some default tags and placeholders, which are listed below.

{msg:[namespace]:[key]}

This resolver resolves any message by namespace and key. Namespace is optional. See messages section for a detailled explanation.

<[style]>{slot}<[style]/>

Any style that has been registered through style storages

<repeat:[count]>{slot}</repeat>

Repeats the slot given amount of times.

<shorten:[length]>{slot}</shorten>

Shortens the slot to the given length of characters and appends "...". <shorten:4>Hello world!</shorten> will become Hell...

<shorturl:[a]:[b]>{slot}</shorturl>

Shortens an url of format

(https?://)?[^/]/(.+)

Where the "tail" is everything that comes after the domain. a describes how many characters of the tail start should be visible, with default of 3. b describes how many end characters of the tail should be visible, again default 3. so <shorturl>https://github.com/KyoriPowered/adventure</shorturl> will have a tail of KyoriPowered/adventure that is being reduced to Kyo...ure when a=3 and b=3.

<lower>{slot}</lower>

Makes the content lower case. <lower><msg:prefix></lower> will render the prefix in lower case without retyping it.

<upper>{slot}</upper>

Makes the content upper case.

<lighter>{slot}</lighter>

Makes the content brighter, where explicit colors are being used.

<brighter><red>Hello</red></brighter> is a brighter shade of red, while
<red><brighter>Hello</red></brighter> is not, since <red> is not part of the tag content.

<darker>{slot}</darker>

Makes the content darker, see <lighter>.

<reverse>{slot}</reverse>

Mirrors the slot char by char. <reverse>Hello wor<red>ld!</red></reverse> becomes the parsed version of <red>!dl</red>row olleH