Astral Realms Documentation Help

Placeholders

AstralStats registers a single namespace, stats, as a ComplexPlaceholder (StatsPlaceholder) on AstralCore's RootPlaceholderContainer (AstralStats.onEnable). It resolves anywhere AstralCore evaluates a placeholder — menus, dialogs, action arguments, item lore, messages — the same surfaces described in Placeholders.

Every stats_* lookup requires the placeholder context to be a Player:

if (!context.hasNext() || !(context.context() instanceof Player player)) return null;

If the placeholder is resolved without one — no sub-key at all, or a context that isn't a Player — it returns null rather than throwing.

plugin.yml hard-depends on both AstralCore and PlaceholderAPI: Bukkit refuses to enable AstralStats unless PlaceholderAPI is already loaded, which guarantees the PAPI fallback built into AstralCore's own placeholder chain (see PlaceholderAPI Fallback) is available alongside %stats_...% wherever AstralStats' own configs or messages are resolved.

stats_*

Placeholder

Returns

Notes

%stats_stat_<stat-key>%

The stat's current computed value, formatted #.##

<stat-key> is a namespaced Key, e.g. astralstats:attack_damage — see the stat catalog.

%stats_resource_<type>%

The resource's current amount, formatted #.##

<type> is a case-insensitive ResourceType name: health, mana, stamina, shield, charge.

The namespace prefix on <stat-key> is required. Key.key(String) (Adventure) defaults to the minecraft namespace when no : is present, so omitting astralstats: resolves the wrong key instead of failing loudly:

%stats_stat_astralstats:attack_damage% → Key astralstats:attack_damage (matches the shipped stat) %stats_stat_attack_damage% → Key minecraft:attack_damage (won't match, renders -1)

Example — read attack damage and shield into a menu item's lore:

lore: - "<gray>Attack Damage: %stats_stat_astralstats:attack_damage%" - "<gray>Shield: %stats_resource_shield%"

Sub-key routing

StatsPlaceholder.get consumes the first remaining token with context.next() to pick a branch, then rejoins every token still left with context.collapseRemaining() (delimiter _) to rebuild the <stat-key>/<type> argument:

return switch (context.next()) { case "stat" -> { String statName = context.collapseRemaining(); Key statKey = Key.key(statName.replace("-", "_")); yield DECIMAL_FORMAT.format(this.plugin.stats().statValue(player, statKey)); } case "resource" -> { String resourceName = context.collapseRemaining(); ResourceType type = ResourceType.fromName(resourceName); yield DECIMAL_FORMAT.format(this.plugin.resources().resourceValue(player, type)); } case null, default -> null; };

Any first token other than stat or resource (including none) resolves to null. For the stat branch, hyphens anywhere in the collapsed key are converted to underscores before the Key is built — so %stats_stat_astralstats:critical-chance% and %stats_stat_astralstats:critical_chance% resolve the same key.

Resolution defaults and errors

Case

Behavior

<stat-key> has no blueprint loaded for the player

StatService.statValue returns -1.0 → the placeholder renders -1.

<type> is a valid ResourceType name but has no entry loaded for the player (unconfigured in resources.yml, or the player's ResourceContainer isn't loaded)

ResourceService.resourceValue returns 0.0 → the placeholder renders 0.

<type> does not match any ResourceType constant

ResourceType.fromName throws IllegalArgumentException("Unknown resource type: " + name) instead of resolving.

stat sub-key with nothing after it (bare %stats_stat%)

collapseRemaining() returns null; statName.replace("-", "_") then throws a NullPointerException instead of resolving to -1. A bare %stats_resource% doesn't share this crash — it hits the row above via ResourceType.fromName(null).

Formatting

Both branches format their result through a single shared DecimalFormat("#.##") instance — at most two decimal places, no trailing zeros, no thousands separator: 50.0"50", 12.345"12.35", the -1.0 unregistered-stat default → "-1".

Last modified: 25 July 2026