Skip to content

Developers Tooltips

Yanny edited this page Oct 4, 2026 · 7 revisions

Tooltips

Applies to both mods: TooltipBuilder and 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.

TooltipBuilder

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.

Nested values — getValueTooltip

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.

The two dispatch tiers

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.

Enum values

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.fast

Do 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 EquipmentSlot on Forge but class_1304 under 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.

Numbers

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.

Number functions

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.1 new ResourceLocation("mymod", "triangle").

  • An expand function 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, bounds and distribution itself. Anything left out degrades gracefully: no bounds shows ?, no distribution shows no ~.
  • Register functions in registerCommon, not registerServer — 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).

The missing-entry fallback

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.

Caching and the wire

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().

Clone this wiki locally