User Tools

Site Tools


tutorial:datagen_advancements

This is an old revision of the document!


Advancements Generation

One way to make a mod feel more integrated into Minecraft is for it to generate custom advancements. How do we do that?

Before continuing

Make sure you've to read the first section of the Getting started with Data Generation page, have a class that implements DataGeneratorEntrypoint, and know about the gradle task that needs to be called after any change in our data generators.

Hooking Up the Provider

To begin making custom advancements, we need to hook up an advancement generator to the class which implements DataGeneratorEntrypoint as follows:

import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricAdvancementProvider;
import net.minecraft.advancement.*;
import java.util.function.Consumer;
 
public class ExampleModDataGenerator implements DataGeneratorEntrypoint {
 
    @Override
    public void onInitializeDataGenerator(FabricDataGenerator generator) {
        FabricDataGenerator.Pack pack = generator.createPack();
 
        pack.addProvider(AdvancementsProvider::new);
    }
 
    static class AdvancementsProvider extends FabricAdvancementProvider {
        protected AdvancementsProvider(FabricDataOutput dataGenerator) {
            super(dataGenerator);
        }
 
        @Override
        public void generateAdvancement(Consumer<AdvancementEntry> consumer) {
            // 
            // We will create our custom advancements here...
            //
        }
    }
}

Simple Advancement

Let's start simple and work our way up to custom criterions. We'll start with an advancement that activates after you pick up your first dirt block, and we're going to add it to the function generateAdvancement inside the AdvancementsProvider class we just wrote.

// ... (Previous imports)
import net.minecraft.advancement.criterion.InventoryChangedCriterion;
import net.minecraft.item.Items;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
 
public class ExampleModDataGenerator implements DataGeneratorEntrypoint {
 
    // ... (Rest of the code)
 
    static class AdvancementsProvider extends FabricAdvancementProvider {
 
        // ... (Rest of the code)
 
        @Override
        public void generateAdvancement(Consumer<AdvancementEntry> consumer) {
            AdvancementEntry rootAdvancement = Advancement.Builder.create()
                    .display(
                            Items.DIRT, // The display icon
                            Text.literal("Your First Dirt Block"), // The title
                            Text.literal("Now make a three by three"), // The description
                            new Identifier("textures/gui/advancements/backgrounds/adventure.png"), // Background image used
                            AdvancementFrame.TASK, // Options: TASK, CHALLENGE, GOAL
                            true, // Show toast top right
                            true, // Announce to chat
                            false // Hidden in the advancement tab
                    )
                    // The first string used in criterion is the name referenced by other advancements when they want to have 'requirements'
                    .criterion("got_dirt", InventoryChangedCriterion.Conditions.items(Items.DIRT))
                    .build(consumer, "your_mod_id_please_change_me" + "/root");
        }
    }
}
  • Make sure you change the your_mod_id_please_change_me string to the name of your mod. (Also leave the “/root” part as is).

I'll explain in more detail what everything means, but if you compile your program now, and jump into a world in minecraft, you'll notice nothing happens. That's because we haven't generated the data. We haven't ran the runDatagen gradle task we created earlier and we need to do that every time we add, modify, or remove one of our custom made advancements, otherwise the change won't be reflected in the game.

If you have a configuration on IntelliJ IDEA that runs the gradle task you can use that, or you can open your projects root folder on the terminal and run:

Windows
gradlew runDatagen
Linux
./gradlew runDatagen

In the src/main/generated/data/minecraft/advancements/yourmodid/ folder we talked about before, you should now see a file root.json which holds our advancements data. Something like this:

{
  "criteria": {
    "got_dirt": {
      "conditions": {
        "items": [
          {
            "items": [
              "minecraft:dirt"
            ]
          }
        ]
      },
      "trigger": "minecraft:inventory_changed"
    }
  },
  "display": {
    "announce_to_chat": true,
    "background": "minecraft:textures/gui/advancements/backgrounds/adventure.png",
    "description": {
      "text": "Now make a three by three"
    },
    "frame": "task",
    "hidden": false,
    "icon": {
      "item": "minecraft:dirt"
    },
    "show_toast": true,
    "title": {
      "text": "Your First Dirt Block"
    }
  },
  "requirements": [
    [
      "got_dirt"
    ]
  ]
}

Go ahead and run the game now and see if the advancement works by collecting a dirt block. You should even be able to leave the world, come back, collect another dirt block and notice that there is no re-trigger. If you press Escape and open up the Advancements tab, you should see our advancement with it's title and description, on it's own tab, separate from the vanilla advancements.

  • NOTE: You have to complete one advancement in the tab group to open it up, otherwise the tab wont show (just in case you were wondering were the vanilla advancements were).

Advancements Explained

All advancements in minecraft look like that root.json file we generated. In fact, it is not at all required to write any code to make advancements, as long as your mods blocks, items, weapons, and so on are registered on its given registry (how that's done for blocks for example), you could reference any custom items your mod adds, be it food, or whatever and make advancements with them like if they were vanilla using datapacks. We still recommend you follow this method instead as it's far more durable than writing out advancements by hand.

Let's go through the advancement we created step by step and see the options we have. We start by calling the Advancement.Builder.create() and assigning it to the variable rootAdvancement. (We'll be making use of this later).

AdvancementEntry rootAdvancement = Advancement.Builder.create()

Then we call the chain method 'display' on it, which takes seven arguments.

.display(
    /** This is the item that gets used as the icon (You can use any of your mods icons as long as they're registered) */
    Items.DIRT,
 
    /** This is the text that gets used as the title */
    Text.literal("Your First Dirt Block"),
    /** This is the text that gets used as the description */
    Text.literal("Now make a three by three"),
 
    /** This is the background image that is going to be used for the tab in the advancements page. */
    new Identifier("textures/gui/advancements/backgrounds/adventure.png"),
 
    /** The type of advancement it should be. */
    AdvancementFrame.TASK,
 
    /** Boolean if when you get the advancement, a toast should be created (the top right screen announcement) */
    true,
    /** Boolean if when you get the advancement, it should send a message in the chat */
    true,
 
    /** Boolean if the advancement should be seen in the advancements page. */
    false
)

Then we tell Minecraft when this advancement should be triggered (like after eating an item, or in our case, after a block enters our inventory) calling the criterion function.

.criterion("got_dirt", InventoryChangedCriterion.Conditions.items(Items.DIRT))

The first argument is a name of type String.

  • This name is only ever used by requirements (another property we can add to advancements) which make it so that before an advancement activates, the requirements (other advancements) need to be fulfilled first. In other words, it mostly doesn't matter what name you give the criterion.

The second argument is the criterion. In our example we use the InventoryChangedCriterion and we pass it the item we want it to trigger for Items.DIRT. But there are many criterions. The Minecraft Wiki has them listed as “List of triggers”. But the better reference to use is the Minecraft source itself. (If you haven't generated the source yet, read here.) You can take a look at the net.minecraft.advancement.criterion folder where they are all located and see what's already available.

PlayerHurtEntityCriterion.class ImpossibleCriterion.class Criterion.class AbstractCriterion.class
PlayerInteractedWithEntityCriterion.class InventoryChangedCriterion.class CriterionConditions.class
RecipeUnlockedCriterion.class ItemCriterion.class CriterionProgress.class
ShotCrossbowCriterion.class ItemDurabilityChangedCriterion.class CuredZombieVillagerCriterion.class
SlideDownBlockCriterion.class KilledByCrossbowCriterion.class EffectsChangedCriterion.class
StartedRidingCriterion.class LevitationCriterion.class EnchantedItemCriterion.class
SummonedEntityCriterion.class LightningStrikeCriterion.class EnterBlockCriterion.class
TameAnimalCriterion.class OnKilledCriterion.class EntityHurtPlayerCriterion.class
TargetHitCriterion.class PlacedBlockCriterion.class FilledBucketCriterion.class
ThrownItemPickedUpByEntityCriterion.class PlayerGeneratesContainerLootCriterion.class FishingRodHookedCriterion.class
TickCriterion.class TravelCriterion.class UsedEnderEyeCriterion.class UsedTotemCriterion.class

And then, the last call to our custom advancement was:

.build(consumer, "your_mod_id_please_change_me" + "/root");

We pass it the consumer, and set the id of the advancement.

  • Make sure you change the your_mod_id_please_change_me string to the name of your mod. (Also leave the “/root” part as is).

One More Example

Just to get the hang of it, lets add two more advancements to our example.

package com.example;
 
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricAdvancementProvider;
import net.minecraft.advancement.*;
import java.util.function.Consumer;
import net.minecraft.advancement.criterion.ConsumeItemCriterion;
import net.minecraft.advancement.criterion.InventoryChangedCriterion;
import net.minecraft.item.Items;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
 
public class ExampleModDataGenerator implements DataGeneratorEntrypoint {
 
    // ... (Rest of the code)
 
    static class AdvancementsProvider extends FabricAdvancementProvider {
 
        // ... (Rest of the code)
 
        @Override
        public void generateAdvancement(Consumer<AdvancementEntry> consumer) {
            AdvancementEntry rootAdvancement = Advancement.Builder.create()
                    .display(
                            Items.DIRT, // The display icon
                            Text.literal("Your First Dirt Block"), // The title
                            Text.literal("Now make a three by three"), // The description
                            new Identifier("textures/gui/advancements/backgrounds/adventure.png"), // Background image used
                            AdvancementFrame.TASK, // Options: TASK, CHALLENGE, GOAL
                            true, // Show toast top right
                            true, // Announce to chat
                            false // Hidden in the advancement tab
                    )
                    // The first string used in criterion is the name referenced by other advancements when they want to have 'requirements'
                    .criterion("got_dirt", InventoryChangedCriterion.Conditions.items(Items.DIRT))
                    .build(consumer, "your_mod_id_please_change_me" + "/root");
 
            AdvancementEntry gotOakAdvancement = Advancement.Builder.create().parent(rootAdvancement)
                    .display(
                            Items.OAK_LOG,
                            Text.literal("Your First Log"),
                            Text.literal("Bare fisted"),
                            null, // children to parent advancements don't need a background set
                            AdvancementFrame.TASK,
                            true,
                            true,
                            false
                    )
                    .rewards(AdvancementRewards.Builder.experience(1000))
                    .criterion("got_wood", InventoryChangedCriterion.Conditions.items(Items.OAK_LOG))
                    .build(consumer, "your_mod_id_please_change_me" + "/got_wood");
 
            AdvancementEntry eatAppleAdvancement = Advancement.Builder.create().parent(rootAdvancement)
                    .display(
                            Items.APPLE,
                            Text.literal("Apple and Beef"),
                            Text.literal("Ate an apple and beef"),
                            null, // children to parent advancements don't need a background set
                            AdvancementFrame.CHALLENGE,
                            true,
                            true,
                            false
                    )
                    .criterion("ate_apple", ConsumeItemCriterion.Conditions.item(Items.APPLE))
                    .criterion("ate_cooked_beef", ConsumeItemCriterion.Conditions.item(Items.COOKED_BEEF))
                    .build(consumer, "your_mod_id_please_change_me" + "/ate_apple_and_beef");
        }
    }
}

Don't forget to generate the data (Run the gradle task).

Windows
gradlew runDatagen
Linux
./gradlew runDatagen

We added an advancement that activates when you get an oak log, and which awards one-thousand experience when completed. And we added another advancement which the player must actually complete two criterions for, before being awarded the advancement. One criterion to eat an apple, and one criterion to eat cooked beef. This was done by just chain linking some method calls. We've also, importantly, put reasonable values for the fields, like calling the criterion that triggers when an apple is eaten 'ate_apple'. Also notice that we kept the “your_mod_id_please_change_me” part the same but changed the second part with the dash:

.build(consumer, "your_mod_id_please_change_me" + "/ate_apple_and_beef");
 
// ....
 
.build(consumer, "your_mod_id_please_change_me" + "/got_wood");

Another key part is that the two advancements we created are calling the parent function and passing it our root advancement.

AdvancementEntry gotOakAdvancement = Advancement.Builder.create().parent(rootAdvancement)
 
// ....
 
AdvancementEntry eatAppleAdvancement = Advancement.Builder.create().parent(rootAdvancement)
  • If an advancement doesn't have a parent, it creates a new page, and becomes its root.

We also, of course, changed the titles and descriptions, and even the frame for the ate_apple_and_beef advancement into a challenge type. (Those advancements which print out in purple and make crazy sound effects). One thing to keep in mind is the current root advancement for our mod is not very good. You want it to be something that is almost guaranteed to happen in your mod. For instance some mods make the root advancement triggered by detecting a custom book in the players inventory (a tutorial book basically), and then put the book in the players inventory when they spawn. The root advancement should be basically free, the children should be challenges.

When To Make a Custom Criterion?

There are many pre-made criterions to choose from that already probably do what you want, and as long as your custom mod items and blocks are registered, you can go pretty far without the use of any custom criterions. How do you know if you need a custom criterion?

The general rule is, if your mod introduces some new mechanic which Minecraft isn't keeping track of, and you want to have an advancement based on it, then make a criterion for it. For example, if your mod adds jumping-jacks into the game, and you want to have an advancement when a player does one hundred of them, how would minecraft know anything about that? It doesn't, which is why you'll need to make a custom criterion.

How To Make a Custom Criterion?

Our mod is keeping track of how many jumping jacks a player has done, and we want to make an advancement when they complete one hundred. First thing we got to do is make the JumpingJacks class which will extend the AbstractCriterion<JumpingJacks.Condition> class like so:

import com.google.gson.JsonObject;
import net.minecraft.advancement.criterion.AbstractCriterion;
import net.minecraft.advancement.criterion.AbstractCriterionConditions;
import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer;
import net.minecraft.predicate.entity.EntityPredicate;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class JumpingJacks extends AbstractCriterion<JumpingJacks.Condition> {
 
    /**
    /* Don't forget to change: "your_mod_id_please_change_me"
     **/
    public static final Identifier ID = new Identifier("your_mod_id_please_change_me", "jumping_jacks");
 
    @Override
    protected Condition conditionsFromJson(JsonObject obj, EntityPredicate.Extended playerPredicate, AdvancementEntityPredicateDeserializer predicateDeserializer) {
        return new Condition();
    }
 
    @Override
    public Identifier getId() {
        return ID;
    }
 
    public void trigger(ServerPlayerEntity player) {
        trigger(player, condition -> {
            return true;
        });
    }
 
    public static class Condition extends AbstractCriterionConditions {
 
        public Condition() {
            super(ID, EntityPredicate.Extended.EMPTY);
        }
    }
}

You'll notice inside the class, there is another class called Condition which implements AbstractCriterionConditions. It just calls the super function for now and nothing else. In fact this whole class is basically doing nothing, (other than making an ID). The only function that does anything is the trigger function which calls the trigger function which exists in the AbstractCriterion class we extended, and which we, with no checking against any data, return true always. That means that any time this JumpingJacks criterion is triggered, it'll award the player the advancement.

Let's create an advancement with it now.

import net.minecraft.advancement.Advancement;
import net.minecraft.advancement.AdvancementFrame;
import net.minecraft.item.Items;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
 
import java.util.function.Consumer;
 
public class Advancements implements Consumer<Consumer<Advancement>> {
 
    @Override
    public void accept(Consumer<Advancement> consumer) {
        Advancement rootAdvancement = Advancement.Builder.create()
                .display(
                        Items.BLUE_BED,
                        Text.literal("Jumping Jacks"),
                        Text.literal("You jumped Jack 100 times"),
                        new Identifier("textures/gui/advancements/backgrounds/adventure.png"),
                        AdvancementFrame.TASK,
                        true,
                        true,
                        false
                )
                .criterion("jumping_jacks", new JumpingJacks.Condition())
                .build(consumer, "your_mod_id_please_change_me" + "/root");
    }
}

Because we've changed the advancement generator code, don't forget to generate the data again.

./gradlew runDatagen

Before, with the vanilla provided criterions, we would've been done at this stage, but because we created the criterion, we need to actually call the trigger function ourselves, and register it. Behind the scenes, for the consume item criterion for instance, minecraft is doing this kind of trigger. You can see in the eat function that every time the player eats, it sends the item that was ate to the consume item criterion to check if an advancement should be awarded. We have to do the same for our mod. We are responsible for calling the trigger function. Here's an example:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.advancement.criterion.Criteria;
 
public class AdvancementTutorial implements ModInitializer {
 
    /**
    /* You ABSOLUTELY must register your custom
    /* criterion as done below for each one you create:
     */
    public static JumpingJacks JUMPING_JACKS = Criteria.register(new JumpingJacks());
 
    @Override
    public void onInitialize() {
        ServerPlayConnectionEvents.JOIN.register((handler, origin, destination) -> {
            if (checkedPlayerStateAndHesJumpedOneHundredTimes(handler.player)) {
                //
                // Because of the way we wrote our JumpingJacks class, 
                // calling the trigger function will ALWAYS grant us the advancement.
                //
                JUMPING_JACKS.trigger(handler.player);
            }
        });
    }
}
  • Because we aren't implementing a full blown mod we pretend a function checkedPlayerStateAndHesJumpedOneHundredTimes exists and returns true when the player has done more than one-hundred jumping jacks.
  • NOTE: You must call Criteria.register with your custom made criterion, or your game won't award the advancements. (And it MUST be done server side, which is why we do this in the ModInitializer class).

If you run your game now (changing that fake function to a simple true so it compiles), when you log into a world, you should be granted the jumping jack advancement, but because we are using the server join event here to do this, it gives it to you before you load in, which is why you don't get a toast message. If you open up the advancements page in the escape menu, you'll see it was in fact granted.

The last thing to do is to make a criterion that takes in some data when created and uses it during when the trigger function is called. (Like how the consume item criterion takes an Item, and then only triggers when that specific Item is consumed).

Criterion with State

We can imagine a mod that has different elemental wands you could use, like fire, water, ice and so on, and that each of these categories has multiple varieties (2 fire wands, 4 water wands, 3 ice wands). Let's say we want to make an advancement every time the player has used every wand in a specific category (all the ice wands, or all the fire wands). We could use what we know so far to accomplish this. We would simply make three criterions, fire, water, and ice, that we trigger when we've detected the user has used all the wands in that category. But we could save ourselves a lot of copy pasting by passing in a little bit of state when the criterion is made.

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.minecraft.advancement.criterion.AbstractCriterion;
import net.minecraft.advancement.criterion.AbstractCriterionConditions;
import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer;
import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer;
import net.minecraft.predicate.entity.EntityPredicate;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class WandCategoryUsed extends AbstractCriterion<WandCategoryUsed.Condition> {
 
    public static final Identifier ID = new Identifier("your_modid_here_please_change_me", "finished_wand_category");
 
    @Override
    protected Condition conditionsFromJson(JsonObject obj, EntityPredicate.Extended playerPredicate, AdvancementEntityPredicateDeserializer predicateDeserializer) {
        JsonElement cravingTarget = obj.get("wandElement");
        return new Condition(cravingTarget.getAsString());
    }
 
    @Override
    public Identifier getId() {
        return ID;
    }
 
    public void trigger(ServerPlayerEntity player, String wandElement) {
        trigger(player, condition -> condition.test(wandElement));
    }
 
    public static class Condition extends AbstractCriterionConditions {
        String wandElement;
 
        public Condition(String wandElement) {
            super(ID, EntityPredicate.Extended.EMPTY);
            this.wandElement = wandElement;
        }
 
        public boolean test(String wandElement) {
            return this.wandElement.equals(wandElement);
        }
 
        @Override
        public JsonObject toJson(AdvancementEntityPredicateSerializer predicateSerializer) {
            JsonObject jsonObject = super.toJson(predicateSerializer);
            jsonObject.add("wandElement", new JsonPrimitive(wandElement));
            return jsonObject;
        }
    }
}

Our Condition class now takes in a string in it's constructor and saves it for later use. The Condition class also has a new function test which takes in a string and returns true if it equals it's own wandElment string. In the toJson function we convert the wandElement to json so it can be saved to disk.

You'll also notice that the trigger function doesn't just return true now, it actually uses the new test function in the Condition class to see if the passed in data matches. And in the conditionsFromJson we convert the saved out wandElement json back to string.

Now we could write our advancement like so:

Advancement.Builder.create()
        .display(Items.FIRE_WAND_1, Text.literal("Used all water wands"),
                Text.literal("Used all water wands"),
                null,
                AdvancementFrame.TASK,
                true,
                true,
                false
        )
        .parent(parentAdvancement)
        .criterion("used_all_fire_wands", new WandCategoryUsed.Condition("fire"))
        .build(consumer, "your_mod_id_please_change_me" + "/used_all_fire_wands");
 
Advancement.Builder.create()
        .display(Items.ICE_WAND_1, Text.literal("Used all ice wands"),
                Text.literal("Used all ice wands"),
                null,
                AdvancementFrame.TASK,
                true,
                true,
                false
        )
        .parent(parentAdvancement)
        .criterion("used_all_ice_wands", new WandCategoryUsed.Condition("ice"))
        .build(consumer, "your_mod_id_please_change_me" + "/used_all_ice_wands");
 
Advancement.Builder.create()
        .display(Items.WATER_WAND_1, Text.literal("Used all water wands"),
                Text.literal("Used all water wands"),
                null,
                AdvancementFrame.TASK,
                true,
                true,
                false
        )
        .parent(parentAdvancement)
        .criterion("used_all_water_wands", new WandCategoryUsed.Condition("water"))
        .build(consumer, "your_mod_id_please_change_me" + "/used_all_water_wands");

Then we'd make sure to register the criterion (logical server side).

public static WandCategoryUsed WAND_USED = Criteria.register(new WandCategoryUsed());

And whenever we detected the player having used all the wands for a particular category, we could trigger the advancement like this:

if (playerUsedAllFireWands) {
    WAND_USED.trigger(player, "fire"); // The string here is whatever we initiated the condition with.
}
if (playerUsedAllWaterWands) {
    WAND_USED.trigger(player, "water"); // The string here is whatever we initiated the condition with.
}
if (playerUsedAllIceWands) {
    WAND_USED.trigger(player, "ice"); // The string here is whatever we initiated the condition with.
}
tutorial/datagen_advancements.1696010929.txt.gz · Last modified: 2023/09/29 18:08 by jmanc3