Repository navigation
Developers Loot Info Nodes And Widgets
The loot tree is made of data nodes. A node is built on the server, serialized, and reconstructed and rendered
on the client by a matching widget. You only need your own node type when your content does not fit any
existing one — for plain item drops, reuse ALI's ItemNode. A complete composite node, from the entry to the
widget, is walked through in Example: a custom node.
A node implements com.yanny.ali.api.IDataNode:
| Member | Purpose |
|---|---|
Identifier getId() |
the node type ID; the key that pairs the node with its widget |
TooltipNode getTooltip() |
the tooltip tree of this node |
void encode(IServerUtils utils, RegistryFriendlyByteBuf buf) |
writes the node to the network buffer |
float getChance() (default 1) |
used to sort siblings by descending chance |
Every node has two constructors by convention:
- a server constructor that takes the domain objects and builds the tooltip,
- a client constructor
(IClientUtils utils, RegistryFriendlyByteBuf buf)that reads back exactly whatencodewrote.
Tooltips are not sent inline — they are interned and referenced by ID:
public class MyNode implements IDataNode {
public static final Identifier ID = Identifier.fromNamespaceAndPath("mymod", "my_node");
private final TooltipNode tooltip;
private final float chance;
public MyNode(float chance, TooltipNode tooltip) {
this.chance = chance;
this.tooltip = tooltip;
}
public MyNode(IClientUtils utils, RegistryFriendlyByteBuf buf) {
this.tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
this.chance = buf.readFloat();
}
@Override
public void encode(IServerUtils utils, RegistryFriendlyByteBuf buf) {
buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
buf.writeFloat(chance);
}
@Override
public @NotNull TooltipNode getTooltip() {
return tooltip;
}
@Override
public @NotNull Identifier getId() {
return ID;
}
@Override
public float getChance() {
return chance;
}
}On 1.21.1:
ResourceLocationinstead ofIdentifier, created withResourceLocation.fromNamespaceAndPath("mymod", "my_node").
On 1.20.1: the ID is a
ResourceLocationbuilt with its constructor, and the buffer is a plainFriendlyByteBuf— inencode, in the client constructor and inregisterDataNode:public class MyNode implements IDataNode { public static final ResourceLocation ID = new ResourceLocation("mymod", "my_node"); private final TooltipNode tooltip; private final float chance; public MyNode(float chance, TooltipNode tooltip) { this.chance = chance; this.tooltip = tooltip; } public MyNode(IClientUtils utils, FriendlyByteBuf buf) { this.tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt()); this.chance = buf.readFloat(); } @Override public void encode(IServerUtils utils, FriendlyByteBuf buf) { buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip)); buf.writeFloat(chance); } @Override public @NotNull TooltipNode getTooltip() { return tooltip; } @Override public @NotNull ResourceLocation getId() { return ID; } @Override public float getChance() { return chance; } }
encodeand the client constructor must stay in exact sync. A mismatch corrupts everything that follows in the buffer, not just your node.
For a node that contains other nodes (alternatives, groups, sequences, …), extend
com.yanny.ali.api.ListNode instead of implementing IDataNode directly. It handles the child list, sorting and
the child part of the encoding; you add your own payload in encodeNode, and read it back in the client
constructor after calling super.
The client sorts the children by descending chance. A node whose children are positional — the first child means
something different from the second, like the cost and result slots of a trade — overrides isOrdered() to return
true, and its children arrive in the order they were added. It is called from the decoding constructor, before
any field of your own is set, so it returns a constant.
A recipe viewer needs flat item lists next to the rendered tree: the recipe's outputs, the two sides of a trade, and the reverse index that answers which loot table drops this item. ALI does not receive those lists from the server — it reads them out of the tree — so a node holding an item has to say so.
A leaf holding one item (or one tag) implements com.yanny.ali.api.IItemNode:
| Member | Purpose |
|---|---|
Either<ItemStack, TagKey<? extends ItemLike>> getItem() |
what the slot stands for: one stack, or the tag it picks from |
List<ItemStack> getItems() |
the stacks it resolves to — a tag's members, expanded against the client's registry |
void retainItems(Predicate<ItemStack> isVisible) (default: nothing) |
drop the members the viewer hides |
List<LootItemCondition> getConditions() |
the conditions of the entry |
List<LootItemFunction> getFunctions() |
the functions of the entry |
NumberExpr getCount() |
the dropped count, used for the count shown on the slot |
float getChance() |
the drop chance |
boolean hasPredicates() |
whether the entry is gated by conditions other than plain drop chance |
Resolve the tag in the constructor, into a mutable list, and keep getItems() returning that same list — this is
what lets retainItems narrow it and what makes a tag follow datapack reloads. NodeUtils.resolveItems does it
for you. An empty stack stands for nothing and must not be reported.
Reusing ItemNode gives you all of this. Only a node that holds an item in some shape of its own needs to
implement the interface.
A trade additionally implements com.yanny.ali.api.ITradeNode (getInputItems, getOutputItems). A leaf
cannot answer this — an item node is one stack and does not know whether it was a cost or a result — so the node
that holds the whole trade splits its own children. Built-in ItemsToItemsNode already does.
The client side of the pair renders the node. Both halves are registered in registerClient under the same ID:
@Override
public void registerClient(IClientRegistry registry) {
registry.registerWidget(MyNode.ID, MyWidget::new);
registry.registerDataNode(MyNode.ID, MyNode::new);
}-
registerDataNodetakes(IClientUtils, RegistryFriendlyByteBuf) -> IDataNode— your client constructor (FriendlyByteBufon 1.20.1). -
registerWidgettakes(IWidgetUtils utils, IDataNode entry, RelativeRect rect, int maxWidth) -> IWidget.
A widget implements com.yanny.aci.api.IWidget (getRect, getDirection, render, getTooltipLines); for
a widget that lays out children, extend com.yanny.ali.api.ListWidget, which draws the connecting branch lines
for you. Geometry uses RelativeRect, which is positioned relative to its parent.
An item slot is not drawn by the widget: it asks the recipe viewer for one with
utils.addSlotWidget(item, node, rect), and the viewer draws the item, its count and the node's tooltip.
utils.addSlotWidget(options, rect) takes a list of item nodes instead: the slot cycles through them and shows the
count and tooltip of the one currently displayed — how a trade slot with several possible items is shown.
The same widget is used in EMI, JEI and REI — each viewer integration adapts IWidget to its own API, so you do
not write anything viewer-specific.
Before writing your own, check whether one of ALI's existing nodes fits — ItemNode (an item or tag with a count
and chance), EmptyNode, AlternativesNode, GroupNode, SequenceNode, ReferenceNode, ModifiedNode. Their
widgets are already registered, so you register nothing on the client.
Users
Loot Info
Worldgen Info
Developers
Loot Info
Examples
Worldgen Info