Repository navigation
Developers Examples Global Loot Modifier
Forge / NeoForge only. A complete, minimal plugin that makes ALI display one global loot modifier (GLM) of your own mod. The reference behind every step is Global Loot Modifiers; this page only puts the pieces together.
Without a plugin ALI already shows your modifier, as an Auto-detected JSON dump of its fields. After the four steps below it shows the item it adds as a normal drop — with its count and chance and the modifier's conditions — on exactly the loot tables those conditions allow.
Say your mod has a modifier that adds count pieces of item to whatever loot it applies to:
package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import net.neoforged.neoforge.common.loot.LootModifier;
import java.util.Optional;
public class AddItemModifier extends LootModifier {
public static final MapCodec<AddItemModifier> CODEC = RecordCodecBuilder.mapCodec((i) -> codecStart(i).and(i.group(
BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(AddItemModifier::getItem),
Codec.INT.fieldOf("count").forGetter(AddItemModifier::getCount)
)).apply(i, AddItemModifier::new));
private final Item item;
private final int count;
public AddItemModifier(Optional<Holder<LootItemCondition>> condition, int priority, Item item, int count) {
super(condition, priority);
this.item = item;
this.count = count;
}
@Override
public MapCodec<AddItemModifier> codec() {
return CODEC;
}
@Override
protected ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> loot, LootContext context) {
loot.add(new ItemStack(item, count));
return loot;
}
// ALI needs to read these three
public Item getItem() { return item; }
public int getCount() { return count; }
public Optional<Holder<LootItemCondition>> getCondition() { return condition; }
}LootModifier comes from net.neoforged.neoforge.common.loot on NeoForge and net.minecraftforge.common.loot on
Forge. The only thing ALI needs from your class is a way to read its fields — add getters (as above), or make the
fields accessible some other way.
The codec is registered under your mod ID; call MyModLoot.register(modEventBus) from your mod's constructor:
package com.example.mymod.loot;
import com.mojang.serialization.MapCodec;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.common.loot.IGlobalLootModifier;
import net.neoforged.neoforge.registries.DeferredRegister;
import net.neoforged.neoforge.registries.NeoForgeRegistries;
public class MyModLoot {
private static final DeferredRegister<MapCodec<? extends IGlobalLootModifier>> LOOT_MODIFIERS =
DeferredRegister.create(NeoForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "mymod");
public static void register(IEventBus modEventBus) {
LOOT_MODIFIERS.register("add_item", () -> AddItemModifier.CODEC);
LOOT_MODIFIERS.register(modEventBus);
}
}and every modifier instance is a data file in data/mymod/loot_modifiers/, e.g. add_diamond.json:
{
"type": "mymod:add_item",
"condition": {
"type": "neoforge:loot_table_id",
"loot_table_id": "minecraft:blocks/oak_leaves"
},
"item": "minecraft:diamond",
"count": 1
}26.1.2–26.2 (NeoForge):
LootModifierholdsLootItemCondition[] conditionsinstead, and the data file lists them underconditions, each naming its type withcondition.MyModLootis the same as above.
AddItemModifier and its data file 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 it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import net.neoforged.neoforge.common.loot.LootModifier;
public class AddItemModifier extends LootModifier {
public static final MapCodec<AddItemModifier> CODEC = RecordCodecBuilder.mapCodec((i) -> codecStart(i).and(i.group(
BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(AddItemModifier::getItem),
Codec.INT.fieldOf("count").forGetter(AddItemModifier::getCount)
)).apply(i, AddItemModifier::new));
private final Item item;
private final int count;
public AddItemModifier(LootItemCondition[] conditions, int priority, Item item, int count) {
super(conditions, priority);
this.item = item;
this.count = count;
}
@Override
public MapCodec<AddItemModifier> codec() {
return CODEC;
}
@Override
protected ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> loot, LootContext context) {
loot.add(new ItemStack(item, count));
return loot;
}
// ALI needs to read these three
public Item getItem() { return item; }
public int getCount() { return count; }
public LootItemCondition[] getConditions() { return conditions; }
}{
"type": "mymod:add_item",
"conditions": [
{
"condition": "neoforge:loot_table_id",
"loot_table_id": "minecraft:blocks/oak_leaves"
}
],
"item": "minecraft:diamond",
"count": 1
}1.21.1–1.21.11 (NeoForge) and 1.21.1 (Forge): as on 26.1.2–26.2, but
LootModifierhas no priority, and a data file only takes effect once it is listed in the loader'sglobal_loot_modifiers.json. On Forge,LootModifierandIGlobalLootModifierare innet.minecraftforge.common.loot, and the condition isforge:loot_table_id.
AddItemModifier, MyModLoot and the data files 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 it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import net.neoforged.neoforge.common.loot.LootModifier;
public class AddItemModifier extends LootModifier {
public static final MapCodec<AddItemModifier> CODEC = RecordCodecBuilder.mapCodec((i) -> codecStart(i).and(i.group(
BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(AddItemModifier::getItem),
Codec.INT.fieldOf("count").forGetter(AddItemModifier::getCount)
)).apply(i, AddItemModifier::new));
private final Item item;
private final int count;
public AddItemModifier(LootItemCondition[] conditions, Item item, int count) {
super(conditions);
this.item = item;
this.count = count;
}
@Override
public MapCodec<AddItemModifier> codec() {
return CODEC;
}
@Override
protected ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> loot, LootContext context) {
loot.add(new ItemStack(item, count));
return loot;
}
// ALI needs to read these three
public Item getItem() { return item; }
public int getCount() { return count; }
public LootItemCondition[] getConditions() { return conditions; }
}NeoForge registers it exactly as on 26.3 (MyModLoot above). Forge on 1.21.1:
package com.example.mymod.loot;
import com.mojang.serialization.MapCodec;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
public class MyModLoot {
private static final DeferredRegister<MapCodec<? extends IGlobalLootModifier>> LOOT_MODIFIERS =
DeferredRegister.create(ForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "mymod");
public static void register(IEventBus modEventBus) {
LOOT_MODIFIERS.register("add_item", () -> AddItemModifier.CODEC);
LOOT_MODIFIERS.register(modEventBus);
}
}data/mymod/loot_modifiers/add_diamond.json (forge:loot_table_id on Forge):
{
"type": "mymod:add_item",
"conditions": [
{
"condition": "neoforge:loot_table_id",
"loot_table_id": "minecraft:blocks/oak_leaves"
}
],
"item": "minecraft:diamond",
"count": 1
}data/neoforge/loot_modifiers/global_loot_modifiers.json (data/forge/… on Forge):
{
"replace": false,
"entries": [
"mymod:add_diamond"
]
}On 1.20.1 (Forge) the modifier is serialized by a full
Codecinstead of aMapCodec; the rest is as on Forge 1.21.1.
AddItemModifier, MyModLoot and the data files on 1.20.1
package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import net.minecraftforge.common.loot.LootModifier;
public class AddItemModifier extends LootModifier {
public static final Codec<AddItemModifier> CODEC = RecordCodecBuilder.create((i) -> codecStart(i).and(i.group(
BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(AddItemModifier::getItem),
Codec.INT.fieldOf("count").forGetter(AddItemModifier::getCount)
)).apply(i, AddItemModifier::new));
private final Item item;
private final int count;
public AddItemModifier(LootItemCondition[] conditions, Item item, int count) {
super(conditions);
this.item = item;
this.count = count;
}
@Override
public Codec<AddItemModifier> codec() {
return CODEC;
}
@Override
protected ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> loot, LootContext context) {
loot.add(new ItemStack(item, count));
return loot;
}
// ALI needs to read these three
public Item getItem() { return item; }
public int getCount() { return count; }
public LootItemCondition[] getConditions() { return conditions; }
}package com.example.mymod.loot;
import com.mojang.serialization.Codec;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
public class MyModLoot {
private static final DeferredRegister<Codec<? extends IGlobalLootModifier>> LOOT_MODIFIERS =
DeferredRegister.create(ForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS, "mymod");
public static void register(IEventBus modEventBus) {
LOOT_MODIFIERS.register("add_item", () -> AddItemModifier.CODEC);
LOOT_MODIFIERS.register(modEventBus);
}
}data/mymod/loot_modifiers/add_diamond.json:
{
"type": "mymod:add_item",
"conditions": [
{
"condition": "forge:loot_table_id",
"loot_table_id": "minecraft:blocks/oak_leaves"
}
],
"item": "minecraft:diamond",
"count": 1
}data/forge/loot_modifiers/global_loot_modifiers.json:
{
"replace": false,
"entries": [
"mymod:add_diamond"
]
}Compile against ALI and ACI and keep both optional at runtime, exactly as described in Getting Started → Depending on ALI. Your plugin class (and the class from step 3) are the only classes allowed to reference ALI.
package com.example.mymod.ali;
import com.example.mymod.loot.AddItemModifier;
import com.yanny.ali.api.AliEntrypoint;
import com.yanny.ali.plugin.glm.IGlobalLootModifierPlugin;
@AliEntrypoint
public class MyModAliPlugin implements IGlobalLootModifierPlugin {
@Override
public String getModId() {
return "mymod";
}
@Override
public void registerGlobalLootModifier(IRegistry registry) {
registry.registerGlobalLootModifier(AddItemModifier.class, MyModGlm::getAddItemModifier);
}
}-
@AliEntrypointis the whole discovery on Forge / NeoForge; nothing goes intomods.tomlfor it. -
getModId()is your mod ID.registerCommon,registerClientandregisterServerhave empty defaults and are not needed here. - The class is looked up exactly: register every modifier class you have, a subclass does not inherit its parent's registration.
package com.example.mymod.ali;
import com.example.mymod.loot.AddItemModifier;
import com.yanny.aci.api.NumberExpr;
import com.yanny.ali.api.IOperation;
import com.yanny.ali.api.IServerUtils;
import com.yanny.ali.plugin.common.NodeUtils;
import com.yanny.ali.plugin.common.nodes.ItemNode;
import com.yanny.ali.plugin.glm.GlobalLootModifierUtils;
import com.yanny.ali.plugin.glm.IPageLootModifier;
import com.yanny.ali.plugin.server.LootCount;
import com.yanny.ali.plugin.server.TooltipUtils;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.entries.UniformContainerBase;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class MyModGlm {
public static Optional<IPageLootModifier> getAddItemModifier(IServerUtils utils, AddItemModifier modifier) {
return Optional.of(GlobalLootModifierUtils.getLootModifier(utils, modifier, NodeUtils.unwrapConditions(utils, modifier.getCondition()),
(page, conditions) -> List.of(new IOperation.AddOperation((stack) -> true,
addedItem(utils, conditions, modifier.getItem().getDefaultInstance(), modifier.getCount())))));
}
private static ItemNode addedItem(IServerUtils utils, List<LootItemCondition> conditions, ItemStack item, int count) {
NumberExpr countExpr = NumberExpr.constant(count);
var chance = NodeUtils.getChance(utils, conditions, 1f);
var tooltip = TooltipUtils.getTooltip(utils, UniformContainerBase.DEFAULT_QUALITY, chance,
LootCount.of(countExpr), NodeUtils.getCountLimit(item), Collections.emptyList(), conditions);
return new ItemNode(1f, countExpr, item, tooltip.build(), Collections.emptyList(), conditions);
}
}Up to 26.2, on every loader, the conditions are the modifier's array, passed as
Arrays.asList(modifier.getConditions()), and the quality constant isLootPoolSingletonContainer.DEFAULT_QUALITY.
MyModGlm up to 26.2
package com.example.mymod.ali;
import com.example.mymod.loot.AddItemModifier;
import com.yanny.aci.api.NumberExpr;
import com.yanny.ali.api.IOperation;
import com.yanny.ali.api.IServerUtils;
import com.yanny.ali.plugin.common.NodeUtils;
import com.yanny.ali.plugin.common.nodes.ItemNode;
import com.yanny.ali.plugin.glm.GlobalLootModifierUtils;
import com.yanny.ali.plugin.glm.IPageLootModifier;
import com.yanny.ali.plugin.server.LootCount;
import com.yanny.ali.plugin.server.TooltipUtils;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.entries.LootPoolSingletonContainer;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class MyModGlm {
public static Optional<IPageLootModifier> getAddItemModifier(IServerUtils utils, AddItemModifier modifier) {
return Optional.of(GlobalLootModifierUtils.getLootModifier(utils, modifier, Arrays.asList(modifier.getConditions()),
(page, conditions) -> List.of(new IOperation.AddOperation((stack) -> true,
addedItem(utils, conditions, modifier.getItem().getDefaultInstance(), modifier.getCount())))));
}
private static ItemNode addedItem(IServerUtils utils, List<LootItemCondition> conditions, ItemStack item, int count) {
NumberExpr countExpr = NumberExpr.constant(count);
var chance = NodeUtils.getChance(utils, conditions, 1f);
var tooltip = TooltipUtils.getTooltip(utils, LootPoolSingletonContainer.DEFAULT_QUALITY, chance,
LootCount.of(countExpr), NodeUtils.getCountLimit(item), Collections.emptyList(), conditions);
return new ItemNode(1f, countExpr, item, tooltip.build(), Collections.emptyList(), conditions);
}
}What each part does:
-
NodeUtils.unwrapConditionsturns the modifier's single, possibly combined (all_of) condition into the list of conditions ALI works with. -
GlobalLootModifierUtils.getLootModifier(utils, modifier, conditions, operations)does the hard part. It reads the modifier's conditions page by page and decides where the modifier applies — aLootTableIdConditionpins it to the one table it names, a condition needing a parameter a page never has (an entity on a block page, a block state on an entity page) rules it out there. You do not evaluate anything yourself. - The operations lambda is called per page with the conditions that page does not already explain — the table-id condition is gone on the table it names, because the page itself says so. Pass exactly those on to the node, so the tooltip lists only what the player still needs to know.
-
AddOperationadds a node to the page's loot. Its predicate selects the stacks the operation acts on; an added item acts on nothing that is already there, so(stack) -> trueis fine. For modifiers that remove or replace drops useRemoveOperation/ReplaceOperationinstead — see IPageLootModifier. -
ItemNodeis ALI's own node for one item drop, so the result renders exactly like a vanilla drop, with its count, chance and conditions in the tooltip. Its widget is registered by ALI already —registerClientstays empty. Chance1fmeans "always, once the conditions pass";NodeUtils.getChancefolds the conditions' own chances (e.g.random_chance) into it. - Returning
Optional.empty()is allowed and means "this instance adds nothing displayable".
NodeUtils, ItemNode, TooltipUtils and LootCount are ALI's built-in helpers rather than the api package:
their signatures may follow vanilla between Minecraft versions, which is why the example keeps the chance and
tooltip in var. Compile against the ALI jar for your Minecraft version.
Start the game with ALI and a recipe viewer, then open the loot of a block or mob your modifier targets: the added
item is listed among the normal drops, and the server log no longer prints Missing GLM for com.example.mymod.loot.AddItemModifier.
If it does not show up:
-
The modifier applies "everywhere". A modifier with no condition that points at particular tables (only a
random_chance, say) is unbounded and hidden unlessshowUnboundedGlobalLootModifiersis enabled in the config. - The target lives in the modifier's own fields, not in a condition — a list of blocks, a table id, a path fragment. ALI cannot see it there; register a page resolver for the modifier class, see registerPageResolver.
- A condition reads your mod's runtime state. It cannot be decided statically either; a page resolver for that condition class is the answer there too.
Questions: Discord.
Users
Loot Info
Worldgen Info
Developers
Loot Info
Examples
Worldgen Info