User Tools

Site Tools


tutorial:datagen_advancements

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?

To start, let's create a new minecraft mechanic.

In your .java class which implements ModInitializer, write the following:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.item.Item;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
 
import java.util.HashMap;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    @Override
    public void onInitialize() {
        HashMap<Item, Integer> tools = new HashMap<>();
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (player instanceof ServerPlayerEntity) {
                Item item = player.getMainHandStack().getItem();
 
                Integer wrongToolUsedCount = tools.getOrDefault(item, 0);
                wrongToolUsedCount++;
                tools.put(item, wrongToolUsedCount);
 
                player.sendMessage(Text.literal("You've used '" + item + "' as a wrong tool: " + wrongToolUsedCount + " times."));
            }
        });
    }
}

In the code, when we detect the player has broken a block, we pull out the Integer out of the hashmap using the active Item in the players hand and increase the Integer by one. We then store that Integer back in the HashMap.

  • Note: the HashMap isn't being saved using Persistent Storage so when the world closes, whatever Integer's were being stored, are lost. The mechanic is also not keeping track of each player individually. In other words: This code is really bad, and written solely to show off custom criterions.

If you launch the game now, you should see a message pop up everytime you break a block telling you how many times that Item has been used as a 'wrong' tool. Understand what is happening thoroughly before continuing.

Next, let's create a custom criterion WrongToolCriterion which will be triggered and granted when we detect our custom game mechanic. In the same folder as your .java class which implements ModInitializer create a new file WrongToolCriterion.java and fill it as follows:

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.LootContextPredicate;
import net.minecraft.server.network.ServerPlayerEntity;
 
import java.util.Optional;
import java.util.function.Predicate;
 
public class WrongToolCriterion {
    //
    // Nothing yet
    //
}

Now, write a class inside our WrongToolCriterion class which extends AbstractCriterionConditions. We will call it Conditions. It will force you to implement the constructor.

public class WrongToolCriterion {
 
    public static class Conditions extends AbstractCriterionConditions {
        public Conditions() {
            // The base class Constructor wants an 'Optional<LootContextPredicate> playerPredicate'.
            // Since it's optional, we give them nothing.
            super(Optional.empty());
        }
 
        boolean requirementsMet() {
            return true;
        }
    }
}
  • Note: At the moment, our WrongToolCriterion has no requirements, so we just return true always from the function requirementsMet.

Make our class WrongToolCriterion extend AbstractCriterion which will take a type. In fact, it's going to take a class of type AbstractCriterionConditions (the class we just wrote). Should look like:

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
 
    public static class Conditions extends AbstractCriterionConditions {
        public Conditions() {
            super(Optional.empty());
        }
 
        boolean requirementsMet() {
            return true;
        }
    }
}

The code will now be complaining that you need to implement the conditionsFromJson function. So do so:

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
 
    @Override
    protected Conditions conditionsFromJson(JsonObject json,
                                            Optional<LootContextPredicate> playerPredicate,
                                            AdvancementEntityPredicateDeserializer predicateDeserializer) {
        Conditions conditions = new Conditions();
        return conditions;
    }
 
    public static class Conditions extends AbstractCriterionConditions {
        public Conditions() {
            super(Optional.empty());
        }
 
        boolean requirementsMet() {
            return true;
        }
    }
}

You may be asking yourself, what exactly is this conditionsFromJson function? When does it get called? Who calls it? What data does it get passed in? And what is it supposed to return? All very good questions. As has been mentioned before, advancements are simply .json files. This function conditionsFromJson gets passed in the conditions section from our advancements' .json files. In fact, the example advancements we wrote earlier in the article (root.json) had a conditions section.

{
  "criteria": {
    "got_dirt": {
      "conditions": {
        "items": [
          {
            "items": [
              "minecraft:dirt"
            ]
          }
        ]
      },
      "trigger": "minecraft:inventory_changed"
    }
  },
 
  // ... (Rest of json)
}

The JsonObject which you receive in the function is simply the above highlighted json. You are meant to pull out any data you need from it and pass it to the constructor of the Conditions object, and save it as fields. We then return that new Conditions object. Since our custom criterion is not going to have any 'requirements' we won't need to worry about loading and saving from json, yet.

The final modification we need to complete our WrongToolCriterion is to write a trigger function. This is the function we are going to call when we detect our new game mechanic activating.

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
 
    // ... (Rest of the code)
 
    protected void trigger(ServerPlayerEntity player) {
        trigger(player, conditions -> {
            return conditions.requirementsMet();
        });
    }
}

Inside our trigger function, we call another trigger function (the one in the base class AbstractCriterion). The function which we are calling takes in a ServerPlayerEntity and a Predicate<Conditions>.

What's a predicate?

In simple terms, the Predicate is going to hand you a variable of the type which is inside its <…> (in our case we get handed a variable of the type: Conditions (the class we wrote)) and it wants us to return true or false based on whatever criteria we want.

What we do is call the function requirementsMet (which returns a boolean) on the conditions variable, and return that. (We could've just return true instead of return conditions.requirementsMet() as well.) It will become clear how you can use this to have requirements on your criterions soon, but for now, any time our trigger function gets called, it grants the advacement.

The next step is to add our WrongToolCriterion to the Criteria register.

In your class which implements ModInitializer add the following:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.advancement.criterion.Criteria;
import net.minecraft.item.Item;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import java.util.HashMap;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static WrongToolCriterion WRONG_TOOl = Criteria.register(MOD_ID + "/wrong_tool", new WrongToolCriterion());
 
    @Override
    public void onInitialize() {
        HashMap<Item, Integer> tools = new HashMap<>();
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (player instanceof ServerPlayerEntity) {
                Item item = player.getMainHandStack().getItem();
 
                Integer wrongToolUsedCount = tools.getOrDefault(item, 0);
                wrongToolUsedCount++;
                tools.put(item, wrongToolUsedCount);
 
                WRONG_TOOl.trigger((ServerPlayerEntity) player);
 
                player.sendMessage(Text.literal("You've used '" + item + "' as a wrong tool: " + wrongToolUsedCount + " times."));
            }
        });
    }
}
  • Note: the first parameter (a string) can be anything. We prepend it with the MOD_ID so that our name doesn't clash with any of the default minecraft Criterion's or other mods.

Now, when we detect our custom game mechanic being activated we call the trigger function of the criterion, and if there are any advancements who use that criteria, they should be satisfied and grant you the advancement.

In our FabricAdvancementProvider class which we wrote earlier write the following advancement using our custom criteria:

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.item.Items;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
 
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) {
            AdvancementEntry rootAdvancement = Advancement.Builder.createUntelemetered()
                    .display(
                            Items.BELL,
                            Text.literal("Wrong Tool Buddy"),
                            Text.literal("That's not the right tool"),
                            new Identifier("textures/gui/advancements/backgrounds/adventure.png"),
                            AdvancementFrame.TASK,
                            true,
                            true,
                            false
                    )
                    .criterion("wrong_tool", ExampleMod.WRONG_TOOl.create(new WrongToolCriterion.Conditions()))
                    .build(consumer, "your_mod_id_please_change_me" + "/root");
        }
    }
}

Remember that after any modification we make to our DataGeneratorEntrypoint we need to run the gradle task runDatagen.

Windows
gradlew runDatagen
Linux
./gradlew runDatagen

Or if you have the configuration in your IDE, run that.

If you launch the game now, when you break a block, you should be granted our custom advacement satisfied from our custom game mechanic, using our custom criterion.

Conditions with State

With just what we have, we can already manually have any custom requirements for our advancements, that is, maybe we'd like are advancement to only be granted if the player has used the wrong tool atleast five times, or they had to be jumping while doing it, or any other things we might want to be true. And then, only when all the things we want to be true, are true, do we call the trigger function.

Look at the following:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.advancement.criterion.Criteria;
import net.minecraft.item.Item;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import java.util.HashMap;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static WrongToolCriterion WRONG_TOOl = Criteria.register(MOD_ID + "/wrong_tool", new WrongToolCriterion());
 
    @Override
    public void onInitialize() {
        HashMap<Item, Integer> tools = new HashMap<>();
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (player instanceof ServerPlayerEntity) {
                Item item = player.getMainHandStack().getItem();
 
                Integer wrongToolUsedCount = tools.getOrDefault(item, 0);
                wrongToolUsedCount++;
                tools.put(item, wrongToolUsedCount);
 
                if (wrongToolUsedCount > 5) {
                    WRONG_TOOl.trigger((ServerPlayerEntity) player);
                }
 
                player.sendMessage(Text.literal("You've used '" + item + "' as a wrong tool: " + wrongToolUsedCount + " times."));
            }
        });
    }
}

If you run the game now, and break some blocks, you'll notice the advancement will only be granted when you've used the wrong tool to break some block atleast five times.

This is really all most modders need.

So when do you need criterion's that hold state?

You should use them when you have some advancements which are very similar in function but slightly different. Like if we wanted an advancement when the player has used the wrong tool 1 time, and also 5 times, and also 10 times, then what we would do without criterions with state is have to copy and paste our criterion three times blowing up the size of the code, registering all three, and calling them seperately. This is a perfect example of where if the Conditions simply took an Integer specifying how many times an item had to be used as a wrong tool before activating, it would greately improve our code.

To do so, first, in our Conditions class, add a parameter to the constructor, an Integer, and assign it to a field, that way the Conditions has a copy of the number for later use.

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
    // ... (Rest of the code)
 
    public static class Conditions extends AbstractCriterionConditions {
 
        Integer minimumAmount;
 
        public Conditions(Integer minimumAmount) {
            super(Optional.empty());
 
            this.minimumAmount = minimumAmount;
        }
 
        boolean requirementsMet() {
            return true;
        }
    }
 
    // ... (Rest of the code)
}
  • Note: anywhere in the code where new WrongToolCriterion.Conditions() was called should be complaining but lets ignore it for now.

@Override the toJson function inside Conditions and write the following:

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
    // ... (Rest of the code)
 
    public static class Conditions extends AbstractCriterionConditions {
        // ... (Rest of the code)
 
        @Override
        public JsonObject toJson() {
            JsonObject json = super.toJson();
            json.addProperty("amount", minimumAmount);
            return json;
        }
    }
 
    // ... (Rest of the code)
}

When our gradle task runDatagen is ran, it calls this toJson function when it's writing the conditions portion of our custom advacements. That's why we make sure to add to the JsonObject our Conditions's private data: the field minimumAmount. That way when the game is ran, and it reads the advacement off the disk (in conditionsFromJson), it can read off the Integer we saved here.

Rewrite the conditionsFromJson as follows:

public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
 
    @Override
    protected Conditions conditionsFromJson(JsonObject json,
                                            Optional<LootContextPredicate> playerPredicate,
                                            AdvancementEntityPredicateDeserializer predicateDeserializer) {
        Integer minimiumAmount = json.get("amount").getAsInt();
        Conditions conditions = new Conditions(minimiumAmount);
        return conditions;
    }
 
    // ... (Rest of the code)
}

As we spoke about before, this funciton is called when the game client is ran and it passes us the JsonObject we wrote in toJson, therefore we read out the amount and cast it to an Integer which we know it is. We then pass that to the Conditions constructor so it can store it as a field.

Let's use that field now. Add a new parameter to our requirementsMet function, an Integer, which is supposed to be the amount that a particular item has used the wrong tool. In the function we will return true if that item has used more than the minimium the criteria requires. The final WrongToolCriterion should be as follows:

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.LootContextPredicate;
import net.minecraft.server.network.ServerPlayerEntity;
import java.util.Optional;
 
public class WrongToolCriterion extends AbstractCriterion<WrongToolCriterion.Conditions> {
 
    @Override
    protected Conditions conditionsFromJson(JsonObject json,
                                            Optional<LootContextPredicate> playerPredicate,
                                            AdvancementEntityPredicateDeserializer predicateDeserializer) {
        Integer minimiumAmount = json.get("amount").getAsInt();
        Conditions conditions = new Conditions(minimiumAmount);
        return conditions;
    }
 
    public static class Conditions extends AbstractCriterionConditions {
 
        Integer minimumAmount;
 
        public Conditions(Integer minimumAmount) {
            super(Optional.empty());
 
            this.minimumAmount = minimumAmount;
        }
 
        boolean requirementsMet(Integer amount) {
            return amount > minimumAmount;
        }
 
        @Override
        public JsonObject toJson() {
            JsonObject json = super.toJson();
            json.addProperty("amount", minimumAmount);
            return json;
        }
    }
 
    protected void trigger(ServerPlayerEntity player, Integer amount) {
        trigger(player, conditions -> {
            return conditions.requirementsMet(amount);
        });
    }
}

In our class which implements ModInitializer re-write our trigger function:

public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static WrongToolCriterion WRONG_TOOl = Criteria.register(MOD_ID + "/wrong_tool", new WrongToolCriterion());
 
    @Override
    public void onInitialize() {
        HashMap<Item, Integer> tools = new HashMap<>();
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (player instanceof ServerPlayerEntity) {
                Item item = player.getMainHandStack().getItem();
 
                Integer wrongToolUsedCount = tools.getOrDefault(item, 0);
                wrongToolUsedCount++;
                tools.put(item, wrongToolUsedCount);
 
                WRONG_TOOl.trigger((ServerPlayerEntity) player, wrongToolUsedCount);
 
                player.sendMessage(Text.literal("You've used '" + item + "' as a wrong tool: " + wrongToolUsedCount + " times."));
            }
        });
    }
}

And finally, pass 3 to the custom advancement and create a second one as well. (Dont forget to re-run the gradle task runDatagen)

AdvancementEntry rootAdvancement = Advancement.Builder.createUntelemetered()
        .display(
                Items.BELL,
                Text.literal("Wrong Tool Buddy"),
                Text.literal("That's not the right tool"),
                new Identifier("textures/gui/advancements/backgrounds/adventure.png"),
                AdvancementFrame.TASK,
                true,
                true,
                false
        )
        .criterion("wrong_tool", ExampleMod.WRONG_TOOl.create(new WrongToolCriterion.Conditions(3)))
        .build(consumer, "your_mod_id_please_change_me" + "/root");
 
AdvancementEntry second = Advancement.Builder.createUntelemetered().parent(rootAdvancement)
        .display(
                Items.QUARTZ,
                Text.literal("You did hear me didn't you?"),
                Text.literal("That's not the right tool"),
                new Identifier("textures/gui/advancements/backgrounds/adventure.png"),
                AdvancementFrame.TASK,
                true,
                true,
                false
        )
        .criterion("wrong_tool", ExampleMod.WRONG_TOOl.create(new WrongToolCriterion.Conditions(5)))
        .build(consumer, "your_mod_id_please_change_me" + "/root");

If you run the game now (don't forget to re-run the gradle task runDatagen), you should see that the advancements are granted when their conditions are met '3' and '5' respectively (actually '4' and '6' because we didn't use '>=').

You can also see the conditions section of the root.json file has the variable we wrote amount:

{
  "criteria": {
    "wrong_tool": {
      "conditions": {
        "amount": 5
      },
      "trigger": "minecraft:your_unique_mod_id_change_me_please/wrong_tool"
    }
  },
  // ... (Rest of JSON)
}
tutorial/datagen_advancements.txt · Last modified: 2023/10/02 23:11 by jmanc3