Skip to content

Developers Loot Info Global Loot Modifiers

Yanny edited this page Oct 3, 2026 · 8 revisions

Global Loot Modifiers (developers)

Forge / NeoForge only. If your mod ships datapack global loot modifiers (GLM), ALI can already show them — as an Auto-detected JSON dump. To get them rendered properly, implement the GLM plugin interface. A complete minimal plugin is walked through in Example: a Global Loot Modifier.

The interface

com.yanny.ali.plugin.glm.IGlobalLootModifierPlugin extends IPlugin with one extra method:

@AliEntrypoint
public class MyModAliPlugin implements IGlobalLootModifierPlugin {
    @Override
    public String getModId() {
        return "mymod";
    }

    @Override
    public void registerGlobalLootModifier(IRegistry registry) {
        registry.registerGlobalLootModifier(MyAddItemModifier.class, MyGlm::getAddItemModifier);
    }

    // registerCommon / registerClient / registerServer as usual
}

The getter has the shape BiFunction<IServerUtils, T, Optional<IPageLootModifier>>. Returning Optional.empty() means "this instance adds nothing displayable" and is a valid answer.

Building the IPageLootModifier by hand is rarely necessary. Hand the modifier itself and its own conditions to GlobalLootModifierUtils.getLootModifier(utils, modifier, conditions, operationSupplier) instead and ALI judges it against every page for you, reading the conditions to decide where it can and cannot apply:

public static Optional<IPageLootModifier> getAddItemModifier(IServerUtils utils, MyAddItemModifier modifier) {
    return Optional.of(GlobalLootModifierUtils.getLootModifier(utils, modifier, NodeUtils.unwrapConditions(utils, modifier.getCondition()),
            (page, conditions) -> List.of(new IOperation.AddOperation((stack) -> true, myNode(utils, conditions, modifier)))));
}

getCondition() exposes the Optional<Holder<LootItemCondition>> your LootModifier holds; NodeUtils.unwrapConditions (com.yanny.ali.plugin.common) turns that single, possibly combined condition into the list ALI works with.

Up to 26.2: a LootModifier holds a LootItemCondition[] conditions array — expose it as getConditions() and pass it as a list:

public static Optional<IPageLootModifier> getAddItemModifier(IServerUtils utils, MyAddItemModifier modifier) {
    return Optional.of(GlobalLootModifierUtils.getLootModifier(utils, modifier, Arrays.asList(modifier.getConditions()),
            (page, conditions) -> List.of(new IOperation.AddOperation((stack) -> true, myNode(utils, conditions, modifier)))));
}

The operation supplier is called per page, with that page (LootPage — its table id, its loot parameters, and the blocks or entity types it stands for) and the conditions still worth showing there: a condition the page fully accounts for is left out, because the page already says it.

Most conditions need nothing from you. ALI decides a condition it has no resolver for by running it against a context built for that page, which answers what the page knows — the loot parameters it is rolled with, its table id — and refuses every read it cannot. A condition needing a parameter the page never has rules the modifier out by itself, and AllOf / AnyOf / inverted groups are descended into.

Two cases do need telling, and registerPageResolver covers both:

  • A condition only your mod understands. One that reads your own runtime state, rather than the loot context, cannot be decided by running it. Register a resolver for that condition class.
  • A modifier that keeps its target in its own fields — a table id, a path fragment, a list of blocks — says nothing in any condition. Register a resolver for the modifier class; that is what the modifier argument above is passed for. It is read for modifiers ALI renders as Auto-detected too, so a resolver alone is enough to place an otherwise unadapted modifier on the right pages.

A condition of yours that reads the queried loot table id from its own carrier is the third case, and it wants registerLootContextPreparer rather than a resolver: fill the carrier and the condition decides itself.

null from a resolver means "no opinion", and every condition plus the modifier itself is consulted, so a modifier may be pinned by its fields and constrained by its conditions at the same time.

If your modifier's node is one of ALI's own, such as ModifiedNode, its widget and client constructor are already registered — registerClient needs nothing for it. Only a node type of your own needs registering, as described in Nodes and Widgets.

IPageLootModifier

An IPageLootModifier answers two questions, both about one page:

Member Meaning
PageMatch test(LootPage page) whether it applies here — Match.YES, Match.NO, or Match.UNKNOWN when nothing settles it — together with the conditions this page does not already account for
List<IOperation> getOperations(LootPage page, PageMatch match) what it does there, given the result of test for the same page

PageMatch.NO is the ready-made answer for a page the modifier never applies to. A hand-built modifier that has no conditions to hide passes all of its conditions as the unexplained ones, and the operations then show them in the tooltip.

IOperation is a sealed set of three records, each carrying a Predicate<ItemStack> selecting the stacks it acts on:

  • AddOperation(predicate, node) — adds a node to the matched loot,
  • RemoveOperation(predicate, factory) — replaces a matched node with a "removed" rendering,
  • ReplaceOperation(predicate, factory) — replaces a matched node with one or more other nodes.

These are the same operations ALI's own GLM support and the LootJS module build, so a modifier registered this way is displayed exactly like a built-in one.

A modifier is bound on a page when nothing answers Match.NO there and at least one condition — or the modifier itself — answers Match.YES. A modifier that comes out Match.UNKNOWN everywhere, pointing at nothing in particular and deciding at runtime, is shown only with showUnboundedGlobalLootModifiers enabled in the config, since one such modifier can otherwise appear on hundreds of pages; it then lands wherever nothing rules it out.

A bound modifier is also checked against the items of the page before it is attached: it is kept when one of its operations matches an item that page can produce, or when the page produces no items at all. A modifier that matches nothing anywhere is dropped, and the count of those is logged at startup.

Non-GLM loot injection

If your mod injects loot without the datapack GLM system (including anything on Fabric), do not use this interface. Use registerLootModifiers on IServerRegistry instead. Those are ILootModifiers — the same IOperations, but bound to a block, an entity type or one loot table by hand instead of being judged per page — and they are registered directly:

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

See the Registration Reference.

Clone this wiki locally