Skip to content

Developers Worldgen Info Nodes And Widgets

Yanny edited this page Oct 3, 2026 · 2 revisions

Nodes and Widgets

The worldgen 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 a plain block, reuse AWI's BlockNode.

The contract

A node implements com.yanny.awi.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 what encode wrote.

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;

    public MyNode(TooltipNode tooltip) {
        this.tooltip = tooltip;
    }

    public MyNode(IClientUtils utils, RegistryFriendlyByteBuf buf) {
        this.tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
    }

    @Override
    public void encode(IServerUtils utils, RegistryFriendlyByteBuf buf) {
        buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
    }

    @Override
    public @NotNull TooltipNode getTooltip() {
        return tooltip;
    }

    @Override
    public @NotNull Identifier getId() {
        return ID;
    }
}

On 1.21.1: ResourceLocation instead of Identifier, created with ResourceLocation.fromNamespaceAndPath("mymod", "my_node").

On 1.20.1: the ID is a ResourceLocation built with its constructor, and the buffer is a plain FriendlyByteBuf — in encode, in the client constructor and in registerDataNode:

public class MyNode implements IDataNode {
    public static final ResourceLocation ID = new ResourceLocation("mymod", "my_node");

    private final TooltipNode tooltip;

    public MyNode(TooltipNode tooltip) {
        this.tooltip = tooltip;
    }

    public MyNode(IClientUtils utils, FriendlyByteBuf buf) {
        this.tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
    }

    @Override
    public void encode(IServerUtils utils, FriendlyByteBuf buf) {
        buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
    }

    @Override
    public @NotNull TooltipNode getTooltip() {
        return tooltip;
    }

    @Override
    public @NotNull ResourceLocation getId() {
        return ID;
    }
}

encode and the client constructor must stay in exact sync. A mismatch corrupts everything that follows in the buffer, not just your node.

Nodes with children

For a node that contains other nodes, extend com.yanny.awi.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. That is what AWI's own LevelStemNode, BiomeNode, GenerationStepNode, BaseTerrainNode and PlacedFeatureNode are.

Nodes that stand for blocks

A recipe viewer needs a flat block list next to the rendered tree: the entry's outputs, and the reverse index that answers which biome generates this block. AWI does not receive those lists from the server — it reads them out of the tree — so a node holding a block has to say so, by implementing com.yanny.awi.api.IBlockNode:

Member Purpose
Either<Block, TagKey<Block>> getBlock() what the slot stands for: one block, or the tag the feature picks from
List<Block> getBlocks() the blocks it resolves to — a tag's members, expanded against the client's registry
void retainBlocks(Predicate<Block> isVisible) (default: nothing) drop the members the viewer hides
float getChance() sorting

Resolve the tag in the client constructor, into a mutable list, and keep getBlocks() returning that same list — this is what lets retainBlocks narrow it and what makes a tag follow datapack reloads. Members are kept in registry order, so a cycling slot does not jitter between reloads.

The index and the pruning both go through this interface, so they cannot disagree: a tag contributes its members to the reverse index, retainBlocks drops the ones the viewer hides, and a node left with nothing is dropped entirely.

Reusing BlockNode gives you all of this. Only a node that holds a block in some shape of its own needs to implement the interface.

The widget

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);
}
  • registerDataNode takes (IClientUtils, RegistryFriendlyByteBuf) -> IDataNode — your client constructor (FriendlyByteBuf on 1.20.1).
  • registerWidget takes (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.awi.api.ListWidget, which draws the connecting branch lines for you. Geometry uses RelativeRect, which is positioned relative to its parent. render receives a GuiGraphicsExtractor.

1.20.1–1.21.11: render receives a GuiGraphics instead.

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.

Reusing built-in nodes

Before writing your own, check whether one of AWI's existing nodes fits: BlockNode (a block or a block tag, with its own tooltip), PlacedFeatureNode, GenerationStepNode, BaseTerrainNode, BiomeNode, LevelStemNode. Their widgets are already registered, so you register nothing on the client.

Clone this wiki locally