Skip to content

Developers Loot Info Registration Reference

Yanny edited this page Oct 9, 2026 · 18 revisions

Registration Reference

Everything below is registered from registerServer(IServerRegistry registry) unless stated otherwise. Read Tooltips first — it explains TooltipBuilder and the difference between exact-class category lookup and superclass-walking value lookup.

Availability differs by Minecraft version. The API follows vanilla: a registration method only exists where the vanilla concept exists. The version notes below say where each one is available. When in doubt, check the IServerRegistry interface in the jar you compile against.

Loot entries

registerEntry

Turns your LootPoolEntryContainer into a node of the displayed tree.

registry.registerEntry(MyEntry.class, MyNodes::getMyEntryNode);
public static IDataNode getMyEntryNode(IServerUtils utils, MyEntry entry, NumberExpr chance, NumberExpr sumWeight,
                                       List<TooltipNode> chanceConditions,
                                       List<LootItemFunction> functions, List<LootItemCondition> conditions) {
    // combine the entry's own functions/conditions with the inherited ones,
    // compute the final chance from chance, sumWeight and utils.getEntryWeight(utils, entry, ...),
    // then build a node - reusing ALI's ItemNode is usually enough
    return new ItemNode(finalChance, count, itemStack, tooltip, allFunctions, allConditions);
}

The factory receives the raw chance and the pool's total weight as NumberExprs (the entry's own weight has to be applied against it), the condition tooltips the NumberExpr.Branches in those expressions point at (chanceConditions), plus the functions and conditions inherited from the pool. If your entry produces plain items, build an ItemNode (float chance, NumberExpr count, item or tag, tooltip, functions, conditions); if it is a composite entry with children, extend ListNode — see Nodes and Widgets.

registerEntryWeight

Says how much weight your entry type takes in its pool, as a NumberExpr. An entry type with no weight registered weighs 0. Lookup walks superclasses; ALI already covers vanilla's singleton, composite and alternatives entry bases, so only an entry type outside those needs it:

registry.registerEntryWeight(MyEntry.class, (utils, entry, conditions) -> NumberExpr.constant(entry.weight()));

A factory reads it back with utils.getEntryWeight(utils, entry, conditions).

registerEntryChildren

Lists the entries your composite entry contains, so ALI finds the loot tables referenced from inside it. Vanilla's composite entries are already covered. Lookup walks superclasses:

registry.registerEntryChildren(MyCompositeEntry.class, (utils, entry) -> entry.children());

registerEntryTooltip

Describes the entry itself as tooltip text. registerEntry decides how an entry becomes a node of the tree; registerEntryTooltip decides how it reads when it is written into a tooltip instead — an entry nested in another entry's Entries: list, or one carried by a function that fills a container.

registry.registerEntryTooltip(MyEntry.class, MyTooltips::getMyEntryTooltip);
public static TooltipBuilder getMyEntryTooltip(IServerUtils utils, MyEntry entry) {
    return TooltipBuilder.array((b) -> {
        b.add(utils.getValueTooltip(utils, entry.item).build(MyLang.Value.ITEM));
        b.add(utils.getValueTooltip(utils, entry.condition).build(MyLang.Branch.CONDITIONS));
    }, MyLang.Entry.MY_ENTRY);
}

Up to 26.2 an entry holds a list of conditions, entry.conditions, instead of the single entry.condition.

A composite entry describes its children by asking the utils for each one, so the nesting keeps working for entry types it has never heard of:

b.add(TooltipBuilder.array((c) -> {
    for (LootPoolEntryContainer child : entry.children) {
        c.add(utils.getEntryTooltip(utils, child));
    }
}, MyLang.Branch.ENTRIES));

Lookup is by exact class, like the other category registrations. An unregistered entry type falls back to a JSON dump of the entry, which is readable but noisy — register the types your mod adds.

Conditions and functions

registerConditionTooltip / registerFunctionTooltip

The two most common registrations: describe your LootItemCondition / LootItemFunction.

registry.registerFunctionTooltip(MyFunction.class, MyTooltips::getMyFunctionTooltip);
registry.registerConditionTooltip(MyCondition.class, MyTooltips::getMyConditionTooltip);
public static TooltipBuilder getMyFunctionTooltip(IServerUtils utils, MyFunction fun) {
    return TooltipBuilder.array((b) -> {
        b.add(utils.getValueTooltip(utils, fun.amount).build(MyLang.Value.AMOUNT));
        b.add(utils.getValueTooltip(utils, fun.condition).build(MyLang.Branch.CONDITIONS));
    }, MyLang.Functions.MY_FUNCTION);
}

Up to 26.2 a conditional function holds a list of conditions, fun.predicates, instead of the single fun.condition.

registerPageResolver

Only relevant on Forge / NeoForge, for global loot modifiers. ALI judges every modifier against every page it displays — a block page, a mob page, a loot table page — and a page resolver answers that question for one class of yours:

registry.registerPageResolver(MyBlockCondition.class, (utils, condition, page) ->
        GlobalLootModifierUtils.testBlocks(page, condition.blocks()::contains, true));

The registered class is any class. Register a condition class when the condition is what names the target, and the modifier class itself when the target lives in the modifier's own fields:

registry.registerPageResolver(MyAddItemModifier.class, (utils, modifier, page) ->
        GlobalLootModifierUtils.testTable(page, (id) -> id.getPath().contains(modifier.path()), false));

A resolver answers a Verdict: Match.NO when the modifier can never apply on that page, Match.YES when the page selects it, Match.UNKNOWN when nothing there settles it. null means "no opinion" and hands the term back to ALI — the right answer for the pages your class says nothing about.

A LootPage carries what the page is made of: tableId(), the paramSet() vanilla rolls it with, blocks(), entityTypes() and the lazily built samples(). GlobalLootModifierUtils.testBlocks / testEntityTypes / testTable cover the usual answers — matching by predicate, so block::equals / id::equals covers a single target, a collection's ::contains a fixed group, and a predicate over a tag, a path fragment or a regex an open-ended family — so a hand-built Verdict is rarely needed.

The boolean those helpers take is explained. Pass true when the condition means nothing beyond selecting the page and it is left out of the tooltip, because the page already tells the whole story; pass false when it also constrains something else (a block state, an entity's NBT, …) and should stay visible. It has no effect for a resolver registered on a modifier class: only conditions are ever hidden from the tooltip.

A condition with no resolver is still decided. ALI runs it against a context built for the page, which answers what the page knows and refuses every read it cannot — a condition that needs a loot parameter the page never has is ruled out that way, with no registration at all, and AllOf / AnyOf / inverted groups are descended into. Register a resolver only where running the condition cannot answer it: when it reads your mod's own runtime state, or when the answer lives in the modifier's fields.

registerLootContextPreparer

A condition that reads the queried loot table id out of a carrier of its own — the loaders' loot table id conditions do exactly this — needs that carrier filled before it is run:

registry.registerLootContextPreparer((utils, context, page) -> MyLootHooks.setTableId(context, page.tableId()));

Every preparer runs, in registration order, on every context ALI builds for a page. With the id in place the condition decides itself and needs no resolver of its own.

registerEntitySubPredicateResolver

An entity_properties condition carrying a type_specific sub-predicate can only be judged when that sub-predicate is a function of the entity class: ALI's sample entities are bare instances carrying no spawn-time state — no variant, no age, nothing a mob decides when it spawns. Declare that for a sub-predicate of your own:

registry.registerEntitySubPredicateResolver(MyKindPredicate.class, (utils, predicate, page) ->
        page.samples().get().stream().allMatch((entity) -> entity instanceof MyMob) ? Verdict.yes(true) : Verdict.NO);

1.21.1–26.1.2: keyed by the sub-predicate's MapCodec instead of its class, like registerEntitySubPredicateTooltip: registry.registerEntitySubPredicateResolver(MyKindPredicate.CODEC, ...).

An unregistered sub-predicate leaves the condition undecided, which is the safe answer — a resolver that reads state off a sample produces wrong verdicts instead.

Numbers, chances and counts

Every count, chance, roll and trade price is a NumberExpr (com.yanny.aci.api): an expression describing the value instead of only bounding it. The client derives the range, the most likely value, the enchantment level rows, the chart and — with F3 + H — the formula from it, so a registration only has to say what the value is. Build expressions through the static factories (NumberExpr.constant, range, uniformInt, binomial, add, mul, min, max, clamp, fn, …), never the record constructors: the factories fold constants and simplify, so the formula carries no 1 × or + 0. Expressions and their functions are described in Tooltips.

registerContextIntProvider / registerContextFloatProvider

Converts your ContextIntProvider / ContextFloatProvider into a NumberExpr, so counts and rolls show a real number, range and distribution instead of ? (<provider type>). Vanilla number providers come in these two kinds, each held as a Holder, so each kind is registered with one converter:

registry.registerContextIntProvider(MyIntProvider.class, (utils, provider, conditions) ->
        NumberExpr.uniformInt(provider.min(), provider.max()));
registry.registerContextFloatProvider(MyFloatProvider.class, (utils, provider, conditions) ->
        NumberExpr.uniformFloat(provider.min(), provider.max()));

Up to 26.2 there is a single NumberProvider kind; see registerNumberProvider.

When the distribution is not known, say only what is: NumberExpr.range(min, max) gives the bounds and no most likely value.

Every converter also receives conditions, the list of condition tooltips collected for the value being built. A value that depends on a loot condition returns NumberExpr.cond(branches, otherwise): add the condition's tooltip to conditions and give its branch that index, new NumberExpr.Branch(conditions.size() - 1, value) — the condition's tooltip is then shown under its row. A branch whose condition has no tooltip uses -1 and shows without it.

Reading a provider back out of your own function goes through utils.convertContextInt(utils, provider, conditions) or utils.convertContextFloat(utils, provider, conditions), which take the Holder<ContextIntProvider> / Holder<ContextFloatProvider> your function holds — pass on the conditions you were given.

registerNumberProvider

Up to 26.2. A number provider is a single NumberProvider with both getFloat and getInt; on 26.3 use registerContextIntProvider / registerContextFloatProvider instead.

registry.registerNumberProvider(MyProvider.class,
        (utils, provider, conditions) -> NumberExpr.uniformFloat(provider.min(), provider.max()),
        (utils, provider, conditions) -> NumberExpr.uniformInt(provider.min(), provider.max()));

The first converter describes getFloat, the optional second one getInt. Give the second one when your getInt is not just getFloat rounded — vanilla's uniform generator, for instance, draws a whole number between the rounded bounds. Without it, the int value is the float one rounded. Both converters receive conditions as described above.

Reading a provider back out of your own function goes through utils.convertNumber(utils, provider, conditions) (float) or utils.convertIntNumber(utils, provider, conditions) (int), which take the NumberProvider itself — use the one your function itself calls on the provider.

registerIntProvider / registerFloatProvider

The same for vanilla's IntProvider / FloatProvider, wherever one appears inside a tooltip: one converter each, with the same (utils, provider, conditions) shape. Read one back with utils.convertIntProvider / utils.convertFloatProvider. Both work the same in Advanced Worldgen Info.

registerLevelBasedValue

Available from 1.21.1. Converts your LevelBasedValue (enchantment effect amounts, enchanted-bonus chances, …) into a NumberExpr of the enchantment level, which the converter receives as its third argument. Build the value from it rather than evaluating per level — the client derives one row per level from the expression:

registry.registerLevelBasedValue(MyLevelValue.class, (utils, value, level) ->
        NumberExpr.add(NumberExpr.constant(value.base()), NumberExpr.mul(NumberExpr.constant(value.perLevel()), level)));

A value wrapping another one converts it through utils.convertLevelBasedValue(utils, inner, level). An unregistered value, or one whose converter throws (logged), shows as ? (<value type>).

registerCountModifier

A function that changes how many items drop. Registering it lets the displayed count reflect the function instead of being just the pool's roll count. The modifier receives the count so far and returns the new one; conditions is the same condition list the number converters receive:

registry.registerCountModifier(MySetCountFunction.class, (utils, fun, count, conditions) ->
        NumberExpr.add(count, utils.convertContextInt(utils, fun.count(), conditions)));

Up to 26.2 fun.count() is a NumberProvider, read with utils.convertIntNumber(utils, fun.count(), conditions).

A count that grows with an enchantment uses the level as a variable, NumberExpr.level(enchantmentId, maxLevel). The client shows one row per level, up to maxLevel, by substituting it — nothing is computed per level:

registry.registerCountModifier(MyHarvestFunction.class, (utils, fun, count, conditions) ->
        NumberExpr.add(count, NumberExpr.uniformInt(NumberExpr.constant(0), NumberExpr.level("mymod:harvesting", 3))));

A function carrying its own conditions is handled by ALI: the count splits into a row per condition (or, for a plain random chance, into a weighted choice between the modified and the original count). A modifier that throws turns only that value into ? (<function type>), logged — the loot table is still shown.

registerChanceModifier

A condition that changes the drop chance (a random chance, a bonus per enchantment level, …), so the tree can show the resulting probability. The chance is a fraction (0.25), rendered as a percent.

registry.registerChanceModifier(MyChanceCondition.class, (utils, condition, chance, conditions) ->
        NumberExpr.mul(chance, NumberExpr.constant(condition.probability())));

registerItemStackModifier

A function that changes the resulting ItemStack itself (enchanting it, setting components/NBT, …). The returned stack is what the viewer shows in the slot.

registry.registerItemStackModifier(MyEnchantFunction.class, (utils, fun, stack) -> fun.apply(stack));

registerFunctionUnwrapper / registerConditionUnwrapper

For a function or condition that only wraps others. ALI applies count, chance and item modifiers per class, so a set_count inside your wrapper would otherwise be ignored. An unwrapper returns the functions or conditions the wrapper applies, and ALI treats them as if they stood in the wrapper's place. It unwraps recursively, so a returned wrapper is unwrapped again. A wrapper nested inside another condition or function is still shown as itself in the tooltip.

A loot table, pool and entry each hold a single condition and a single function, and several of them are combined into one all_of / sequence. ALI unwraps those through the same unwrappers, so the conditions and functions listed on a table, pool or entry are the unwrapped ones — a wrapper of yours placed there is listed as its contents too.

Up to 26.2 a loot table, pool and entry hold lists of conditions and functions.

registry.registerConditionUnwrapper(MyAllOfCondition.class, (utils, condition) -> condition.terms());
registry.registerFunctionUnwrapper(MySequenceFunction.class, (utils, function) -> function.functions());

Only unwrap what is exactly equivalent to the list you return: conditions that must all pass, or functions that always run in order. Return null otherwise — for "any of", for an inverted condition, or for a wrapper with conditions of its own — and the wrapper is kept as a whole. ALI already unwraps vanilla all_of and a sequence without a condition of its own; references are resolved by Minecraft itself.

Up to 26.2 Minecraft leaves references unresolved, and ALI unwraps vanilla all_of, sequence, predicate references and item modifier references.

Ingredients

registerIngredientTooltip

For custom Ingredient subclasses — which only Forge allows. Vanilla Ingredient is final, so on Fabric and NeoForge a custom ingredient is registered as a value tooltip instead; see registerIngredientUnwrapper below.

registry.registerIngredientTooltip(MyIngredient.class, MyTooltips::getMyIngredientTooltip);

registerIngredientUnwrapper

On some loaders a custom ingredient is not an Ingredient subclass of its own. On Fabric every custom ingredient arrives in the same Fabric API wrapper class; on NeoForge it is a plain Ingredient holding an ICustomIngredient. Exact-class dispatch through registerIngredientTooltip cannot tell such ingredients apart. An unwrapper returns the object an ingredient wraps, or null for an ingredient it does not handle. The returned object is rendered through the ordinary value tooltip tier; when no value tooltip matches it, ALI shows the generic tooltip built from the ingredient's serialized form.

ALI registers the unwrapper for Fabric's CustomIngredient and NeoForge's ICustomIngredient itself, so on those loaders a value tooltip for your custom ingredient's class is all you need:

registry.registerValueTooltip(MyCustomIngredient.class, MyTooltips::getMyCustomIngredientTooltip);

Register an unwrapper of your own only for a wrapping scheme ALI does not know about:

registry.registerIngredientUnwrapper((ingredient) -> ingredient instanceof MyWrapper wrapper ? wrapper.getInner() : null);

On Forge custom ingredients are Ingredient subclasses, so registerIngredientTooltip covers them.

Predicates and components

Method Available Purpose
registerItemSubPredicateTooltip 1.21.1 only describe a custom ItemSubPredicate
registerEntitySubPredicateTooltip 1.21.1+ describe a custom EntitySubPredicate
registerDataComponentTypeTooltip 1.21.1+ describe the value of a custom DataComponentType
registerDataComponentPredicateTooltip 1.21.11+ describe a custom DataComponentPredicate
registerConsumeEffectTooltip 1.21.11+ describe a custom ConsumeEffect
registerSlotSourceTooltip 1.21.11+ describe a custom SlotSource

All follow the same shape, (utils, value) -> TooltipBuilder, keyed by the described class:

registry.registerSlotSourceTooltip(MySlotSource.class, MyTooltips::getMySlotSourceTooltip);

One is keyed differently: registerDataComponentTypeTooltip takes the DataComponentType itself and receives that component's value.

registry.registerDataComponentTypeTooltip(MyComponents.MY_COMPONENT, MyTooltips::getMyComponentTooltip);
registry.registerEntitySubPredicateTooltip(MySubPredicate.class, MyTooltips::getMySubPredicateTooltip);

1.21.1–26.1.2: registerEntitySubPredicateTooltip is keyed differently too, by the sub-predicate's MapCodec: registry.registerEntitySubPredicateTooltip(MySubPredicate.CODEC, MyTooltips::getMySubPredicateTooltip);

Trades

registerItemListing

Up to 1.21.11. From 26.1.2 on, a trade is data-driven (minecraft:villager_trade), so there is no ItemListing class to describe and this method does not exist. Trades a mod still defines in code are handed over as nodes through TradeLevel.OfTrades instead.

registry.registerItemListing(MyItemListing.class, (utils, listing, condition) ->
        new ItemsToItemsNode(
                utils,
                Either.left(listing.cost()), NumberExpr.constant(listing.cost().getCount()),
                Either.left(ItemStack.EMPTY), NumberExpr.constant(0),
                Either.left(listing.result()), NumberExpr.constant(listing.result().getCount()),
                listing.maxUses(), listing.villagerXp(), listing.priceMultiplier(), condition));

The node takes the two cost slots and the result, each as an item (or tag) plus its count as a NumberExpr — a price rolled from a range is NumberExpr.uniformInt(min, max), and shows its most likely value like any count. The exact constructor overloads differ slightly per Minecraft version — TradeUtils in ALI's own source is the reference. From 26.1.2 on there is no overload without per-slot tooltips: each slot is followed by a TooltipNode (a condition on a cost, a modifier on the result, TooltipNode.empty() for none), and max uses and villager XP are NumberExprs, followed by the trade's tooltip.

That is all that is needed to make the trade findable from either side: ItemsToItemsNode implements ITradeNode, and the recipe viewer reads the accepted and offered items back out of the node itself. A trade node of your own has to implement ITradeNode for that to work — see Nodes and Widgets.

A slot may also be any node instead of one item — for a cost or result your mod describes as a loot pool entry, possibly several items with weights, conditions and functions:

new ItemsToItemsNode(
        TradeUtils.getSlotNode(utils, costEntry),      // a LootPoolEntryContainer, rolled like a pool of that one entry
        TradeUtils.getEmptySlotNode(),                 // no second cost
        TradeUtils.getSlotNode(utils, resultEntry),
        tooltip);                                      // the trade's own tooltip: max uses, XP, conditions, …

Every item the slot's node can produce becomes one option of the slot: the recipe viewer cycles through them, showing the chance, count and functions of the one currently displayed, and the trade is found from any of them.

A slot whose item your trade picks at random from a known set is built the same way, from the set itself:

new ItemsToItemsNode(
        utils,
        TradeUtils.getItemSlotNode(List.of(new ItemStack(Items.EMERALD)), NumberExpr.uniformInt(2, 4), TooltipNode.empty()),
        TradeUtils.getEmptySlotNode(),
        TradeUtils.getItemSlotNode(saplings, NumberExpr.constant(1), TooltipNode.empty()),   // one option per stack
        maxUses, villagerXp, priceMultiplier, condition);                                  // the usual trade tooltip

TradeUtils.getSlotNode(List<IDataNode>) joins options built separately, for alternatives that differ in count. All slots cycle on one clock, so two slots whose items vary together (a log and its stripped log) stay matched when both list their options in the same order and number.

From 26.1.2 on only the first form exists, the one taking the trade's own tooltip.

registerTrades

A trader of your own — a custom entity, or a second kind of wandering merchant — has to be registered, otherwise its offers are never scanned. Each level is a TradeLevel:

registry.registerTrades(
        Identifier.fromNamespaceAndPath("mymod", "my_trader"),   // the trader's ID
        MyEntities.MY_TRADER.get(),                              // the trader's entity type, or null
        (utils) -> {                                             // Int2ObjectMap<TradeLevel>, per level
            Int2ObjectMap<TradeLevel> levels = new Int2ObjectOpenHashMap<>();

            levels.put(1, new TradeLevel.OfSet(ResourceKey.create(Registries.TRADE_SET,
                    Identifier.fromNamespaceAndPath("mymod", "my_trader/level_1"))));
            levels.put(2, new TradeLevel.OfTrades(
                    new TradeLevelInfo(NumberExpr.constant(1), 0.5F),   // offers picked from that level, and its chance
                    (utils) -> MyTrades.getNodes(utils)));         // List<IDataNode>, built by your plugin
            return levels;
        });

Up to 1.21.11 a level is a set of ItemListings, and how many offers it contributes is a separate argument:

registry.registerTrades(
        Identifier.fromNamespaceAndPath("mymod", "my_trader"),   // the trader's ID
        MyEntities.MY_TRADER.get(),                              // the trader's entity type, or null
        () -> MyTrades.TRADES,                                   // Int2ObjectMap<VillagerTrades.ItemListing[]>, per level
        (level) -> new TradeLevelInfo(NumberExpr.constant(2)));  // offers picked from that level, and its chance

On 1.21.1 and older the ID is a ResourceLocation: ResourceLocation.fromNamespaceAndPath("mymod", "my_trader") on 1.21.1, new ResourceLocation("mymod", "my_trader") on 1.20.1.

  • The ID is what the trade category patterns (tradeCategories) are matched against, and it selects the displayed name: a villager profession is named by entity.minecraft.villager.<path>, anything else by entity.<namespace>.<path>.
  • The entity type links the trader to its entity: the recipe viewer shows the entity's spawn egg in front of the trader's name, lists the trades under that spawn egg, and the name's tooltip shows the entity's ID. It is null for a trader that is no entity — the entry then has neither. Several traders may share one entity type; every villager profession is registered with EntityType.VILLAGER.
  • The levels are built during the scan, not at registration time, so they may use data that is filled later during startup. From 26.1.2 on that is a function taking IServerUtils: a trader whose trades live in a datapack registry of your own reads it through utils.lookupProvider(), and utils.convertIntNumber turns its NumberProviders into the NumberExpr a TradeLevelInfo needs. Up to 1.21.11 it is a supplier taking nothing.
  • How many offers a level contributes is shown in the level badge's tooltip, and so is a chance below 1, for a level the trader offers only sometimes. Where both come from depends on the kind of level, below.

A level is one of two kinds:

  • TradeLevel.OfSet names a TradeSet of the minecraft:trade_set registry. The set is looked up when the scan runs, so it may be one a datapack provides; a level whose set does not resolve is skipped. How many offers it contributes is the set's own amount — a range, when that amount is not a fixed number. TradeLevel.ofSets(...) turns an Int2ObjectMap<ResourceKey<TradeSet>> into levels of this kind, for a trader that consists of trade sets only.
  • TradeLevel.OfTrades is for a trader that assembles its offers in Java and has no trade sets to name. It carries a TradeLevelInfo (offers picked, chance) and a function building the trade nodes — usually ItemsToItemsNodes, see registerItemListing for what the node needs. The function is called during the scan with IServerUtils.

Both kinds may be mixed in one trader, which is how a trader that adds some vanilla trade sets alongside its own offers is described.

Up to 1.21.11 registerTrades takes no level kinds: the TradeLevelInfo the last argument returns supplies the offers picked and the chance of every level — vanilla villagers use 2 for every level, the wandering trader 5 for its first pool and 1 for its second. On 1.20.1–1.21.11 levels returned by registerTradeOverride are TradeLevels, of the kinds TradeLevel.OfListings (an ItemListing[] and its TradeLevelInfo) and TradeLevel.OfTrades as above.

A level may pick its offers from several groups independently, each its own number — a trade group is a TradeGroupNode(NumberExpr offers, List<IDataNode> trades) returned among the trades of a TradeLevel.OfTrades. The level badge then counts the trades inside the groups, so pass the sum of the groups' selectionCounts as the level's offers picked.

Every vanilla villager profession and the wandering trader are registered this way by ALI itself, so a modded profession added to the vanilla VillagerProfession registry needs no registration — only a trader that is not a villager profession does.

registerTradeOverride

For a mod that replaces the trades of a trader that is already registered — its own data instead of the vanilla trades of a profession, for example:

registry.registerTradeOverride((utils, traderId) -> {
    MyTradeTable table = MyTradeTables.get(traderId);   // null when your mod leaves this trader alone

    return table == null ? null : MyTrades.getLevels(utils, table);   // Int2ObjectMap<TradeLevel>, per level
});

It is called for every registered trader during the scan, after your mod's data is loaded. Overrides are asked in the order they were registered and the first one that does not return null replaces the trader's levels; a trader no override answers for keeps the levels it was registered with. The trader keeps its ID and entity type.

Loot modifiers

registerLootModifiers

For loot your mod injects outside of loot tables and outside the Forge/NeoForge GLM system. The returned ILootModifiers are applied to the matching block/entity/table nodes.

registry.registerLootModifiers((utils) -> MyLootRegistry.all().stream()
        .map((entry) -> (ILootModifier<?>) new MyLootModifier(utils, entry))
        .toList());

For the Forge/NeoForge datapack GLM system, use the dedicated plugin interface instead: Global Loot Modifiers.

registerGlobalLootModifiers

The same idea for modifiers judged per page (IPageLootModifier) instead of by a fixed type. This is how the loader glue feeds the datapack GLMs in; a mod shipping its own GLMs goes through the plugin interface above.

Value types

Not on IServerRegistry alone — registerValueTooltip is the shared, superclass-aware tier:

registry.registerValueTooltip(MyPayload.class, (utils, value) -> TooltipBuilder.array((b) -> {
    b.add(utils.getValueTooltip(utils, value.item()).build(MyLang.Value.ITEM));
    b.add(utils.getValueTooltip(utils, value.amount()).build(MyLang.Value.AMOUNT));
}));

Use it for your own records and data holders that appear inside another tooltip. Enums, collections, optionals, numbers, Holders, tags and the like are already handled — do not register those. See Tooltips.

registerEnumTranslation

Optional. An enum is already displayed through a translation key derived from its class name; this only replaces the namespace and the derived part with ones you choose:

registry.registerEnumTranslation(MyMode.class, "mymod", "my_mode");   // mymod.enum.my_mode.fast

See Tooltips → Enum values for the key shape and the fallback behaviour.

Other registrations

Covered on their own pages, listed here so this reference is complete:

Method Registry Where it is described
registerCacheCleaner IServerRegistry Plugin Lifecycle → Caches for data collection
registerTranslationKey ICommonRegistry Plugin Lifecycle → registerCommon
registerEntityVariants ICommonRegistry Plugin Lifecycle → registerCommon
registerDataNode / registerWidget IClientRegistry Nodes and Widgets
registerGlobalLootModifier IGlobalLootModifierPlugin.IRegistry Global Loot Modifiers

Clone this wiki locally