-
Notifications
You must be signed in to change notification settings - Fork 7
FancyText
What this page covers: the rich-text model behind every message EverNifeCore sends.
FancyText is the interface; FancySegment is one styled piece (text + optional hover tooltip +
optional click action) and FancyFormatter is an ordered chain of pieces. Both render to an Adventure
Component, so hover and click work across supported Minecraft versions (and, within what the
platform supports, on Hytale). A resolved Localization message is a FancyText.
import br.com.finalcraft.evernifecore.fancytext.FancyText;
// One piece with a hover tooltip and a click-to-run-command action.
FancyText.of("&aClick me")
.setHover("&7Runs /spawn for you")
.setClickCommand("/spawn")
.send(player);// A multi-piece line (FancyFormatter) - each .append(...) adds a piece.
FancyText.of("&7Player: &f" + name)
.append(" &e[teleport]")
.setHover("&7Teleport to " + name)
.setClickCommand("/tp " + name)
.append(" &c[kick]")
.setHover("&7Kick " + name)
.setClickSuggest("/kick " + name + " ")
.send(sender);send(FCommandSender...) renders and sends; broadcast() sends to the platform's broadcast
audience, console included.
| Type | What it is |
|---|---|
FancyText |
The interface everything is written against, plus the of(...) factories. |
FancySegment |
The leaf: one run of text with at most one hover and at most one click. |
FancyFormatter |
The chain: an ordered list of pieces, rendered as a single component. |
A formatter never contains another formatter: appending one splices its pieces in.
FancyText a = FancyText.of("&aHello"); // one piece
FancyText b = new FancySegment("&aHello"); // same thing, explicit type
FancyText c = FancyText.of("&aHello", "&7a hover tooltip"); // text + hover
FancyText d = FancyText.of("&aClick", "&7hover", "/spawn"); // text + hover + run-command
FancyText e = FancyText.of("&aClick", "&7hover", "https://x.dev", ClickActionType.OPEN_URL);
FancyFormatter chain = FancyFormatter.of("&6=== Menu ==="); // start a chain directly
FancyText block = FancyText.of(Arrays.asList("&7line one", "&7line two")); // one piece per lineThen refine it fluently. The vocabulary is uniform: setX replaces an attribute, addX
accumulates - and LocaleMessage/SendCustom speak exactly the same one, so knowing one is
knowing both.
| Method | Effect |
|---|---|
setText(String) |
Replace the piece's text. |
setHover(String) |
Set the hover tooltip. Replaces whatever hover was there. |
setHover(List<String>) / setHover(String...)
|
Multi-line hover (joined with newlines). |
setHover(FancyHover) |
Set an arbitrary registry-backed hover value (see Pluggable hover). |
setHoverItem(String) |
Show a serialized item as the tooltip - shorthand for setHover(new ItemHover(...)). |
setClick(String, ClickActionType) |
Set click value + type. A piece carries at most one click. |
setClickType(ClickActionType) |
Change only the type, keeping the value. |
setClickCommand(String) |
Click runs the given command. |
setClickSuggest(String) |
Click puts the text in the chat box. |
setClickLink(String) |
Click opens a URL. |
addPlaceholder(String, Object|Supplier|Function<PlayerData,?>) |
Declare ${key} (see Placeholders). |
addPlaceholders(Map<String, ?>) |
Declare several keys at once. |
addParser(String, [String description,] Function<RenderContext,?>) |
Declare a key computed from the whole render context. |
addReplacer(CompoundReplacer) |
Attach a Placeholders replacer on top of the ${key} pass. |
replace(String, String) |
Bake a literal substitution into text/hover/click, right now. Not the placeholder engine. |
bake(UnaryOperator<String>) |
The general form of replace: rewrite every text payload in one pass. |
copy() |
A structurally identical, independent copy. |
isEmpty() |
Whether this would render to nothing at all (decoration on empty text does not count). |
ClickActionType is one of RUN_COMMAND, SUGGEST_COMMAND, OPEN_URL, NONE.
Color codes use & or §. A trailing color on one piece carries into the next (see
Color bleed).
Hover and click are independent - a piece can have either, both, or neither. Each is single-valued:
calling setHover twice leaves only the second tooltip.
FancyText.of("&e[Confirm]")
.setHover("&aClick to confirm the trade")
.setClickCommand("/trade confirm")
.send(player);
// Multi-line hover:
FancyText.of("&6" + item.getName())
.setHover("&7Rarity: &bEpic", "&7Value: &a1200")
.send(player);In a chain, a setter always targets the piece you just appended:
FancyText.of("&7Status: &aOnline") // piece 1 - no hover
.append(" &e(?)") // piece 2
.setHover("&7Last seen just now"); // hover attaches to piece 2 onlyFancyFormatter.last() returns that piece explicitly, for the caller who would rather say so out
loud than rely on the implicit target.
A hover is a FancyHover value, and how it renders comes from FancyHoverRegistry - so a hover kind
is never hardcoded in the render path. Core registers two:
typeId |
Value type | Renders as |
|---|---|---|
text |
TextHover |
A plain tooltip (HoverEvent.showText). |
item |
ItemHover |
An actual item tooltip (HoverEvent.showItem), from an item id or serialized item. |
fancyText.setHoverItem("minecraft:diamond_sword"); // tooltip renders as the item, not as textAn integrating plugin registers its own kind at startup:
FancyHoverRegistry.register(
FancyHoverType.<MyHover>of("myplugin:profile", hover -> HoverEvent.showText(render(hover)))
.withDegrade(hover -> new TextHover(hover.summary())) // used where the platform can't show it
.withCodec(MyHover::payload, MyHover::new)); // lets it survive in lang_XX.ymlTwo things decide what a player actually sees:
-
The platform.
IPlatformChatAdapter.supportsHover(typeId)is consulted per render. On Bukkit every registered kind is supported; on Hytale hover is not part of the server API at all, so hovers are dropped there. -
Degradation. An unsupported kind falls back to its declared
withDegradevalue exactly once - a further degrade is never chased, so two kinds that degrade into each other cannot recurse. No degrade declared means no hover is attached, never an exception.
Registering a different type under an id that is already taken throws; registering the same instance
again (a plugin re-running its own bootstrap) is a harmless no-op. withCodec is what lets a custom
hover round-trip through a plugin's lang_XX.yml; without it the type is rendered but not persisted.
Calling .append(...) on a FancyText produces a FancyFormatter and returns it, so you can keep
chaining:
import br.com.finalcraft.evernifecore.fancytext.FancyFormatter;
FancyFormatter formatter = FancyFormatter.of("&6=== Menu ===");
formatter.appendLine("&e- option A").setHover("&7pick A").setClickCommand("/pick A");
formatter.appendLine("&e- option B").setHover("&7pick B").setClickCommand("/pick B");
formatter.send(sender);append accepts a String, a (text, hover) / (text, hover, runCommand) tuple, or a whole
FancyText/FancyFormatter. appendLine(...) is append with a leading newline, so a caller stops
spelling "\n".
append has one semantics, always: it flattens, and it copies. A formatter appended into another
is spliced in piece by piece, and every piece is copied on the way in. So:
- two chains can never share a piece, and mutating what you appended does not reshape what is already inside;
- to decorate what was just appended, chain on the return value (
formatter.append(x).setHover(...)), which acts on the copy that is actually in the chain; -
copy()is structural, andcopy().equals(original)always holds - which is what keeps the language files from being rewritten on every reload just because two equal messages compared unequal.
A placeholder declared on the chain is visible to every piece in it; a piece that declares the same key shadows it, the way an inner scope shadows an outer one.
FancyFormatter list = FancyText.join("&7, ", players, player -> FancyText.of("&e" + player.getName())
.setHover("&7Click to teleport")
.setClickCommand("/tp " + player.getName()));Each item keeps its own hover and click - which is exactly what joining the rendered strings instead would throw away.
In legacy chat a color code bleeds into the text that follows it; Adventure components, by contrast,
do not inherit a sibling's color. FancyFormatter bridges the two: it carries each piece's
trailing color into the next as its starting color, so "&aHello " followed by "World" renders
World in green, exactly as a legacy string would. You don't configure anything - it's how pieces
are joined.
A message declares the value of ${key}. The key is declared bare and cited with its delimiters
in the text:
FancyText.of("&7#${number}: &a${player} &7(${score})")
.addPlaceholder("number", 1)
.addPlaceholder("player", target.getName())
.addPlaceholder("score", () -> scoreboard.scoreOf(target)) // computed only if cited
.send(sender);-
${key}is case-insensitive, and it is the only closure the message engine reads. - Nothing is computed eagerly: a key the text never cites is never resolved, and a key cited twice is resolved once per render.
- The key must be the bare name -
"score", never"%score%"or"${score}". A key declared with delimiters is registered exactly like that, never matches, and says so once in the console. - Two keys that differ only in case are a collision and throw at registration, rather than silently shadowing each other at lookup.
| Declaration | Resolved from |
|---|---|
addPlaceholder(key, Object) |
A constant. |
addPlaceholder(key, Supplier<?>) |
Computed on demand, no recipient needed. |
addPlaceholder(key, Function<PlayerData,?>) |
The recipient's cached PlayerData. A recipient with none (the console) leaves the token as written. |
addParser(key, [description,] Function<RenderContext,?>) |
The whole render context: recipient, their PlayerData, and the command scope. |
addReplacer(CompoundReplacer) |
A Placeholders replacer, applied after the ${key} pass. |
The description of addParser is public contract: getPlaceholderProvider().describeAll() lists
every key a message answers for, which is how an integrating plugin shows a user what it can offer.
Every message additionally answers for ${label} and ${subcmd} - the command label and
sub-command the player typed. They are lowest precedence, so a message declaring its own label
shadows them.
For the replacer engine itself and PlaceholderAPI bridging, see Placeholders.
fancyText.send(sender); // one or more FCommandSender
fancyText.send(playerA, playerB); // varargs
fancyText.send(listOfSenders); // a List
fancyText.broadcast(); // the broadcast audience, console includedEvery recipient gets their own render, which is what lets one message show each of them their own placeholder values.
Rendering writes nothing back onto the instance: it is a pure function, so sending the same message to two recipients at the same time is safe.
| Method | Returns |
|---|---|
render(String startingColor, RenderContext) |
RenderedText: the component and the color the text ended on. Everything else is written in terms of it. |
toComponent() |
The component with no recipient - a ${key} nobody can answer for stays as written. |
toComponent(RenderContext) |
The component for one recipient, placeholders resolved. |
toLegacyString() / toLegacyString(RenderContext)
|
The rendered §-formatted string, without sending. |
toPlainText() / toPlainText(RenderContext)
|
The same with color codes stripped. |
${label}/${subcmd} come from the command scope of the sending thread. A message delivered
later, from a scheduled or async task, has no such scope - so pass the context explicitly:
RenderContext context = RenderContext.of(sender, CommandMessageContext.of(label, "give"));
FCScheduler.runAsync(() -> message.send(context, sender));Each recipient still gets their own render; what the explicit context contributes is its
CommandMessageContext, which wins over whatever scope happens to be open where the send finally
happens.
📌 Note - a resolved
LocaleMessageis aFancyTextunder the hood, so everything here applies to localized messages too. Prefer declaring player-facing text as@FCLocalemessages (Localization) and reserve rawFancyTextfor text you build dynamically (menus, lists, confirmation buttons).
-
Localization -
@FCLocale/LocaleMessage, which produceFancyText; the recommended way to declare messages. -
Placeholders -
Replacer/RegexReplacer/CompoundReplacerandaddReplacer(...), PlaceholderAPI bridging. - Command Framework - building clickable menus and lists inside a command.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference