Repository navigation
Developers Tooltips
Applies to both mods:
TooltipBuilderand the two dispatch tiers live in the shared library. Only the category registrations differ — ALI's are in Registration Reference, AWI's in Worldgen Info → Registration Reference.
Everything ALI displays is a tree of tooltip nodes, built on the server and rendered into text on the client.
You never build Components yourself — you build a TooltipBuilder, and the mod takes care of translation,
indentation, deduplication and transfer.
com.yanny.aci.tooltip.TooltipBuilder is a small fluent API:
| Call | Produces |
|---|---|
TooltipBuilder.value(Object... v) |
a leaf carrying values |
TooltipBuilder.keyValue(String key, Object... v) |
a leaf with its own key |
TooltipBuilder.keyOnly(String | IMultiKey key) |
a leaf that is only a label |
TooltipBuilder.array(Consumer<TooltipBuilder> logic[, IMultiKey key]) |
a list of children under one heading |
TooltipBuilder.branch(Consumer<TooltipBuilder> logic) |
a sub-branch of the tree |
TooltipBuilder.error(String msg) |
an error row |
TooltipBuilder.number(NumberExpr expr) / .percent(NumberExpr expr)
|
a number, formatted on the client — see Numbers |
TooltipBuilder.intervals(List<NumberInterval> intervals, Object... v) |
a list of known ranges (NumberInterval.closed(lo, hi)), formatted on the client and joined with , as the key's first argument; v follow it |
TooltipBuilder.empty() |
nothing — use it for types that carry no useful information |
.add(TooltipBuilder | TooltipNode) |
appends a child; empty children are dropped |
.isAdvancedTooltip() |
marks the row as visible only with vanilla advanced tooltips (F3 + H) |
.build(String | IMultiKey key) |
finishes the builder into an immutable node |
Keys are IMultiKey/ITooltipKey constants (singular + plural form, com.yanny.aci.language). Vanilla and ALI
keys live in the mod's Lang enums; your own keys must be registered in
registerCommon and translated in your language file.
Returning TooltipBuilder.empty() is a deliberate choice, not a failure: use it for types that genuinely carry
nothing for the player. Leaving a type unregistered instead makes it fall through to the JSON dump, which is noise.
Inside a builder you almost never format a value by hand. You ask the utils for it, and the value dispatch finds the right renderer:
public static TooltipBuilder getMyFunctionTooltip(IServerUtils utils, MyFunction fun) {
return TooltipBuilder.array((b) -> {
b.add(utils.getValueTooltip(utils, fun.item).build(MyLang.Value.ITEM));
b.add(utils.getValueTooltip(utils, fun.count).build(MyLang.Value.COUNT));
b.add(utils.getValueTooltip(utils, fun.predicates).build(MyLang.Branch.CONDITIONS));
}, MyLang.Functions.MY_FUNCTION);
}fun.predicates is a list, fun.count may be a number provider — both are handled by the value tier below.
Registrations come in two flavours; they behave differently and it matters which one you use.
1. Value tooltips — registry.registerValueTooltip(Class<T>, BiFunction<IServerUtils, T, TooltipBuilder>).
Lookup walks superclasses and interfaces, so registering a base type covers all its subtypes. Common JDK and
Minecraft types are pre-registered — numbers, String, Boolean, Collection, Optional, Holder, ResourceKey,
TagKey, Component, Identifier/ResourceLocation, ranges, and Enum — which is why the example above just works
for lists and optionals.
With showInGameNames on, a ResourceKey or TagKey value of any registry is shown by its translated name, looked
up on the client under the keys listed in IDs and tags, and as its ID when no
translation exists. A bare Identifier/ResourceLocation carries no registry and is always shown as the ID, so when
the value is a registry entry pass its ResourceKey (holder.unwrapKey()), not its location.
Enum being on that list matters in practice: your own enums need no registration at all — see
Enum values below. Register value tooltips only for types the enum renderer cannot describe — your
own records and data holders used inside your conditions and functions.
An enum handed to getValueTooltip is rendered through a translation key rather than as its raw constant name,
so a language file or a resource pack can translate it. The key is
<modid>.enum.<owner>.<constant>
<modid> is the mod that owns the enum — ali or awi unless the enum was registered under another namespace,
see below — <constant> is the enum constant lowercased, and <owner> identifies the enum. A missing translation falls back to the raw constant name, so an untranslated enum
still displays.
You do not have to register anything. For an unregistered enum <owner> is derived from the class: the chain
of enclosing class names plus the enum's own name, each converted from camelCase to snake_case and joined with
.. com.example.somemod.Foo.Mode.FAST becomes ali.enum.foo.mode.fast. Your class names are not remapped, so
that key is stable and you can ship a translation for it.
Registering buys you a shorter, hand-picked key under your own mod id:
registry.registerEnumTranslation(MyMode.class, "mymod", "my_mode"); // mymod.enum.my_mode.fastDo this from registerServer, once per enum, and add the matching entries to your own language file. Two enums
registered with the same namespace and owner share one key space — their constants collide — so keep the pair
unique.
To build the same tooltip yourself, utils.getEnumTranslation(utils, value) returns the ready TooltipBuilder.
Vanilla enums are a different matter. A Minecraft class is named
EquipmentSloton Forge butclass_1304under Fabric's production mappings, so the derived key of an unregistered vanilla enum differs per loader and no single translation matches both. The mods register the vanilla enums they display; a vanilla enum worth translating has to be registered, not translated through its derived key.
2. Category tooltips — registerFunctionTooltip, registerConditionTooltip, registerIngredientTooltip,
registerEntitySubPredicateTooltip (not on 1.20.1), and the rest (see the
Registration Reference); in AWI, registerFeatureTooltip,
registerPlacementModifierTooltip, registerBlockPredicateTooltip and the rest (see its
Registration Reference).
Lookup is by exact class, with no superclass walking, because every implementation in those categories maps
one-to-one to an entry in a vanilla registry. Register each concrete class you ship; registering a shared abstract
base does nothing. A few registrations are keyed by the type's MapCodec instead of its class, matched just as
exactly: AWI's registerFeatureTooltip (up to 26.2 keyed by the feature's FeatureConfiguration class), and on
1.21.1 – 26.1.2 ALI's registerEntitySubPredicateTooltip.
A number that is more than a constant — a count, a chance, a radius rolled from a provider — is a NumberExpr
(com.yanny.aci.api), put into the tree with TooltipBuilder.number(expr) or, for a fraction shown as a percent,
TooltipBuilder.percent(expr). It travels to the client as the expression itself, and the client formats it in the
player's locale: the range, the most likely value (~), one row per level of an enchantment variable, a chart, and
with F3 + H the formula.
b.add(TooltipBuilder.number(NumberExpr.add(NumberExpr.constant(1), NumberExpr.uniformInt(0, 2))).build(MyLang.Value.COUNT));Build expressions through the static factories on NumberExpr (constant, range, atLeast, atMost,
uniformInt, uniformFloat, binomial, add, sub, mul, div, min, max, clamp, level, score,
lookup, cond, weighted, opaque, fn), not the record constructors — the factories fold constants and
simplify. Say only what is known: range(min, max) bounds a value without claiming a distribution, and opaque(typeId) renders
as ? (typeId) for one that cannot be computed.
Everything that computes — +, ×, clamp, round, uniform_int, binomial, … — is a function call fn(id, args) whose meaning is registered under its id. A distribution of your own is a function too:
public static final Identifier TRIANGLE = Identifier.fromNamespaceAndPath("mymod", "triangle");
NumberFunctions.register(NumberFunction.builder(TRIANGLE).arity(2)
.expand((args) -> NumberExpr.add(NumberExpr.uniformInt(NumberExpr.constant(0), args.get(0)),
NumberExpr.uniformInt(NumberExpr.constant(0), args.get(1))))
.build());On 1.21.1 the id is
ResourceLocation.fromNamespaceAndPath("mymod", "triangle"), on 1.20.1new ResourceLocation("mymod", "triangle").
- An
expandfunction is defined by what it stands for, in terms of other functions — the range, most likely value and chart are derived from that, while the formula keeps the function's own name (triangle(3; 3)). This is the easy way for a distribution built from simpler ones. - A primitive function instead gives
evaluate,boundsanddistributionitself. Anything left out degrades gracefully: noboundsshows?, nodistributionshows no~. - Register functions in
registerCommon, notregisterServer— the client evaluates the expressions too. A second registration under a taken id is ignored with a warning. - The function's name in a formula is the translation key
<namespace>.number.fn.<path>(mymod.number.fn.triangle), which your language file has to provide. - A client without the definition still decodes the expression and shows the call as
? (mymod:triangle).
Anything not registered is not lost: it is serialized through its own codec and displayed as raw JSON under an Auto-detected heading. Two log outputs help you find those:
- registration counts per category (
registered/total) at startup, - a list of every class that missed lookup, after the server-side data collection.
Identical tooltip nodes are interned, and the resulting table is sent to the client once per sync, ahead of the loot data itself; nodes then reference it by index. Two practical consequences:
- Building the same node shape for many entries is cheap — do not micro-optimize by omitting information.
- Tooltip content is fixed at build time on the server. It cannot depend on client-side state (keybinds, config of
the client, …) — the only client-side branch available is the advanced-tooltip flag, via
.isAdvancedTooltip().
Users
Loot Info
Worldgen Info
Developers
Loot Info
Examples
Worldgen Info