MultiLoader 1.21+ · Part 17

Data Generation: Advancements (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 20 min read · Aug 31, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

Advancements are the in-game achievement system and double as the internal progression tracker that many game mechanics (recipe unlocks, statistics) rely on. This tutorial generates a small advancement tree with a root node and two children using NeoForge datagen, which outputs JSON files that work identically on Fabric.

NOTE
Complete the Data Generation: Crafting Recipes tutorial before continuing. We add a new provider to the existing gatherData method.

Advancement Provider

In your NeoForge data package, create ExampleAdvancementProvider that extends AdvancementProvider. The actual advancement logic lives in an inner class implementing AdvancementProvider.AdvancementGenerator:

java
public class ExampleAdvancementProvider extends AdvancementProvider {
public ExampleAdvancementProvider(PackOutput output,
CompletableFuture<HolderLookup.Provider> lookupProvider,
ExistingFileHelper existingFileHelper) {
super(output, lookupProvider, existingFileHelper,
List.of(new ExampleAdvancementGenerator()));
}
private static final class ExampleAdvancementGenerator
implements AdvancementProvider.AdvancementGenerator {
@Override
public void generate(HolderLookup.Provider registries,
Consumer<AdvancementHolder> saver,
ExistingFileHelper existingFileHelper) {
// advancement definitions go here
}
}
}

The saver consumer accepts each finished AdvancementHolder and writes it to JSON. You must call saver.accept(...) (via .save()) for every advancement you build. The existingFileHelper is passed to .save() so NeoForge can validate referenced textures and parents exist.

Root Advancement

Every advancement tree needs a root node. The root is what appears as the top-level entry in the advancements screen tab. It has no parent and typically uses an impossible criterion so it is granted automatically at world load:

java
AdvancementHolder root = Advancement.Builder.advancement()
.display(
ItemRegistry.IRON_STICK.get(),
Component.translatable("advancements.examplemod.root.title"),
Component.translatable("advancements.examplemod.root.description"),
ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/advancements/backgrounds/stone.png"),
AdvancementType.TASK,
false, // show toast
false, // announce to chat
false // hidden
)
.addCriterion("impossible", CriteriaTriggers.IMPOSSIBLE.createCriterion(
new ImpossibleTrigger.TriggerInstance()))
.save(saver, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "root"), existingFileHelper);

The background texture path is shown on the advancement tab when this is the root. Any vanilla texture path works here. Setting the first boolean (showToast) to false suppresses the on-screen popup for the root, since it is never actually granted to the player. Note that .save() now takes existingFileHelper as a third argument — this is required in NeoForge 1.21.1.

Child Advancements

Child advancements use .parent(root) to link to their parent node. The first child grants when the player picks up an Iron Stick:

java
AdvancementHolder getIronStick = Advancement.Builder.advancement()
.parent(root)
.display(
ItemRegistry.IRON_STICK.get(),
Component.translatable("advancements.examplemod.get_iron_stick.title"),
Component.translatable("advancements.examplemod.get_iron_stick.description"),
null, // null background for non-root nodes
AdvancementType.TASK,
true,
true,
false
)
.addCriterion("has_iron_stick", InventoryChangeTrigger.TriggerInstance.hasItems(
ItemRegistry.IRON_STICK.get()))
.save(saver, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "get_iron_stick"), existingFileHelper);
AdvancementHolder placeNewDirt = Advancement.Builder.advancement()
.parent(root)
.display(
BlockRegistry.NEW_DIRT.get().asItem(),
Component.translatable("advancements.examplemod.place_new_dirt.title"),
Component.translatable("advancements.examplemod.place_new_dirt.description"),
null,
AdvancementType.GOAL,
true,
true,
false
)
.addCriterion("placed_new_dirt", ItemUsedOnLocationTrigger.TriggerInstance.placedBlock(
BlockRegistry.NEW_DIRT.get()))
.save(saver, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "place_new_dirt"), existingFileHelper);

Criteria and Rewards

Multiple criteria can be combined. By default all must be met; use .requirements(AdvancementRequirements.anyOf(...)) to require only one of several criteria. To reward the player with a recipe unlock add:

java
Advancement.Builder.advancement()
// ...
.rewards(AdvancementRewards.Builder.experience(0)
.addRecipe(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "new_dirt_from_stick")))
.save(saver, ..., existingFileHelper);

Other reward types are .experience(int), .addLootTable(ResourceKey) (grants a loot table's drops), and .runs(ResourceLocation) (runs a data pack function).

Add translation keys for the new advancements to your en_us.json:

json
"advancements.examplemod.root.title": "Example Mod",
"advancements.examplemod.root.description": "Begin your journey with Example Mod.",
"advancements.examplemod.get_iron_stick.title": "A New Tool",
"advancements.examplemod.get_iron_stick.description": "Pick up an Iron Stick.",
"advancements.examplemod.place_new_dirt.title": "Breaking Ground",
"advancements.examplemod.place_new_dirt.description": "Place a New Dirt block."

Registering the Provider

Register the provider in gatherData. Unlike a standard provider, you pass existingFileHelper from the event directly to the constructor:

java
generator.addProvider(
event.includeServer(),
new ExampleAdvancementProvider(output, registries, existingFileHelper));
}

Running Datagen

Run NeoForge Data. The generated files appear at:

text
common/src/generated/resources/data/examplemod/advancement/root.json
common/src/generated/resources/data/examplemod/advancement/get_iron_stick.json
common/src/generated/resources/data/examplemod/advancement/place_new_dirt.json

Launch the client and open the advancements screen (L by default). You should see the Example Mod tab with the root node and two child branches. Pick up an Iron Stick to trigger the first child; it should show a toast and chat message.

TIP
Use /advancement grant @s only examplemod:root to manually grant the root node and see the full tree in the advancements screen, since the impossible criterion never fires naturally.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Data Generation: Language Files (MultiLoader 1.21+)

Replace your hand-written en_us.json with a LanguageProvider that generates translations from your Java registries, using typed addItem and addBlock helpers and a generic add() for advancement and subtitle keys.

Continue →