Skip to content

Placeholders

Petrus Pradella edited this page Jul 27, 2026 · 2 revisions

Placeholders

What this page covers: the placeholder engine - Replacer / RegexReplacer and the Manipulator extension - plus how it bridges to PlaceholderAPI. A RegexReplacer<O> scans a string for closures (${key}, %key% or {key}), looks each one up against parsers you registered, and substitutes the result.

There is one such engine in the project. The ${key} placeholders of a Localization message are this same RegexReplacer, bound to Closures.DOLLAR_CURLY and to a RenderContext - what changes between the two pages is only the object O the engine resolves against.


The 30-second version

import br.com.finalcraft.evernifecore.placeholder.replacer.RegexReplacer;

// A replacer bound to some object type O (here: a Guild).
RegexReplacer<Guild> guildReplacer = new RegexReplacer<Guild>()
        .addParser("guild_name",    Guild::getName)
        .addParser("guild_members", guild -> guild.getMembers().size())
        .addParser("guild_level",   Guild::getLevel);

String line = guildReplacer.apply("&e%guild_name% &7(lvl %guild_level%, %guild_members% members)", guild);
// -> "&eDragons (lvl 4, 12 members)"

Each %key% in the text is resolved by the parser registered under key, passing your object. A placeholder with no matching parser is left untouched.


Closures: which delimiters the engine reads

A closure is the delimiter pair the engine looks for. Three are built in:

Closure Delimiters Used for
DOLLAR_CURLY ${key} EverNifeCore's canonical form - what every message resolves.
PERCENT %key% PlaceholderAPI pass-through, and the default of a bare new RegexReplacer<>().
BRACKET {key} Manipulator templates (see Manipulators).

Pick one when you build the replacer:

RegexReplacer<Guild> canonical = new RegexReplacer<>(Closures.DOLLAR_CURLY)
        .addParser("guild_name", Guild::getName);
canonical.apply("Welcome to ${guild_name}", guild);  // -> "Welcome to Dragons"

RegexReplacer<Guild> braces = new RegexReplacer<>(Closures.BRACKET)
        .addParser("guild_name", Guild::getName);
braces.apply("Welcome to {guild_name}", guild);      // -> "Welcome to Dragons"

A replacer always knows which delimiters it speaks - getClosures() gives them back, and closures.quote("guild_name") writes the token the way that replacer would match it. Key lookup is case-insensitive; two keys differing only in case throw at registration rather than silently shadowing each other.

A candidate that no parser answers for is left untouched, and scanning resumes just after its opening delimiter - so in "100% of %total%" the stray % pairing with the next one does not swallow %total%, which still gets its turn.

apply(List<String>, O) runs the same substitution over a whole list (e.g. an item lore).


Built-in replacers

FCRegexReplacers ships ready-made replacers for the common player types, so you rarely start from scratch:

import br.com.finalcraft.evernifecore.placeholder.FCRegexReplacers;

String s = FCRegexReplacers.PLAYER_DATA.apply("&a%player% &7last seen %player_last_seen%", playerData);

FCRegexReplacers.PLAYER_DATA (an RegexReplacer<IPlayerData>) provides:

Placeholder Value
%player% / %player_name% Player name
%player_uuid% Player UUID
%player_is_online% Whether the player is online
%player_ontime% Formatted play time
%player_last_seen% / %player_last_seen_millis% Last-seen timestamp
%player_first_seen% / %player_first_seen_millis% First-seen timestamp

FCRegexReplacers.PLAYER (an RegexReplacer<FPlayer>) provides %player%, %player_name%, %player_uuid%, %player_isonline%.


Registering parsers

RegexReplacer implements a small provider API. The parser you register receives your object and returns the value (any type - it's stringified):

replacer.addParser("key", obj -> obj.something());                 // dynamic value
replacer.addParser("key", "static description", obj -> obj.x());   // + a description (docs/help)
replacer.addParser("static_key", "literal value");                 // constant value
replacer.addParser(new String[]{"hp", "health"}, obj -> obj.hp()); // several aliases -> one parser

A default parser handles any closure that no named parser matched - useful for a computed family of keys:

replacer.setDefaultParser((obj, key) -> key.startsWith("stat_")
        ? obj.getStat(key.substring("stat_".length()))
        : null);   // return null to leave the placeholder untouched

Listing what a replacer answers for

describeAll() returns every registered key mapped to its description ("" when undescribed), in registration order. This is public contract: it is what an integrating plugin reads to show a user which placeholders it can offer, so a key registered without a description still shows up.

replacer.describeAll().forEach((key, description) ->
        sender.sendMessage("&e" + key + " &7- " + description));

A Localization message exposes the same thing for its own declarations through getPlaceholderProvider().describeAll().


Combining replacers - CompoundReplacer

A single message often needs several data sources (the player, a guild, a PlaceholderAPI user). A CompoundReplacer chains multiple RegexReplacers, each bound to its own object, and applies them in order:

import br.com.finalcraft.evernifecore.placeholder.replacer.CompoundReplacer;

CompoundReplacer compound = FCRegexReplacers.PLAYER_DATA.compound(playerData) // replacer + its object
        .appendReplacer(guildReplacer, guild)
        .usePAPI(fplayer);                                                    // optional, see below

String result = compound.apply("&a%player% of &e%guild_name%: %vault_eco_balance%");

A CompoundReplacer is what FancyText and Localization messages accept via addReplacer(compoundReplacer) - so you can push a fully-composed set of placeholders into a message in one call. It runs after the message's own ${key} pass, on the same payloads (text, hover and click value alike).


Manipulators - parametric placeholders

For placeholders that carry an argument inside the token - %top_kills_1%, %balance_of_Alice% - use a manipulator. You give a template with a {closure} where the argument sits, and read the captured value from the context:

guildReplacer.addManipulator("member_rank_{memberName}", (guild, ctx) -> {
    String memberName = ctx.getString("{memberName}");  // the captured argument
    return guild.rankOf(memberName);
});

guildReplacer.apply("&7Rank: %member_rank_Alice%", guild); // -> "&7Rank: Officer"

ctx.getString("{closure}") returns the captured text; ctx.getArgumento("{closure}") wraps it as an Argumento for typed access. A template may contain multiple closures (kills_{world}_{player}).

📌 Note - manipulators are an advanced tool for a family of placeholders keyed by an argument. For a fixed set of keys, plain addParser(...) is simpler and faster.


PlaceholderAPI integration

Two independent directions, both routed through the Platform Abstraction (IPlatform):

Consuming other plugins' %papi% placeholders inside your text - add a PAPI user to a CompoundReplacer, or parse directly:

compound.usePAPI(fplayer);                                   // PAPI runs after your own replacers
// or, one-off:
String parsed = EverNifeCore.getPlatform().parse(fplayer, "&aBalance: %vault_eco_balance%");
boolean papiHere = EverNifeCore.getPlatform().isPAPIPresent();

Publishing your own data so other plugins can read it through PAPI - register an integration under a base identifier; it returns a RegexReplacer you populate, and each parser becomes a %<baseId>_<key>% PlaceholderAPI placeholder:

RegexReplacer<PlayerData> papi = EverNifeCore.getPlatform()
        .createPlaceholderIntegration(ecPluginData, "myplugin", PlayerData.class);

papi.addParser("coins", pd -> pd.getPDSection(Profile.class).join().coins);
// other plugins can now use %myplugin_coins%

The integration auto-detects legacy vs modern PlaceholderAPI and registers the hook accordingly; you just add parsers.


See also

  • Localization - @FCLocale messages, their ${key} declarations, and addReplacer(...).
  • FancyText - addPlaceholder/addParser/addReplacer on rich text.
  • Platform Abstraction - IPlatform.parse / isPAPIPresent / createPlaceholderIntegration.
  • Integrations - PlaceholderAPI and the other third-party hooks EverNifeCore ships with.

Clone this wiki locally