Repository navigation
Developers Examples Custom Node
A complete, minimal plugin that gives a loot entry of your own mod its own node in the loot tree — the server half that is built and sent, and the client half that draws it. Works on every loader. The reference behind every step is Nodes and Widgets and Registration Reference → registerEntry; this page only puts the pieces together.
A node of your own is rarely needed for a single item — an entry that drops one item or tag builds ALI's ItemNode
and is done. The usual reason is a composite entry: one that holds other entries and decides how they drop, and
whose rule the tree should show. That is the case below.
Say your mod has an entry that drops all of its children, several times over:
package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Holder;
import net.minecraft.world.level.storage.loot.entries.ComposableEntryContainer;
import net.minecraft.world.level.storage.loot.entries.CompositeEntryBase;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.minecraft.world.level.storage.loot.functions.LootItemFunction;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.List;
import java.util.Optional;
public class RepeatEntry extends CompositeEntryBase {
public static final MapCodec<RepeatEntry> MAP_CODEC = RecordCodecBuilder.mapCodec((i) -> i.group(
LootPoolEntries.CODEC.listOf().optionalFieldOf("children", List.of()).forGetter(RepeatEntry::getChildren),
Codec.INT.fieldOf("times").forGetter(RepeatEntry::getTimes)
).and(commonFields(i)).apply(i, RepeatEntry::new));
private final int times;
private RepeatEntry(List<LootPoolEntryContainer> children, int times, Optional<Holder<LootItemCondition>> condition,
Optional<Holder<LootItemFunction>> modifier) {
super(children, condition, modifier);
this.times = times;
}
@Override
public MapCodec<RepeatEntry> codec() {
return MAP_CODEC;
}
@Override
protected ComposableEntryContainer compose(List<? extends ComposableEntryContainer> children) {
return (context, output) -> {
for (int n = 0; n < times; n++) {
for (ComposableEntryContainer child : children) {
child.expand(context, output);
}
}
return true;
};
}
public int getTimes() { return times; }
public List<LootPoolEntryContainer> getChildren() { return children; }
public Optional<Holder<LootItemCondition>> getCondition() { return condition; }
public Optional<Holder<LootItemFunction>> getModifier() { return modifier; }
}children, condition and modifier are inherited — the entry's own condition and function, each combining
several into one. compose is called from the CompositeEntryBase constructor, before times is set; the
returned lambda reads times only when the loot is rolled.
The entry type is registered under your mod ID, like any other loot type. On Fabric, call this from your
ModInitializer:
package com.example.mymod.loot;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
public class MyModLoot {
public static void register() {
Registry.register(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE, "mymod:repeat", RepeatEntry.MAP_CODEC);
}
}On NeoForge, call MyModLoot.register(modEventBus) from your mod's constructor:
package com.example.mymod.loot;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredRegister;
public class MyModLoot {
private static final DeferredRegister<MapCodec<? extends LootPoolEntryContainer>> ENTRIES =
DeferredRegister.create(Registries.LOOT_POOL_ENTRY_TYPE, "mymod");
public static void register(IEventBus modEventBus) {
ENTRIES.register("repeat", () -> RepeatEntry.MAP_CODEC);
ENTRIES.register(modEventBus);
}
}Forge (1.20.1 and 1.21.1 only) registers the same way with net.minecraftforge.registries.DeferredRegister and
net.minecraftforge.eventbus.api.IEventBus; see the version notes below.
26.1.2–26.2: an entry has no
modifier, and its conditions are the inheritedList<LootItemCondition> conditions. BothMyModLootvariants are the same as above.
RepeatEntry on 26.1.2–26.2
package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.world.level.storage.loot.entries.ComposableEntryContainer;
import net.minecraft.world.level.storage.loot.entries.CompositeEntryBase;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.List;
public class RepeatEntry extends CompositeEntryBase {
public static final MapCodec<RepeatEntry> MAP_CODEC = RecordCodecBuilder.mapCodec((i) -> i.group(
LootPoolEntries.CODEC.listOf().optionalFieldOf("children", List.of()).forGetter(RepeatEntry::getChildren),
Codec.INT.fieldOf("times").forGetter(RepeatEntry::getTimes)
).and(commonFields(i).t1()).apply(i, RepeatEntry::new));
private final int times;
private RepeatEntry(List<LootPoolEntryContainer> children, int times, List<LootItemCondition> conditions) {
super(children, conditions);
this.times = times;
}
@Override
public MapCodec<RepeatEntry> codec() {
return MAP_CODEC;
}
@Override
protected ComposableEntryContainer compose(List<? extends ComposableEntryContainer> children) {
return (context, output) -> {
for (int n = 0; n < times; n++) {
for (ComposableEntryContainer child : children) {
child.expand(context, output);
}
}
return true;
};
}
public int getTimes() { return times; }
public List<LootPoolEntryContainer> getChildren() { return children; }
public List<LootItemCondition> getConditions() { return conditions; }
}1.21.1–1.21.11: as on 26.1.2–26.2, but the entry returns its registered type from
getType()instead of a codec fromcodec(), so the type object is created inMyModLootand registered there.
RepeatEntry and MyModLoot on 1.21.1–1.21.11
package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.world.level.storage.loot.entries.ComposableEntryContainer;
import net.minecraft.world.level.storage.loot.entries.CompositeEntryBase;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.List;
public class RepeatEntry extends CompositeEntryBase {
public static final MapCodec<RepeatEntry> MAP_CODEC = RecordCodecBuilder.mapCodec((i) -> i.group(
LootPoolEntries.CODEC.listOf().optionalFieldOf("children", List.of()).forGetter(RepeatEntry::getChildren),
Codec.INT.fieldOf("times").forGetter(RepeatEntry::getTimes)
).and(commonFields(i).t1()).apply(i, RepeatEntry::new));
private final int times;
private RepeatEntry(List<LootPoolEntryContainer> children, int times, List<LootItemCondition> conditions) {
super(children, conditions);
this.times = times;
}
@Override
public LootPoolEntryType getType() {
return MyModLoot.REPEAT;
}
@Override
protected ComposableEntryContainer compose(List<? extends ComposableEntryContainer> children) {
return (context, output) -> {
for (int n = 0; n < times; n++) {
for (ComposableEntryContainer child : children) {
child.expand(context, output);
}
}
return true;
};
}
public int getTimes() { return times; }
public List<LootPoolEntryContainer> getChildren() { return children; }
public List<LootItemCondition> getConditions() { return conditions; }
}Fabric:
package com.example.mymod.loot;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
public class MyModLoot {
public static final LootPoolEntryType REPEAT = new LootPoolEntryType(RepeatEntry.MAP_CODEC);
public static void register() {
Registry.register(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE, "mymod:repeat", REPEAT);
}
}NeoForge, and Forge on 1.21.1 with the Forge packages named above:
package com.example.mymod.loot;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredRegister;
public class MyModLoot {
public static final LootPoolEntryType REPEAT = new LootPoolEntryType(RepeatEntry.MAP_CODEC);
private static final DeferredRegister<LootPoolEntryType> ENTRIES =
DeferredRegister.create(Registries.LOOT_POOL_ENTRY_TYPE, "mymod");
public static void register(IEventBus modEventBus) {
ENTRIES.register("repeat", () -> REPEAT);
ENTRIES.register(modEventBus);
}
}On 1.20.1 the entry is read from JSON by a
Serializerinstead of a codec, and its children and conditions are arrays — the getters below return them as lists, which is what the later steps expect.
RepeatEntry and MyModLoot on 1.20.1
package com.example.mymod.loot;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.level.storage.loot.entries.ComposableEntryContainer;
import net.minecraft.world.level.storage.loot.entries.CompositeEntryBase;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.List;
public class RepeatEntry extends CompositeEntryBase {
private final int times;
public RepeatEntry(LootPoolEntryContainer[] children, LootItemCondition[] conditions, int times) {
super(children, conditions);
this.times = times;
}
@Override
public LootPoolEntryType getType() {
return MyModLoot.REPEAT;
}
@Override
protected ComposableEntryContainer compose(ComposableEntryContainer[] children) {
return (context, output) -> {
for (int n = 0; n < times; n++) {
for (ComposableEntryContainer child : children) {
child.expand(context, output);
}
}
return true;
};
}
public int getTimes() { return times; }
public List<LootPoolEntryContainer> getChildren() { return List.of(children); }
public List<LootItemCondition> getConditions() { return List.of(conditions); }
public static class Serializer extends LootPoolEntryContainer.Serializer<RepeatEntry> {
@Override
public void serializeCustom(JsonObject json, RepeatEntry entry, JsonSerializationContext context) {
json.add("children", context.serialize(entry.children));
json.addProperty("times", entry.times);
}
@Override
public RepeatEntry deserializeCustom(JsonObject json, JsonDeserializationContext context,
LootItemCondition[] conditions) {
LootPoolEntryContainer[] children = GsonHelper.getAsObject(json, "children", context, LootPoolEntryContainer[].class);
return new RepeatEntry(children, conditions, GsonHelper.getAsInt(json, "times"));
}
}
}Fabric:
package com.example.mymod.loot;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
public class MyModLoot {
public static final LootPoolEntryType REPEAT = new LootPoolEntryType(new RepeatEntry.Serializer());
public static void register() {
Registry.register(BuiltInRegistries.LOOT_POOL_ENTRY_TYPE, "mymod:repeat", REPEAT);
}
}Forge:
package com.example.mymod.loot;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryType;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
public class MyModLoot {
public static final LootPoolEntryType REPEAT = new LootPoolEntryType(new RepeatEntry.Serializer());
private static final DeferredRegister<LootPoolEntryType> ENTRIES =
DeferredRegister.create(Registries.LOOT_POOL_ENTRY_TYPE, "mymod");
public static void register(IEventBus modEventBus) {
ENTRIES.register("repeat", () -> REPEAT);
ENTRIES.register(modEventBus);
}
}Up to 26.1.2 vanilla's
ComposableEntryContainer, the typecomposereturns, is not public, so the entry only compiles once your mod widens it. On Fabric, in the access widener thatfabric.mod.jsonnames underaccessWidener(its header ends inofficialon 26.1.2 and innamedbefore):accessWidener v2 named accessible class net/minecraft/world/level/storage/loot/entries/ComposableEntryContainerOn Forge and NeoForge, in
META-INF/accesstransformer.cfg:public net.minecraft.world.level.storage.loot.entries.ComposableEntryContainer
Because it extends vanilla's CompositeEntryBase, ALI already knows its weight and its children —
that is enough for the loot table to be scanned and for references inside it to be followed. Only the node is
missing: without one, the entry is shown as an unsupported entry, with nothing more than its JSON.
An entry that does not extend CompositeEntryBase registers the two itself, with
registerEntryWeight and registerEntryChildren.
Compile against ALI and ACI and keep both optional at runtime, as described in Getting Started → Depending on ALI.
The node's tooltip needs two keys. Declare and translate them exactly as in Example: loot conditions and functions → translation keys:
REPEAT("mymod.ali.entry.repeat", "Repeat:"),
TIMES("mymod.ali.value.times", "Times: %s"),in the MyLang enum, and in assets/mymod/lang/en_us.json:
{
"mymod.ali.entry.repeat": "Repeat:",
"mymod.ali.value.times": "Times: %s"
}package com.example.mymod.ali;
import com.yanny.aci.tooltip.TooltipNode;
import com.yanny.ali.api.IClientUtils;
import com.yanny.ali.api.IServerUtils;
import com.yanny.ali.api.ListNode;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.Identifier;
public class RepeatNode extends ListNode {
public static final Identifier ID = Identifier.fromNamespaceAndPath("mymod", "repeat");
private final int times;
private final TooltipNode tooltip;
public RepeatNode(int times, TooltipNode tooltip) {
this.times = times;
this.tooltip = tooltip;
}
public RepeatNode(IClientUtils utils, RegistryFriendlyByteBuf buf) {
super(utils, buf);
times = buf.readVarInt();
tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
}
@Override
public void encodeNode(IServerUtils utils, RegistryFriendlyByteBuf buf) {
buf.writeVarInt(times);
buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
}
public int getTimes() {
return times;
}
@Override
public TooltipNode getTooltip() {
return tooltip;
}
@Override
public Identifier getId() {
return ID;
}
}- The first constructor is the server's, the second the client's: it gets back exactly what was sent.
-
ListNodewrites and reads the children itself, before your own data. Sosuper(utils, buf)comes first in the client constructor, andencodeNodewrites your fields in the same order the client constructor reads them — a mismatch corrupts everything after it in the buffer, not only this node. - The tooltip is not sent inline:
getTooltipCache()turns it into an index on the server and back on the client. -
IDpairs the node with its widget; keep it under your own namespace.
Up to 1.21.1
IdentifierisResourceLocation, created withResourceLocation.fromNamespaceAndPathon 1.21.1 andnew ResourceLocationon 1.20.1, where the buffer is also a plainFriendlyByteBuf.
RepeatNode on 1.21.1
package com.example.mymod.ali;
import com.yanny.aci.tooltip.TooltipNode;
import com.yanny.ali.api.IClientUtils;
import com.yanny.ali.api.IServerUtils;
import com.yanny.ali.api.ListNode;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
public class RepeatNode extends ListNode {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath("mymod", "repeat");
private final int times;
private final TooltipNode tooltip;
public RepeatNode(int times, TooltipNode tooltip) {
this.times = times;
this.tooltip = tooltip;
}
public RepeatNode(IClientUtils utils, RegistryFriendlyByteBuf buf) {
super(utils, buf);
times = buf.readVarInt();
tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
}
@Override
public void encodeNode(IServerUtils utils, RegistryFriendlyByteBuf buf) {
buf.writeVarInt(times);
buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
}
public int getTimes() {
return times;
}
@Override
public TooltipNode getTooltip() {
return tooltip;
}
@Override
public ResourceLocation getId() {
return ID;
}
}RepeatNode on 1.20.1
package com.example.mymod.ali;
import com.yanny.aci.tooltip.TooltipNode;
import com.yanny.ali.api.IClientUtils;
import com.yanny.ali.api.IServerUtils;
import com.yanny.ali.api.ListNode;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
public class RepeatNode extends ListNode {
public static final ResourceLocation ID = new ResourceLocation("mymod", "repeat");
private final int times;
private final TooltipNode tooltip;
public RepeatNode(int times, TooltipNode tooltip) {
this.times = times;
this.tooltip = tooltip;
}
public RepeatNode(IClientUtils utils, FriendlyByteBuf buf) {
super(utils, buf);
times = buf.readVarInt();
tooltip = utils.getTooltipCache().getNodeById(buf.readVarInt());
}
@Override
public void encodeNode(IServerUtils utils, FriendlyByteBuf buf) {
buf.writeVarInt(times);
buf.writeVarInt(utils.getTooltipCache().getNodeId(tooltip));
}
public int getTimes() {
return times;
}
@Override
public TooltipNode getTooltip() {
return tooltip;
}
@Override
public ResourceLocation getId() {
return ID;
}
}package com.example.mymod.ali;
import com.example.mymod.loot.RepeatEntry;
import com.yanny.aci.api.NumberExpr;
import com.yanny.aci.tooltip.TooltipBuilder;
import com.yanny.aci.tooltip.TooltipNode;
import com.yanny.ali.api.IDataNode;
import com.yanny.ali.api.IServerUtils;
import net.minecraft.core.Holder;
import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;
import net.minecraft.world.level.storage.loot.functions.LootItemFunction;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.List;
import java.util.stream.Stream;
public class MyModNodes {
public static IDataNode getRepeatNode(IServerUtils utils, RepeatEntry entry, NumberExpr chance, NumberExpr sumWeight,
List<TooltipNode> chanceConditions, List<LootItemFunction> functions,
List<LootItemCondition> conditions) {
List<LootItemCondition> allConditions = Stream.concat(conditions.stream(), entry.getCondition().stream()
.filter(Holder::isBound).flatMap((c) -> utils.unwrapCondition(utils, c.value()).stream())).toList();
List<LootItemFunction> allFunctions = Stream.concat(functions.stream(), entry.getModifier().stream()
.filter(Holder::isBound).flatMap((f) -> utils.unwrapFunction(utils, f.value()).stream())).toList();
List<TooltipNode> sharedChanceConditions = List.copyOf(chanceConditions);
TooltipNode tooltip = TooltipBuilder.array((b) -> {
b.add(utils.getValueTooltip(utils, entry.getTimes()).build(MyLang.TIMES));
}, MyLang.REPEAT).build();
RepeatNode node = new RepeatNode(entry.getTimes(), tooltip);
for (LootPoolEntryContainer child : entry.getChildren()) {
node.addChildren(utils.getEntryFactory(utils, child)
.create(utils, child, chance, sumWeight, sharedChanceConditions, allFunctions, allConditions));
}
return node;
}
}Up to 26.2 the entry has no function of its own, so
functionsare passed on unchanged, and its conditions are already a list (the 1.20.1 getter turns the array into one):package com.example.mymod.ali; import com.example.mymod.loot.RepeatEntry; import com.yanny.aci.api.NumberExpr; import com.yanny.aci.tooltip.TooltipBuilder; import com.yanny.aci.tooltip.TooltipNode; import com.yanny.ali.api.IDataNode; import com.yanny.ali.api.IServerUtils; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; import java.util.List; import java.util.stream.Stream; public class MyModNodes { public static IDataNode getRepeatNode(IServerUtils utils, RepeatEntry entry, NumberExpr chance, NumberExpr sumWeight, List<TooltipNode> chanceConditions, List<LootItemFunction> functions, List<LootItemCondition> conditions) { List<LootItemCondition> allConditions = Stream.concat(conditions.stream(), entry.getConditions().stream()).toList(); List<TooltipNode> sharedChanceConditions = List.copyOf(chanceConditions); TooltipNode tooltip = TooltipBuilder.array((b) -> { b.add(utils.getValueTooltip(utils, entry.getTimes()).build(MyLang.TIMES)); }, MyLang.REPEAT).build(); RepeatNode node = new RepeatNode(entry.getTimes(), tooltip); for (LootPoolEntryContainer child : entry.getChildren()) { node.addChildren(utils.getEntryFactory(utils, child) .create(utils, child, chance, sumWeight, sharedChanceConditions, functions, allConditions)); } return node; } }
- The factory gets what the pool knows about this spot: the chance and the total weight of the pool, the
conditions behind the chance, and the functions and conditions inherited from above. A composite passes them on
to its children, adding its own condition and function — exactly what ALI's own group entry does.
utils.unwrapCondition/unwrapFunctionsplit a combinedall_of/sequenceinto the list ALI works with. -
utils.getEntryFactory(utils, child)builds each child with whatever is registered for it — vanilla items, tags, nested composites, other mods' entries — so your node never needs to know what its children are. - The children keep their own counts; the node only states the rule (Repeat: Times: 3). Anything that has to be computed on the client, such as the multiplied count, would go into the node's own data instead.
Register it, and the node's client half, in the plugin:
package com.example.mymod.ali;
import com.example.mymod.loot.RepeatEntry;
import com.yanny.ali.api.AliEntrypoint;
import com.yanny.ali.api.IClientRegistry;
import com.yanny.ali.api.ICommonRegistry;
import com.yanny.ali.api.IPlugin;
import com.yanny.ali.api.IServerRegistry;
@AliEntrypoint
public class MyModAliPlugin implements IPlugin {
@Override
public String getModId() {
return "mymod";
}
@Override
public void registerCommon(ICommonRegistry registry) {
for (MyLang key : MyLang.values()) {
registry.registerTranslationKey(key.singular());
}
}
@Override
public void registerClient(IClientRegistry registry) {
registry.registerDataNode(RepeatNode.ID, RepeatNode::new);
registry.registerWidget(RepeatNode.ID, RepeatWidget::new);
}
@Override
public void registerServer(IServerRegistry registry) {
registry.registerEntry(RepeatEntry.class, MyModNodes::getRepeatNode);
}
}On Fabric list the class under the ali entrypoint in fabric.mod.json as well — see
Making ALI find the plugin. registerClient only runs on
the client, so a dedicated server never loads the widget class.
package com.example.mymod.ali;
import com.yanny.aci.api.IWidget;
import com.yanny.aci.api.RelativeRect;
import com.yanny.ali.api.IDataNode;
import com.yanny.ali.api.IWidgetUtils;
import com.yanny.ali.api.ListWidget;
import com.yanny.ali.plugin.client.widget.TextureWidget;
import net.minecraft.resources.Identifier;
public class RepeatWidget extends ListWidget {
private static final Identifier ICON = Identifier.fromNamespaceAndPath("mymod", "textures/gui/repeat.png");
public RepeatWidget(IWidgetUtils utils, IDataNode entry, RelativeRect rect, int maxWidth) {
super(utils, entry, rect, maxWidth);
}
@Override
public IWidget getGroupWidget(RelativeRect rect, IDataNode entry) {
TextureWidget icon = new TextureWidget(ICON, new RelativeRect(0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, rect),
0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT);
icon.tooltipText(entry.getTooltip());
return icon;
}
}Up to 1.21.1 the icon is a
ResourceLocation; the class is otherwise the same.
RepeatWidget on 1.21.1
package com.example.mymod.ali;
import com.yanny.aci.api.IWidget;
import com.yanny.aci.api.RelativeRect;
import com.yanny.ali.api.IDataNode;
import com.yanny.ali.api.IWidgetUtils;
import com.yanny.ali.api.ListWidget;
import com.yanny.ali.plugin.client.widget.TextureWidget;
import net.minecraft.resources.ResourceLocation;
public class RepeatWidget extends ListWidget {
private static final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("mymod", "textures/gui/repeat.png");
public RepeatWidget(IWidgetUtils utils, IDataNode entry, RelativeRect rect, int maxWidth) {
super(utils, entry, rect, maxWidth);
}
@Override
public IWidget getGroupWidget(RelativeRect rect, IDataNode entry) {
TextureWidget icon = new TextureWidget(ICON, new RelativeRect(0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, rect),
0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT);
icon.tooltipText(entry.getTooltip());
return icon;
}
}RepeatWidget on 1.20.1
package com.example.mymod.ali;
import com.yanny.aci.api.IWidget;
import com.yanny.aci.api.RelativeRect;
import com.yanny.ali.api.IDataNode;
import com.yanny.ali.api.IWidgetUtils;
import com.yanny.ali.api.ListWidget;
import com.yanny.ali.plugin.client.widget.TextureWidget;
import net.minecraft.resources.ResourceLocation;
public class RepeatWidget extends ListWidget {
private static final ResourceLocation ICON = new ResourceLocation("mymod", "textures/gui/repeat.png");
public RepeatWidget(IWidgetUtils utils, IDataNode entry, RelativeRect rect, int maxWidth) {
super(utils, entry, rect, maxWidth);
}
@Override
public IWidget getGroupWidget(RelativeRect rect, IDataNode entry) {
TextureWidget icon = new TextureWidget(ICON, new RelativeRect(0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, rect),
0, 0, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT, GROUP_WIDGET_WIDTH, GROUP_WIDGET_HEIGHT);
icon.tooltipText(entry.getTooltip());
return icon;
}
}-
ListWidgetlays the children out to the right and draws the branch lines; you only supply the group icon on the left, the one the player hovers to read the node's tooltip. It is a 7 × 18 pixel image,assets/mymod/textures/gui/repeat.png. -
getGroupWidgetis called from inside theListWidgetconstructor, before your own constructor body runs — use only its arguments and static fields there. - The same widget is used in EMI, JEI and REI; nothing viewer-specific is needed.
To reuse one of ALI's own group icons instead of drawing one, return WidgetUtils.getAllWidget(rect, entry) (the
all of icon) — or skip the widget class altogether and register GroupWidget::new for your ID, which draws exactly
that. WidgetUtils lives in com.yanny.ali.plugin.client, GroupWidget in com.yanny.ali.plugin.client.widget,
like TextureWidget.
Start the game with ALI and a recipe viewer and open a loot table that uses your entry: its children are drawn as a
branch behind your icon, and hovering the icon shows Repeat: with Times:. If the client logs
node {mymod:repeat} was not registered, registerDataNode is missing or registered under a different ID than
the node's getId().
Questions: Discord.
Users
Loot Info
Worldgen Info
Developers
Loot Info
Examples
Worldgen Info