Advancement Generation 26.1.2
A guide to setting up advancement generation with datagen.
PREREQUISITES
Make sure you've completed the datagen setup process first.
Setup
First, we need to make our provider. Create a class that extends FabricAdvancementProvider and fill out the base methods:
java
public class ExampleModAdvancementProvider extends FabricAdvancementProvider {
protected ExampleModAdvancementProvider(FabricPackOutput output, CompletableFuture<HolderLookup.Provider> registryLookup) {
super(output, registryLookup);
}
@Override
public void generateAdvancement(HolderLookup.Provider wrapperLookup, Consumer<AdvancementHolder> consumer) {
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
To finish setup, add this provider to your DataGeneratorEntrypoint within the onInitializeDataGenerator method.
java
pack.addProvider(ExampleModAdvancementProvider::new);1
Advancement Structure
An advancement is made up a few different components. Along with the requirements, called "criterion", it may have:
- Some
DisplayInfothat tell the game how to show the advancement to players, AdvancementRequirements, which are lists of lists of criteria, requiring at least one criterion from each sub-list to be completed,AdvancementRewards, which the player receives for completing the advancement.- A
Strategy, which tells the advancement how to handle multiple criterion, and - A parent
Advancement, which organizes the hierarchy you see on the "Advancements" screen.
Simple Advancements
Here's a simple advancement for getting a dirt block:
java
AdvancementHolder getDirt = Advancement.Builder.advancement()
.display(
Items.DIRT, // The display icon
Component.literal("Your First Dirt Block"), // The title
Component.literal("Now make a house from it"), // The description
Identifier.withDefaultNamespace("gui/advancements/backgrounds/adventure"), // Background image for the tab in the advancements page, if this is a root advancement (has no parent)
AdvancementType.TASK, // TASK, CHALLENGE, or GOAL
true, // Show the toast when completing it
true, // Announce it to chat
false // Hide it in the advancement tab until it's achieved
)
// "got_dirt" is the name referenced by other advancements when they want to have "requirements."
.addCriterion("got_dirt", InventoryChangeTrigger.TriggerInstance.hasItems(Items.DIRT))
// Give the advancement an id
.save(consumer, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "get_dirt"));1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JSON Output
json
{
"criteria": {
"got_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
}
},
"display": {
"background": "minecraft:gui/advancements/backgrounds/adventure",
"description": "Now make a house from it",
"icon": {
"id": "minecraft:dirt"
},
"title": "Your First Dirt Block"
},
"requirements": [
[
"got_dirt"
]
],
"sends_telemetry_event": true
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Parents
In order to create or extend a tree of advancements, we can set a parent for our advancement. To do this, call Advancement.Builder#parent(...) and pass in a reference to the parent advancement.
java
Advancement.Builder.advancement()
.parent(getDirt)
// ...1
2
3
2
3
If no direct reference to the parent advancement is available (e.g. using a vanilla advancement as a parent), a placeholder can be created using an identifier.
java
Advancement.Builder.advancement()
.parent(createPlaceholder(Identifier.withDefaultNamespace("adventure/root")))
// ...1
2
3
2
3
Your advancements should now be shown as a tree in the advancement menu.

Multiple Criteria
To have more advanced conditions in our advancements, we can call Advancement.Builder#addCriteria(...) more than once with additional criteria.
java
Advancement.Builder.advancement()
.addCriterion("ate_apple", ConsumeItemTrigger.TriggerInstance.usedItem(itemLookup, Items.APPLE))
.addCriterion("ate_cooked_beef", ConsumeItemTrigger.TriggerInstance.usedItem(itemLookup, Items.COOKED_BEEF))
// ...1
2
3
4
2
3
4
By default, all criteria must be met for the advancement to be completed. We can change this behavior by supplying a different strategy.
java
Advancement.Builder.advancement()
.addCriterion("brew_mundane", CriteriaTriggers.BREWED_POTION.createCriterion(
new BrewedPotionTrigger.TriggerInstance(Optional.empty(), Optional.of(Potions.MUNDANE))
))
.addCriterion("brew_thick", CriteriaTriggers.BREWED_POTION.createCriterion(
new BrewedPotionTrigger.TriggerInstance(Optional.empty(), Optional.of(Potions.THICK))
))
.requirements(AdvancementRequirements.Strategy.OR)
// ...1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Rewards
We can attach rewards to our advancements, which will be granted when a player completes the advancement. We can do this by calling Advancement.Builder#rewards(...) with the rewards we want to add.
java
Advancement.Builder.advancement()
.rewards(AdvancementRewards.Builder.experience(10))
// ...1
2
3
2
3
There are multiple other reward types available:
java
Advancement.Builder.advancement()
.rewards(
new AdvancementRewards.Builder()
// Give entries from a loot table
.addLootTable(ModLootTables.ADVANCEMENT_COLLECT_NETHER_STAR)
// Make recipes available in the recipe book
.addRecipe(RecipeBuilder.getDefaultRecipeId(new ItemStackTemplate(Items.BEACON)))
// Run a .mcfunction - https://minecraft.wiki/w/Function_(Java_Edition)
.runs(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "got_nether_star"))
// Give experience points
.addExperience(200)
)
// ...1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Custom Criteria
WARNING
While datagen can be on the client side, Criterions and Predicates are in the main source set (both sides), since the server needs to trigger and evaluate them.
Definitions
A criterion (plural: criteria) is something a player can do (or that can happen to a player) that may be counted towards an advancement. The game comes with many criteria, which can be found in the net.minecraft.advancements.criterion package. Generally, you'll only need a new criterion if you implement a custom mechanic into the game.
Conditions are evaluated by criteria. A criterion is only counted if all the relevant conditions are met. Conditions are usually expressed with a predicate.
A predicate is something that takes a value and returns a boolean. For example, a Predicate<Item> might return true if the item is a diamond, while a Predicate<LivingEntity> might return true if the entity is not hostile to villagers.
Creating Custom Criteria
First, we'll need a new mechanic to implement. Let's tell the player what tool they used every time they break a block.
java
public class ExampleModDatagenAdvancement implements ModInitializer {
@Override
public void onInitialize() {
HashMap<Item, Integer> tools = new HashMap<>();
PlayerBlockBreakEvents.AFTER.register(((level, player, blockPos, blockState, blockEntity) -> {
if (player instanceof ServerPlayer serverPlayer) { // Only triggers on the server side
Item item = player.getMainHandItem().getItem();
Integer usedCount = tools.getOrDefault(item, 0);
usedCount++;
tools.put(item, usedCount);
serverPlayer.sendSystemMessage(Component.nullToEmpty("You've used \"" + item + "\" as a tool " + usedCount + " times!"));
}
}));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Note that this code is really bad. The HashMap is not stored anywhere persistent, so it will be reset every time the game is restarted. It's just to show off Criterions. Start the game and try it out!
Next, let's create our custom criterion, UseToolCriterion. It's going to need its own Conditions class to go with it, so we'll make them both at once:
java
public class UseToolCriterion extends SimpleCriterionTrigger<UseToolCriterion.Conditions> {
@Override
public Codec<Conditions> codec() {
return Conditions.CODEC;
}
public record Conditions(Optional<ContextAwarePredicate> playerPredicate) implements SimpleCriterionTrigger.SimpleInstance {
public static Codec<UseToolCriterion.Conditions> CODEC = ContextAwarePredicate.CODEC.optionalFieldOf("player")
.xmap(Conditions::new, Conditions::player).codec();
@Override
public Optional<ContextAwarePredicate> player() {
return this.playerPredicate;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Whew, that's a lot! Let's break it down.
UseToolCriterionis aSimpleCriterionTrigger, whichConditionscan apply to.Conditionshas aplayerPredicatefield. AllConditionsshould have a player predicate (technically aLootContextPredicate).Conditionsalso has aCODEC. ThisCodecis simply the codec for its one field,playerPredicate, with extra instructions to convert between them (xmap).
INFO
To learn more about codecs, see the Codecs page.
We're going to need a way to check if the conditions are met. Let's add a helper method to Conditions:
java
public boolean requirementsMet() {
return true; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}1
2
3
2
3
Now that we've got a criterion and its conditions, we need a way to trigger it. Add a trigger method to UseToolCriterion:
java
public void trigger(ServerPlayer player) {
trigger(player, Conditions::requirementsMet);
}1
2
3
2
3
Almost there! Next, we need an instance of our criterion to work with. Let's put it in a new class, called ModCriteria.
java
public class ModCriteria {
public static final UseToolCriterion USE_TOOL = CriteriaTriggers.register(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "use_tool").toString(), new UseToolCriterion());
}1
2
3
4
2
3
4
To make sure that our criteria are initialized at the right time, add a blank init method:
java
// :::datagen-advancements:mod-criteria
public static final UseToolCriterion USE_TOOL = CriteriaTriggers.register(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "use_tool").toString(), new UseToolCriterion());
// :::datagen-advancements:mod-criteria
// :::datagen-advancements:new-mod-criteria
public static final ParameterizedUseToolCriterion PARAMETERIZED_USE_TOOL = CriteriaTriggers.register(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "parameterized_use_tool").toString(), new ParameterizedUseToolCriterion());
// :::datagen-advancements:mod-criteria1
2
3
4
5
6
7
2
3
4
5
6
7
And call it in your mod initializer:
java
ModCriteria.init();1
Finally, we need to trigger our criterion. Add this to where we sent a message to the player in the main mod class.
java
ModCriteria.USE_TOOL.trigger(serverPlayer);1
Your shiny new criterion is now ready to use! Let's add it to our provider:
java
AdvancementHolder breakBlockWithTool = Advancement.Builder.advancement()
.parent(getDirt)
.display(
Items.DIAMOND_SHOVEL,
Component.literal("Not a Shovel"),
Component.literal("That's not a shovel (probably)"),
null,
AdvancementType.GOAL,
true,
true,
false
)
.addCriterion("break_block_with_tool", ModCriteria.USE_TOOL.createCriterion(new UseToolCriterion.Conditions(Optional.empty())))
.save(consumer, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "break_block_with_tool"));1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Run the datagen task again, and you've got your new advancement to play with!
Conditions with Parameters
This is all well and good, but what if we want to only grant an advancement once we've done something 5 times? And why not another one at 10 times? For this, we need to give our condition a parameter. You can stay with UseToolCriterion, or you can follow along with a new ParameterizedUseToolCriterion. In practice, you should only have the parameterized one, but we'll keep both for this tutorial.
Let's work bottom-up. We'll need to check if the requirements are met, so let's edit our Conditions#requirementsMet method:
java
public boolean requirementsMet(int totalTimes) {
return totalTimes > this.requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}1
2
3
4
2
3
4
requiredTimes doesn't exist, so make it a parameter of Conditions:
java
public record Conditions(Optional<ContextAwarePredicate> playerPredicate, int requiredTimes) implements SimpleCriterionTrigger.SimpleInstance {
@Override
public Optional<ContextAwarePredicate> player() {
return this.playerPredicate;
}
// :::datagen-advancements:new-requirements-met
public boolean requirementsMet(int totalTimes) {
return totalTimes > this.requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
// :::datagen-advancements:new-requirements-met
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Now our codec is erroring. Let's write a new codec for the new changes:
java
public static Codec<ParameterizedUseToolCriterion.Conditions> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ContextAwarePredicate.CODEC.optionalFieldOf("player").forGetter(Conditions::player),
Codec.INT.fieldOf("requiredTimes").forGetter(Conditions::requiredTimes)
).apply(instance, Conditions::new));
// :::datagen-advancements:new-parameter
@Override
public Optional<ContextAwarePredicate> player() {
return this.playerPredicate;
}
// :::datagen-advancements:new-requirements-met
public boolean requirementsMet(int totalTimes) {
return totalTimes > this.requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
// :::datagen-advancements:new-requirements-met
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Moving on, we now need to fix our trigger method:
java
public void trigger(ServerPlayer player, int totalTimes) {
this.trigger(player, conditions -> conditions.requirementsMet(totalTimes));
}1
2
3
4
2
3
4
If you've made a new criterion, we need to add it to ModCriteria
java
public static final ParameterizedUseToolCriterion PARAMETERIZED_USE_TOOL = CriteriaTriggers.register(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "parameterized_use_tool").toString(), new ParameterizedUseToolCriterion());
// :::datagen-advancements:mod-criteria
// :::datagen-advancements:mod-criteria-init
public static void init() {
}1
2
3
4
5
6
7
2
3
4
5
6
7
And call it in our main class, right where the old one is:
java
ModCriteria.PARAMETERIZED_USE_TOOL.trigger(serverPlayer, usedCount);1
Add the advancement to your provider:
java
AdvancementHolder breakBlockWithToolFiveTimes = Advancement.Builder.advancement()
.parent(breakBlockWithTool)
.display(
Items.GOLDEN_SHOVEL,
Component.literal("Not a Shovel Still"),
Component.literal("That's still not a shovel (probably)"),
null,
AdvancementType.GOAL,
true,
true,
false
)
.addCriterion("break_block_with_tool_five_times", ModCriteria.PARAMETERIZED_USE_TOOL.createCriterion(new ParameterizedUseToolCriterion.Conditions(Optional.empty(), 5)))
.save(consumer, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "break_block_with_tool_five_times"));1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Run datagen again, and you're finally done!





