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:
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:
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:
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:
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:
"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:
generator.addProvider(
event.includeServer(),
new ExampleAdvancementProvider(output, registries, existingFileHelper));
}
Running Datagen
Run NeoForge Data. The generated files appear at:
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 →