Advancements are the in-game achievement system that also drives recipe unlocks. Rather than hand-writing JSON advancement files, you can generate them through datagen using AdvancementProvider. This tutorial creates a small advancement tree rooted on picking up the Iron Stick, with a child for obtaining New Dirt.
NOTE
Complete Data Generation: Crafting Recipes before continuing. We add a new provider to the existing gatherData listener.
Advancement Sub-Provider
AdvancementProvider does not generate advancements itself — it delegates to a list of AdvancementSubProvider instances, each of which generates a logical group of advancements. Create a class implementing the interface in your NeoForge data package:
public class ExampleAdvancementProvider implements AdvancementSubProvider {
@Override
public void generate(HolderLookup.Provider registries,
Consumer<AdvancementHolder> saver) {
// advancement definitions go here
}
}
The saver consumer writes each finished advancement to disk. You call save(saver, identifier) at the end of each builder chain, passing it the consumer and the advancement's resource location.
Root Advancement
Every advancement tree needs a root node. Root advancements have no parent, display a background texture, and typically do not announce in chat or show a toast so they do not spam the player. Use Advancement.Builder.advancement() to start a builder with telemetry enabled:
AdvancementHolder root = Advancement.Builder.advancement()
.display(
new ItemStack(ItemRegistry.IRON_STICK.get()),
Component.translatable("advancements.examplemod.root.title"),
Component.translatable("advancements.examplemod.root.description"),
Identifier.fromNamespaceAndPath("minecraft",
"textures/gui/advancements/backgrounds/stone.png"),
AdvancementType.TASK,
false, // showToast
false, // announceToChat
false) // hidden
.addCriterion("has_iron_stick",
InventoryChangeTrigger.TriggerInstance.hasItems(ItemRegistry.IRON_STICK.get()))
.save(saver, Identifier.fromNamespaceAndPath(Constants.MOD_ID, "root"));
The seven arguments after the background path are: the AdvancementType (TASK, GOAL, or CHALLENGE), whether to show a toast notification, whether to announce in chat, and whether to hide the advancement from the tab until it is earned. Add this call inside generate().
TIP
You can reuse any of Minecraft's built-in background textures by pointing at a path under minecraft:textures/gui/advancements/backgrounds/. Common choices are stone.png, dirt.png, and netherrack.png.
Child Advancement
Child advancements call parent(AdvancementHolder) with the holder returned by the parent's save call. Pass null as the background since only root advancements display one. Here we reward the player for picking up a New Dirt block and show a toast:
Advancement.Builder.advancement()
.parent(root)
.display(
new ItemStack(BlockRegistry.NEW_DIRT.get()),
Component.translatable("advancements.examplemod.new_dirt.title"),
Component.translatable("advancements.examplemod.new_dirt.description"),
null,
AdvancementType.TASK,
true, // showToast
true, // announceToChat
false)
.addCriterion("has_new_dirt",
InventoryChangeTrigger.TriggerInstance.hasItems(BlockRegistry.NEW_DIRT.get()))
.save(saver, Identifier.fromNamespaceAndPath(Constants.MOD_ID, "new_dirt"));
The advancement translation keys (advancements.examplemod.root.title, etc.) are plain language file entries. Add them to your ExampleLanguageProvider using the raw add(String, String) overload:
add("advancements.examplemod.root.title", "Example Mod");
add("advancements.examplemod.root.description", "Begin your journey with the Example Mod.");
add("advancements.examplemod.new_dirt.title", "Dirty Hands");
add("advancements.examplemod.new_dirt.description", "Obtain a New Dirt block.");
Registering the Provider
Advancements are server-side data, so register the provider inside gatherData. Pass a list containing your sub-provider to AdvancementProvider:
public static void gatherData(GatherDataEvent event) {
DataGenerator generator = event.getGenerator();
PackOutput output = generator.getPackOutput();
CompletableFuture<HolderLookup.Provider> registries = event.getLookupProvider();
generator.addProvider(true,
new LootTableProvider(output, Set.of(),
List.of(new LootTableProvider.SubProviderEntry(
ExampleBlockLootTableProvider::new,
LootContextParamSets.BLOCK
)), registries));
generator.addProvider(true,
new ExampleRecipeProvider.Runner(output, registries));
generator.addProvider(true,
new ExampleBlockTagsProvider(output, registries));
generator.addProvider(true,
new ExampleItemTagsProvider(output, registries));
generator.addProvider(true,
new AdvancementProvider(output, registries,
List.of(new ExampleAdvancementProvider())));
}
Running Datagen
Run the NeoForge Data configuration. Check common/src/generated/resources/data/examplemod/advancement/ for two new files:
// data/examplemod/advancement/root.json (excerpt)
{
"criteria": {
"has_iron_stick": {
"trigger": "minecraft:inventory_changed",
"conditions": { "items": [{ "items": "examplemod:iron_stick" }] }
}
},
"display": {
"icon": { "id": "examplemod:iron_stick" },
"title": { "translate": "advancements.examplemod.root.title" },
"description": { "translate": "advancements.examplemod.root.description" },
"background": "minecraft:textures/gui/advancements/backgrounds/stone.png",
"frame": "task",
"show_toast": false,
"announce_to_chat": false,
"hidden": false
}
}
TIP
To add multi-step criteria that all need to be met, call addCriterion multiple times and then call requirements(AdvancementRequirements.allOf(...)) or requirements(AdvancementRequirements.anyOf(...)) to control whether the player needs all of them or just one.
You can find the source for this tutorial here:
View Source on GitHub NEXT IN SERIES
Data Generation: Sound Definitions (MultiLoader 26.1+)
Register a custom SoundEvent with RegistrationProvider, generate the sounds.json entry using SoundDefinitionsProvider, and add a subtitle translation key.
Continue →