Astral Realms Documentation Help

Placeholders

AstralCore's placeholder system is the universal bridge between configuration files and runtime values. The same syntax is used in menus, dialogs, action arguments, requirement values, item names and lore, chat messages, message-config strings — anywhere a string passes through AstralCore's parser.

Syntax

Standard placeholder

%namespace_key_subkey%

Underscores (_) separate namespace tokens. A placeholder is parsed left-to-right: the first token selects a namespace, and each subsequent token narrows the lookup inside that namespace.

title: "<bold>Hello %player_name%" lore: - "<gray>HP: %player_health_rounded%" - "<gray>Helmet: %player_equipment_HEAD%"

Inner placeholders

A placeholder can be nested inside another using curly braces. The inner placeholder is resolved first and its result is substituted into the outer key before the outer lookup runs.

%outer_{inner_placeholder}%
# variables.tab → "weapons", so the resolved key becomes %parameters_weapons% lore: - "<gray>Active tab: %parameters_{variables_tab}%"

Inner placeholders may be combined with literal segments:

- "<gray>Slot: %layouts_{variables_active_layout}_page%"

A brace group may also hold an inline function call or an $e(...) expression rather than a placeholder name. It is computed first and its result is spliced into the outer key, with no second resolution pass:

# $lowercase(Vilain) -> "vilain", so the resolved key is %img_tags:vilain% - "%img_tags:{$lowercase(%parameters_faction%)}%"

Expression evaluation

Wrap an arithmetic expression in $e(...) to evaluate it. Placeholders inside the expression are resolved first; the result must be numeric. Returns an integer when the result is a whole number, otherwise a double.

$e(%variables_count%+1) $e((%variables_qty%*%parameters_price%)-5) $e(round(%player_health%, 1))

Supported operators come from the Crunch math library: +, -, *, /, ^, plus parentheses. The custom round(value, precision) function is registered out of the box.

Expressions may be nested to any depth — the parser matches balanced parentheses rather than using a regex, so there is no depth limit. Inner expressions are evaluated first and passed to the outer one as raw doubles, so precision is never lost to a string round-trip.

$e($e(1+2)+2) # → 5

Each %placeholder% and each nested $e(...) is substituted by a positional variable and the expression is compiled once. A resolved placeholder must be a Number or a string that parses as a double; anything else — including null — fails with an error instead of falling back to a value.

An expression may appear inside an inline function's argument, where it is resolved by that argument's own wrapper:

$max(1,$e(1+$e(2+3))) # → 6.0 (max returns a double)

The reverse also works: an inline function may be nested inside an expression, at any depth. Each nested call becomes a positional input of the compiled expression and is fed in as a raw double — the function's return value is used, not its rendered text.

$e($round(%player_health%)+1)

The nested function must return a Number, or a string that parses as a double. Anything else fails resolution with an error naming both the call and the enclosing expression (Function $x(...) in expression $e(...) must produce a number, but returned "…"). In particular a formatting function cannot feed an expression back — $format-number emits grouping separators, so $e($format-number(%big%)+1) fails at render time:

$e($format-number(%variables_total%)+1) # error: returned "1,234.5"

Expressions are commonly used with set-variable:

- "[set-variable] count $e(%variables_count%+1)" - "[set-variable] qty $e(%variables_qty% < 64 ? %variables_qty%+1 : 64)"

Inline functions

For named, multi-argument helpers (clamped arithmetic, number formatting, date formatting, etc.), see Functions — invoked as $name(arg1, arg2, ...).

MiniMessage in resolved values

When a resolved placeholder or inline-function value is substituted into a component field — an item name, a menu title, any lore line — a value containing MiniMessage tags (<red>, <#FFFFFF>, <gradient:…>) is deserialized as MiniMessage and rendered as formatting. Values with no <…> tag are inserted as plain text and inherit the surrounding style. Detection is the regex <[^<>]+>, and the rule applies to every element of a lore list. Placeholder values substituted into a hover tooltip follow the same rule.

Substitution is a single pass over the line: every registered key is matched by one compiled alternation, so a resolved value is spliced in as-is and is never re-scanned for placeholders or inline tokens. A value that itself contains %…% text therefore renders literally instead of being resolved a second time. List-valued placeholders are the one case where a line expands into several.

List-valued placeholders

A placeholder may resolve to a List. What happens then depends on the context it is written in:

  • In a list context (a lore: line, or any multi-line field) the line expands into one line per element. Surrounding text on the same line is kept on the first produced line only — the remaining elements are emitted as bare lines — so such a placeholder is normally the whole line.

  • An empty list produces zero lines — the lore line disappears entirely rather than rendering [] or an empty line.

  • In a single-component field (an item name:, a menu title, a message line) the list resolves to its first element; an empty list resolves to the empty string.

This is the same rule the iterable and conditional lore modifiers follow — see Lore modifiers and Items.

Placeholders in click and hover events

Inside a <hover:show_text:…> tooltip, placeholders, $e(...) expressions and $fn(...) inline functions resolve exactly as they do in the line's own text — tooltip text goes through the same substitution pass. This includes:

  • a tooltip attached to the whole line (the component the walk starts from), not only to a child span;

  • a line that consists of nothing but a single placeholder and carries a tooltip — the tooltip is preserved and substituted rather than dropped.

A tooltip shares the line's resolved values, so the same token yields the same value in the line and in its tooltip.

Inside a <click:…> value the rule is narrower: only %placeholder% tokens are substituted. A click payload is scanned for %…% alone, so $e(...) and $fn(...) written in a click value are never resolved and are sent through as literal text. Each %…% value is inserted as a plain string (no MiniMessage handling), which is what a command or URL payload needs.

Style-valued placeholders

A placeholder (or inline function) that resolves to an Adventure style rather than to text — a TextColor, a TextDecoration, a whole Style, anything StyleBuilderApplicable — carries no characters of its own. Such a value styles everything that follows it on the same line:

name: "<gray>Rank: %rank_color%%rank_name%"

%rank_color% renders nothing itself and hands its colour to %rank_name% and any text after it. Reach is the rest of that line only — the next lore line starts clean.

Two rules govern what wins:

  • Style set explicitly on the text that follows still wins. %rank_color%<bold>Name</bold> keeps the bold.

  • A second style placeholder restyles from its own position onward, overriding the first one from there:

    name: "%a%first %b%second" # "first" takes %a%, "second" takes %b%

A style placeholder with nothing after it — or one that is the entire value of the field — renders as nothing at all, which is the expected no-op.

Resolution Order

When a placeholder is parsed the namespace token is looked up in this order:

  1. Layout-instance context (when inside an item rendered by a layout)

  2. layouts — per-menu layout instances

  3. variables — per-menu local variables, falling back to global variables from config.yml

  4. parameters — parameters passed into the open call

  5. viewer/player — the player viewing the menu/dialog

  6. server — server-scoped lookups

  7. numberFormat — number formatting helper

  8. PlaceholderAPI fallback for any unmatched key

Nested bodies keep the surrounding scope. Inside a dialog, an item's item-stack body resolves the dialog's own %parameters_*% and %viewer_*%/%player_*% scope, and every lore line produced by an iterable lore modifier keeps the outer scope on each iteration — a nested lookup no longer falls back to the root container. The dialog container's context is the viewing player, so %server_*% keys and lore modifiers that test for a player context work inside dialogs too. See Dialog YAML reference.

player_*

Properties of the player who has the menu/dialog open. Any key not listed below is forwarded to PlaceholderAPI as %player_<remaining>%, so PAPI expansions still work.

Placeholder

Description

%player_name%

In-game name

%player_uuid%

UUID string

%player_ping%

Network latency in milliseconds

%player_health%

Current health (double)

%player_health_max%

Maximum health attribute

%player_health_rounded%

Math.round(health)

%player_displayName%

Display name as an Adventure component (renders with its formatting)

%player_head%

Inline player-head glyph for the viewer, as an Adventure object component (ObjectContents.playerHead, resolved by name). Renders inside text — it is not an ItemStack

%player_level%

Experience level (int)

%player_exp%

Progress to the next level, 0.0–1.0 (float — not total XP)

%player_food%

Food level, 0–20 (int)

%player_saturation%

Saturation level (float)

%player_location%

The player's location — requires a sub-key (see Location and world sub-keys)

%player_world%

The player's world — requires a sub-key (see Location and world sub-keys)

%player_equipment_HEAD%

Helmet (resolved as an item placeholder, see below)

%player_equipment_CHEST%

Chestplate

%player_equipment_LEGS%

Leggings

%player_equipment_FEET%

Boots

%player_equipment_HAND%

Main-hand item

%player_equipment_OFF_HAND%

Off-hand item

The %player_*% namespace is also exposed under the alias %viewer_*% inside dialogs, so %viewer_head% is the same glyph as %player_head%.

%player_head% belongs in a component field — an item name:, a lore line, a message string — never in item-stack.material:

name: "%player_head% <gold>%player_name%"

head is matched before the PlaceholderAPI fallback, so it never reaches PAPI. Note the collision with the looked-up player placeholder: %server_player_<name>_head% resolves through MinecraftPlayerPlaceholder, whose own head key returns an ItemStack PLAYER_HEAD carrying the stored texture profile — an item, not an inline glyph.

Equipment sub-keys

The equipment placeholder returns an ItemStackPlaceholder instance, so the remaining tokens drill into the item:

Sub-key

Description

material

The item's Material — continues into the material sub-keys

name

The custom display name, else the item name, else the translated material name

hover-name

The stack's full hover display name (ItemStack#displayName())

lore

The lore lines as a list, or an empty list

amount

Stack size (int)

custom-model-data

Custom model data, or 0 when unset

durability

Remaining durability (max-damage - damage); 0 for unbreakable or non-damageable items

damage

Damage taken; 0 for unbreakable or non-damageable items

max-damage

Maximum damage the item can take; 0 when not damageable

unbreakable

true when the item carries the unbreakable flag

enchantment_<key>_…

One named enchantment — continues into the enchantment sub-keys below

enchantments

The item's enchantments as an ItemProvider

stored-enchantments

The enchantments stored on an enchanted book, as an ItemProvider

An unrecognised sub-key yields the ItemStack itself.

lore: - "<gray>Helmet: %player_equipment_HEAD_name%" - "<gray>Material: %player_equipment_HEAD_material%" - "<gray>Durability: %player_equipment_HEAD_durability%/%player_equipment_HEAD_max-damage%"

enchantments and stored-enchantments return an ItemProvider, so they take the usual provider sub-keys — _size, _max-index, _at_<index>, _list — and each element is an enchantment placeholder. stored-enchantments is empty for anything that is not an enchanted book, and enchantments is empty (not null) for an unenchanted item.

lore: - "<gray>Enchantments: %item_enchantments_size%" - "$map(%item_enchantments_list%, <gray>· %parameter_entry_name%)"

Enchantment sub-keys

%item_enchantment_<key>_…% resolves a single enchantment on the stack. The enchantment key is read greedily: arguments are consumed until one of them is a known sub-key, so a multi-word key such as silk_touch works without escaping — %item_enchantment_silk_touch_level% reads silk_touch, then level. An unknown or malformed key raises an error rather than resolving to nothing.

Sub-key

Description

key

The enchantment's namespaced key

name

The enchantment's translated name. name_<level> renders the display name with that level (e.g. Sharpness V)

description

The enchantment's description component

level

The level on this item — 0 when the item does not have the enchantment, never null. Available only on an enchantment read from an item

weight

Registry weight used by the enchanting table

max-level

Highest level obtainable in vanilla

start-level

Lowest level of the enchantment

is-curse

true for curses

is-tradeable

true when villagers may trade it

is-discoverable

true when it can appear from enchanting/loot

anvil-cost

Vanilla anvil cost of the enchantment

Because level is 0 rather than null for a missing enchantment, guard on it explicitly instead of relying on the line disappearing:

lore: - "<gray>Sharpness $roman-number(%item_enchantment_sharpness_level%)"

A bare %item_enchantment_<key>% with no sub-key yields the enchantment object itself, which is what an Enchantment argument on an action, requirement or function expects — the enchantment adapter unwraps it.

Material sub-keys

%…_material% returns a MaterialPlaceholder; a bare token yields the Bukkit Material itself.

Sub-key

Description

name

The Material constant name (e.g. DIAMOND_SWORD)

is-block

true when the material can be placed as a block

is-item

true when the material can exist as an item

is-edible

true for food

is-record

true for music discs

is-solid

true when the block is solid

is-occluding

true when the block fully blocks light

is-burnable

true when the block can burn away

is-flammable

true when the block can catch fire

Any other sub-key resolves to null.

Location, world, block and entity sub-keys

%player_location% and %player_world% return chaining objects — a bare %player_location% or %player_world% is not useful on its own; append a sub-key to drill in. The same applies to the block and entity objects documented below.

location sub-keys (also reachable via %player_location_world_*%):

Placeholder

Description

%player_location_x%

X coordinate (double)

%player_location_y%

Y coordinate (double)

%player_location_z%

Z coordinate (double)

%player_location_world_<key>%

The location's world — continues into the world sub-keys below

world sub-keys (reached via %player_world_*% or %player_location_world_*%):

Placeholder

Description

%player_world_name%

World name

%player_world_environment%

NORMAL/NETHER/THE_END/CUSTOM

%player_world_seed%

World seed (long)

%player_world_time%

Day time, 0–24000 (long)

%player_world_fullTime%

Absolute world time (long)

%player_world_difficulty%

PEACEFUL/EASY/NORMAL/HARD

lore: - "<gray>Position: %player_location_x%, %player_location_y%, %player_location_z%" - "<gray>World: %player_world_name% (%player_world_environment%)"

block sub-keys:

Placeholder

Description

%<key>_type%

The block's Material

%<key>_location_<key>%

The block's location — continues into the location sub-keys above

%<key>_world_<key>%

The block's world — continues into the world sub-keys above

entity sub-keys:

Placeholder

Description

%<key>_id%

Entity UUID (getUniqueId())

%<key>_name%

Entity name

%<key>_type%

The EntityType

%<key>_location_<key>%

The entity's location — continues into the location sub-keys above

%<key>_world_<key>%

The entity's world — continues into the world sub-keys above

%<key>_alive%

true while the entity is valid (isValid() — loaded and not removed), not merely not-dead

%<key>_dead%

true when the entity is dead (isDead())

A bare token with no sub-key yields the Bukkit Block/Entity object itself, which is useful only where an adapter unwraps it (a block: or entity: argument). Any other sub-key resolves to null.

stacksuppliers_*

Resolves an item through a registered ItemStackSupplier and returns it as an item placeholder, so the item sub-keys above apply:

%stacksuppliers_<supplier>_<id>[_<item sub-key>…]%

The supplier namespace is the first argument (vanilla, hdb, ce, or any custom one). Everything after it is consumed greedily as the item id until an argument is a known item sub-key, so an id containing underscores needs no escaping — %stacksuppliers_ce_magic_sword_name% asks the ce supplier for magic_sword, then reads name.

lore: - "<gray>Reward: %stacksuppliers_vanilla_diamond_sword_name%"

Unlike most placeholders this one throws rather than resolving to nothing: an unknown supplier namespace, an empty id, or an id the supplier cannot resolve each raise an error that fails the whole string's resolution. Only reference ids you know exist.

server_*

Server-side queries. Unmatched keys fall through to PlaceholderAPI as %server_<remaining>%.

Placeholder

Description

%server_players%

List of online players (use inside a layout provider)

%server_online%

Online player count

%server_online_<uuid-or-name>%

true if the player is online, false otherwise

%server_player_<uuid-or-name>%

Returns the matching player (use as base for further player_* lookups)

%server_currentTimeMillis%

Current wall-clock time as epoch milliseconds (System.currentTimeMillis())

%server_currentTimeSeconds%

Current wall-clock time as epoch seconds

The two time keys pair naturally with $format-date(...)/%player_timestamp_*% for building countdowns and "time since" displays.

parameters_*

Parameters passed when the menu or dialog was opened.

menus.computeAndOpen(player, "shop", Map.of("tab", "weapons", "page", 2));
title: "<bold>Shop — %parameters_tab%" lore: - "<gray>Page %parameters_page%"

Inside dialogs the same namespace exposes the parameters passed to dialogs().openMenu(...).

variables_*

Mutable per-menu state declared under variables: and updated via set-variable. If a key is not found in the menu's variables, the lookup falls through to the global variables: map defined in config.yml, which means the same namespace covers both local and global variables.

variables: mode: "buy"
name: "<yellow>Mode: %variables_mode%"

Global variable example (from config.yml):

variables: server_display_name: "AstralRealms"
lore: - "<gray>Network: %variables_server_display_name%"

layouts_* and layout-instance keys

%layouts_<id>% returns the live layout instance for <id>. Its keys are then accessed via the standard nested chaining.

Placeholder

Description

%layouts_<id>_id%

Layout identifier

%layouts_<id>_page%

Current page index (0-based)

%layouts_<id>_maxPages%

Total number of pages

%layouts_<id>_hasNextPage%

true when a next page exists

%layouts_<id>_hasPreviousPage%

true when a previous page exists

items: next-page-button: slot: 53 item-stack: material: ARROW name: "<green>Page %layouts_items_page% / $e(%layouts_items_maxPages%-1)" view-requirements: - "[compare] %layouts_items_hasNextPage% == true" actions: LEFT: - "[next-page] items"

numberFormat_*

Formats the resolved value as a localised number. The remaining tokens form the placeholder key whose value should be formatted; dashes (-) in the remaining tokens are converted back to underscores before lookup, so keys with their own underscores can be chained without breaking the parser.

Output uses a space as a thousands separator and up to 2 decimal places.

lore: - "<gray>Balance: %numberFormat_parameters_balance%" - "<gray>Total: %numberFormat_{variables_total}%"

Input

Output

1234567

1 234 567

1234.5678

1 234.57

input_* (dialogs only)

Inside the action list of a dialog button, each input's current value is bound as %input_<input-id>%. The type matches the input:

Input type

Value type

text

String

boolean

Boolean ("true"/"false")

slider

Float

options

String key of the selected option

yes-button: label: "Confirm" actions: - "[console] rename %player_name% %input_username%"

PlaceholderAPI Fallback

When a key is not handled by any registered AstralCore placeholder, the entire %...% string is passed to PlaceholderAPI. All standard PAPI expansions (%vault_eco_balance%, %luckperms_prefix%, etc.) work inside menus and dialogs without any extra configuration.

lore: - "<gold>Coins: %vault_eco_balance%" - "<gray>Rank: %luckperms_prefix%"

AstralCore's own PAPI expansion (%player_*%)

AstralCore registers a PlaceholderAPI expansion under the identifier player. These keys are served to any PAPI consumer — chat formats, scoreboards, holograms, other plugins — and are backed by AstralCore's stored MinecraftPlayer record rather than by Bukkit.

Keys served from the stored record (both the online and the offline request path):

Placeholder

Description

%player_fist_joined%

Human-readable time since first login (note the spelling — the source key is fist_joined)

%player_last_login%

Human-readable time since the last login

%player_last_seen%

Human-readable time since the player was last observed connected. Left unresolved when never recorded

%player_raw-last_seen%

Last-seen timestamp as raw epoch milliseconds

%player_playtime%

Human-readable total playtime

%player_raw-playtime%

Total playtime in seconds (online request path only)

Keys served from the live player (online request path):

Placeholder

Description

%player_health%· %player_max_health%

Current / maximum health

%player_saturation%

Saturation level

%player_exp%· %player_level%

Progress to the next level / experience level

%player_gamemode%

Game mode name

%player_world%

Current world name

%player_ping%

Network latency in milliseconds

%player_ip%

Connection host string, or unknown

%player_online%

true/false

%player_locale%

Locale as a language tag

%player_locale_language%· %player_locale_country%· %player_locale_variant%· %player_locale_name%

Locale parts / display name

%player_name%· %player_uuid%· %player_display_name%

Identity

%player_x%· %player_y%· %player_z%

Block coordinates (int)

%player_yaw%· %player_pitch%

View angles (float)

%player_max_air%

Maximum air ticks

Keys served only on the offline request path (an online player is routed to the table above):

Placeholder

Description

%player_first_played%· %player_last_played%

Bukkit's own OfflinePlayer timestamps (epoch millis)

%player_server%· %player_server_name%

Name of the server the player is registered on

%player_server_id%

Server unique id

%player_server_group%

Server group

Playtime and last-seen semantics

  • %player_playtime%/%player_raw-playtime% use effectivePlaytime(): the session in progress is added only while the player is actually online. The persisted value settles on quit. An offline player's playtime is the stored value, with no live delta added.

  • %player_last_login% is the time since the actual login — the login stamp is no longer rewritten on quit, so it no longer means "time since last logout".

  • %player_last_seen%/%player_raw-last_seen% carry the "last observed connected" meaning. The timestamp is stamped on login and on quit, so for a player who is online right now last_seen is the start of their current session.

  • When the timestamp was never recorded (records predating the column read back as -1), the expansion returns null for %player_last_seen%. That is PlaceholderAPI's "not handled" signal, so the raw %player_last_seen% text is left in place rather than rendering as an empty string.

Proxy-side player_* (Velocity)

On the proxy, the player namespace is a different placeholder backed by the stored MinecraftPlayer record. It deliberately does not mirror the Paper %player_*% namespace — only name and online overlap. Any key not listed resolves to null.

Placeholder

Description

%player_id%

UUID

%player_name%

Stored name

%player_firstLogin%

First login as raw epoch milliseconds

%player_lastLogin%

Last login as raw epoch milliseconds

%player_lastSeen%

Last time observed connected, as raw epoch milliseconds (stamped on login as well as quit)

%player_playtime%

Effective playtime in milliseconds — includes the session in progress while online

%player_texture%

Stored skin texture value

%player_online%

true/false

A bare %player% with no sub-key yields the MinecraftPlayer object itself.

Last modified: 03 September 2026