Astral Realms Documentation Help

Transformers

A transformer is a named, reusable piece of rendering logic loaded from a plugin's transformers/ folder and called from any string that supports placeholders with $apply-transformer(<id>).

Where a placeholder returns a value and a function computes one, a transformer produces presentation — one line, several lines, or none at all — out of a small declarative block: pick a branch, bucket a number, draw a progress bar, wrap a paragraph, repeat a template once per entry of a data provider.

Transformers replace the per-item lore-modifiers block, which is deprecated. A transformer is written once and named from every item, menu and dialog that needs it, instead of being copied into each item-stack.

Declaring a transformer

Every Astral plugin loads plugins/<Plugin>/transformers/ on startup and on reload — AstralCore and AstralItems both do. The folder is created if it does not exist, is walked recursively, and every .yml/.yaml file below it is read as one transformer.

# plugins/AstralCore/transformers/listing-status.yml id: "listing-status" global: true transformer: type: "switch" value: "%aah_listing_status%" cases: LISTED: "<green>On sale" EXPIRED: "<red>Expired" default: "<gray>Unknown"

Field

Type

Required

Description

id

String

Yes

The name $apply-transformer(...) looks up. Must be unique within its scope.

global

boolean

Yes

true registers it in the shared registry, so any plugin can name it. false keeps it visible only to the owning plugin's own configuration.

transformer

object

Yes

The transformer itself. Its type key selects one of the types below.

transformer.type

String

Yes

Type id. An unknown value fails the file with Unknown Transformer type: <id>.

A file whose type is missing or unknown fails to load and is logged; the rest of the folder still loads. An id that is already registered — in the plugin's own registry or globally — is skipped with a Transformer with name '<id>' already exists, skipping... warning, so the first one loaded wins.

Scope and lookup

A call resolves against the registry of the plugin whose configuration the call was written in, which falls through to the global registry:

  1. The owning plugin's own transformers (global: false).

  2. The globally registered transformers (global: true, from any plugin).

The owner is captured while the configuration file is deserialized, not when the line is rendered — so a lore line in an AstralItems blueprint keeps resolving against AstralItems' transformers no matter which plugin's code triggers the render. A string built outside a configuration read has no owner and therefore sees the global registry only.

Lookup is an exact, case-sensitive match. An unknown id fails the surrounding string with Transformer not found: <id>.

Calling a transformer

lore: - "$apply-transformer(listing-status)"

What a transformer returns decides what the line becomes:

Returned

Result on a lore line

A single component

The line renders as that component.

A list of components

The line is expanded into one line per element — see List-valued placeholders.

An empty list

The line is removed entirely.

Because a one-line result is a component and not a one-element list, the same transformer can also be named from a field that only accepts a single value — an item name, a menu title.

Transformer types

Type

Purpose

inline

Render one wrapped value or function call.

inline-conditional

Alias of inline.

conditional

Render the first group whose requirements pass.

switch

Look the rendered value up in a map of cases.

range

Pick the first [min, max] bucket containing a number.

gauge

Draw a row of slots — a page indicator, a pip row, a progress bar.

wrap

Break one run of text into as many lines as it needs.

list

Render one line per entry of a data provider.

list-conditional

As list, with prioritised groups evaluated per entry.

inline, switch, range, gauge and wrap are platform-independent; conditional, list and list-conditional are registered by the Paper module because they evaluate requirements and read Paper data providers.

inline

Renders a single wrapped value and returns whatever it resolves to. Useful to give a long expression or nested function call a name.

Field

Type

Required

Description

function

String (placeholder)

Yes

The value to resolve. Placeholders, $e(...) and $fn(...) all work.

id: "net-price" global: true transformer: type: "inline" function: "$format-number($e(%aah_listing_price% * 1.05))"

inline-conditional is registered as a second name for the same type; the two are interchangeable.

conditional

Renders the first group whose requirements pass, or fallback when none do.

Field

Type

Required

Description

groups

Map<String, Group>

Yes

Named candidate branches.

groups.<name>.priority

int

No

Higher is tested first. Default 0.

groups.<name>.function

String \|List<String>

Yes

The branch content — one line or several.

groups.<name>.requirements

List<Requirement>

No

Guards — see Requirements. Omitted, or an empty list, always passes.

fallback

String \|List<String>

No

Rendered when no group passes. Omitted means nothing is rendered and the line disappears.

Groups are sorted by descending priority once, at load time; groups sharing a priority keep the order they were written in. A requirement that throws is logged to the console and its group is skipped, so evaluation continues with the next one.

id: "rank-line" global: true transformer: type: "conditional" groups: hidden: priority: 100 requirements: - "[compare] %parameter_hidden% == true" function: [ ] # highest priority + empty list = render nothing vip: priority: 10 requirements: - "[compare] %player_group% == vip" function: - "<gold>VIP member" - "<gray>Bonus: <yellow>+25%" fallback: "<gray>Standard member"

switch

Looks the rendered value up in cases. This is the shape a conditional takes when every group compares the same placeholder to a literal, at the cost of one placeholder resolution and one hash lookup instead of a requirement walk per group.

Field

Type

Required

Description

value

String (placeholder)

Yes

The value to look up.

cases

Map<String, String \| List<String>>

Yes

Case key → branch content.

default

String \|List<String>

No

Rendered on a miss. Omitted means the line disappears.

Matching is textual and case-sensitive. An enum-valued placeholder renders to its constant name, so EXPIRED matches; booleans work with "true"/"false" keys (quote them — YAML would otherwise read them as booleans).

id: "listing-status" global: true transformer: type: "switch" value: "%aah_listing_status%" cases: LISTED: "<green>On sale" SOLD: "<yellow>Sold" EXPIRED: - "<red>Expired" - "<gray>Collect it from your mailbox." default: "<gray>Unknown"

range

The numeric counterpart of switch: picks the first group whose [min, max] range contains value.

Field

Type

Required

Description

value

Number (placeholder)

Yes

The number to bucket.

groups

Map<String, Group>

Yes

Named buckets, tested in written order.

groups.<name>.min

Number (placeholder)

Yes

Lower bound, inclusive.

groups.<name>.max

Number (placeholder)

Yes

Upper bound, inclusive.

groups.<name>.function

String \|List<String>

Yes

The branch content.

fallback

String \|List<String>

No

Rendered when no bucket matches.

Groups are tested in the order they are written — where ranges overlap the earlier one wins, so order the narrow buckets first. A group whose min or max resolves to nothing is skipped. A value that resolves to nothing, or that no bucket covers, renders fallback; with no fallback the line disappears rather than rendering null.

id: "durability-bar" global: true transformer: type: "range" value: "%instance_durability_percent%" groups: low: min: 0 max: 33 function: "<red>Worn" mid: min: 34 max: 66 function: "<yellow>Used" high: min: 67 max: 100 function: "<green>Pristine"

gauge

Renders count slots on a single line, either marking one of them or filling up to one — a page indicator, a stat pip row, a progress bar. Returns one component.

Field

Type

Required

Description

count

int (placeholder)

Yes

Number of slots. A value <= 0 renders an empty list, so the line disappears.

index

Number (placeholder)

Yes

In marker mode, the 1-based slot to mark. In fill mode, the progress value.

mode

marker \|fill

No

Default marker.

max

Number (placeholder)

No

fill only — the scale index is measured against, so a percentage or an XP total maps onto the slots without arithmetic at the call site.

filled

String

Yes

The marked/filled slot glyph.

empty

String

Yes

The unmarked/unfilled slot glyph.

separator

String

No

Inserted between slots. Default: nothing.

prefix

String

No

Prepended to the line.

suffix

String

No

Appended to the line.

  • marker marks the single slot at index, counted from 1 so it reads like "page 2 of 3". An index outside 1..count marks nothing.

  • fill fills every slot up to index. Filling is floored, so a bar only reads as full once the value actually reaches the top; without max, index is the slot count itself, and with max the ratio is scaled onto the slots and clamped at both ends. A max of 0, or one that resolves to nothing, renders an empty gauge rather than dividing by it.

# "@ . ." / ". @ ." / ". . @" id: "page-indicator" global: true transformer: type: "gauge" count: "%layouts_items_maxPages%" index: "$increment(%layouts_items_page%)" # page is 0-based, marker is 1-based filled: "<gold>@" empty: "<dark_gray>." separator: " "
# "###--" at 60 XP out of 100 id: "xp-bar" global: true transformer: type: "gauge" mode: "fill" count: 5 index: "%player_exp%" max: 100 filled: "<green>#" empty: "<dark_gray>-"

wrap

Breaks one run of text into as many lines as it needs, so a description is written as prose instead of being hand-wrapped and re-flowed after every edit. Returns a list of lines.

Field

Type

Required

Description

content

String

Yes

The text to wrap. MiniMessage and placeholders are resolved first.

width

int (placeholder)

Yes

Line width, in units.

unit

chars \|pixels

No

Default chars. pixels measures with Minecraft's default-font advances (bold included).

prefix

String

No

Prepended to every produced line and counted against width — the leading indent of a lore paragraph.

Wrapping happens on the rendered component, so styling written mid-sentence survives the break: a colour opened before a break is still in effect on the next line. Consequences worth knowing:

  • Put the paragraph's base styling at the front of content, not in prefix. The prefix is appended as a sibling and a sibling's style does not carry to the text after it.

  • A word longer than the whole width overflows on a line of its own rather than being cut.

  • pixels is approximate outside printable ASCII. Accented Latin, symbols and resource-pack glyphs (%img_*%) fall back to a typical glyph width.

  • A prefix at least as wide as width leaves one unit of room, so each word lands on its own line instead of looping.

id: "flavour-text" global: true transformer: type: "wrap" width: 180 unit: "pixels" prefix: "<dark_gray>| " content: "<gray><italic>%instance_blueprint_description%"

list

Renders a single line template once per entry of a data provider, returning one line per entry. This is the replacement for the iterable lore modifier.

Field

Type

Required

Description

provider

String (placeholder)

Yes

Resolves to the data source — same contract as a layout's provider, see Data Providers. A null result makes the call resolve to nothing.

function

String

Yes

The line template, rendered once per entry.

maximum-entries

int (placeholder)

No

Caps how many entries are examined. Only applied when greater than 0; 0 or omitted means every entry.

requirement

List<Requirement>

No

Per-entry filter. An entry whose requirements fail emits no line.

Inside function (and inside requirement, which runs after the entry placeholders are injected):

Placeholder

Description

%parameter_index%

Zero-based index of the entry.

%parameter_position%

1-based position (index + 1).

%parameter_<namespace>_…%

The entry itself, under the namespace its provider declares.

The surrounding container is wrapped, so %player_…%, %variables_…%, %parameters_…% and every global namespace still resolve. Inherited parameter values are merged in after the entry keys and therefore win on a collision — inside a layout slot, %parameter_index% reports the layout's index and %parameter_position% is the reliable per-line counter.

id: "crate-contents" global: true transformer: type: "list" provider: "%parameters_rewards%" maximum-entries: 5 requirement: - "[compare] %parameter_reward_hidden% == false" function: "<gray>%parameter_position%. <white>%parameter_reward_name%"

list-conditional

As list, but each entry is rendered through a set of prioritised groups instead of a single template.

Field

Type

Required

Description

provider

String (placeholder)

Yes

The data source — see Data Providers.

groups

Map<String, Group>

Yes

Named line templates, evaluated per entry.

groups.<name>.priority

int

No

Higher is evaluated first; ties keep written order. Default 0.

groups.<name>.function

String

Yes

The line emitted when this group passes.

groups.<name>.requirements

List<Requirement>

No

Guards for this group, evaluated against the entry. Omitted means the group always emits.

maximum-entries

int (placeholder)

No

Caps how many entries are examined. 0 or omitted means every entry.

filter

List<Requirement>

No

Per-entry filter, evaluated before the groups. A failing entry emits nothing.

Group and filter requirements see the same per-entry placeholders as list, and a requirement that throws skips that group (or, for filter, that entry).

id: "auction-entries" global: true transformer: type: "list-conditional" provider: "%parameters_listings%" maximum-entries: 10 filter: - "[compare] %parameter_listing_cancelled% == false" groups: header: priority: 20 function: "<white>%parameter_position%. %parameter_listing_item%" sold: priority: 10 requirements: - "[compare] %parameter_listing_status% == SOLD" function: "<gray> Sold for <gold>%parameter_listing_price%"

Registering a custom type

A transformer type is a @ConfigSerializable class implementing Transformer, registered by class against the id configuration files select it with:

@ConfigSerializable public record ShoutTransformer(PlaceholderWrapper<String> value) implements Transformer { @Override public Object transform(Function<String, Object> parser) { String text = this.value.get(parser); // A component, a List<Component>, or List.of() to remove the line. return text == null ? List.of() : Component.text(text.toUpperCase(Locale.ROOT)); } }
// During onEnable, before configuration is loaded. TransformerTypeRegistry.register("shout", ShoutTransformer.class);

Member

Behaviour

TransformerTypeRegistry.register(String, Class<? extends Transformer>)

Makes a type selectable as type: <name> in any transformers/ file. Registering the same class under two names gives it an alias — this is how inline-conditional exists.

Transformer#transform(Function<String, Object> parser)

Runs one render. Return a Component, a List<Component>, or List.of().

Transformer.render(ComponentWrapperList, parser)

Collapses a branch to what the caller expects: one line → a component, several → a list, missing or empty → List.of(). Use it for anything that reads like a group, case or fallback.

TransformerRegistry.get()

The shared registry. registerGlobally(name, transformer) adds an instance built in code rather than loaded from a file.

AstralMCPlugin#transformers()

The plugin's TransformerContainer. registry() is its plugin-scoped registry, which falls through to the global one; load() re-reads the transformers/ folder.

Call transformers().load() from your plugin's configuration-loading step — after the type registry is populated, since a file naming an unregistered type fails to load.

See also

  • Functions — the $apply-transformer(...) call.

  • Requirements — the requirement forms accepted by conditional, list and list-conditional.

  • Menu Layouts — provider types accepted by list and list-conditional.

  • Lore Modifiers — the deprecated per-item block transformers replace.

Last modified: 03 September 2026