🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21
This page is written for version:
1.21
PREREQUISITES
Make sure you've completed the datagen setup process first.
First, we need to make our provider. Create a class that extends FabricAdvancementProvider
and fill out the base methods:
public class FabricDocsReferenceAdvancementProvider extends FabricAdvancementProvider {
protected FabricDocsReferenceAdvancementProvider(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registryLookup) {
super(output, registryLookup);
}
@Override
public void generateAdvancement(RegistryWrapper.WrapperLookup wrapperLookup, Consumer<AdvancementEntry> consumer) {
}
}
To finish setup, add this provider to your DataGeneratorEntrypoint
within the onInitializeDataGenerator
method.
pack.addProvider(FabricDocsReferenceAdvancementProvider::new);
An advancement is made up a few different components. Along with the requirements, called "criterion," it may have:
AdvancementDisplay
that tells 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.CriterionMerger
, which tells the advancement how to handle multiple criterion, andAdvancement
, which organizes the hierarchy you see on the "Advancements" screen.Here's a simple advancement for getting a dirt block:
AdvancementEntry getDirt = Advancement.Builder.create()
.display(
Items.DIRT, // The display icon
Text.literal("Your First Dirt Block"), // The title
Text.literal("Now make a house from it"), // The description
Identifier.ofVanilla("textures/gui/advancements/backgrounds/adventure.png"), // Background image for the tab in the advancements page, if this is a root advancement (has no parent)
AdvancementFrame.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."
.criterion("got_dirt", InventoryChangedCriterion.Conditions.items(Items.DIRT))
// Give the advancement an id
.build(consumer, FabricDocsReference.MOD_ID + "/get_dirt");
Not Found: /home/runner/work/fabric-docs/fabric-docs/reference/latest/src/main/generated/data/fabric-docs-reference/advancement/fabric-docs-reference/get_dirt.json
Just to get the hang of it, let's add one more advancement. We'll practice adding rewards, using multiple criterion, and assigning parents:
AdvancementEntry appleAndBeef = Advancement.Builder.create()
.parent(getDirt)
.display(
Items.APPLE,
Text.literal("Apple and Beef"),
Text.literal("Ate an apple and beef"),
null, // Children don't need a background, the root advancement takes care of that
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, FabricDocsReference.MOD_ID + "/apple_and_beef");
Don't forget to generate them! Use the terminal command below or the run configuration in IntelliJ.
gradlew runDatagen
./gradlew runDatagen
WARNING
While datagen can be on the client side, Criterion
s and Predicate
s are in the main source set (both sides), since the server needs to trigger and evaluate them.
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.advancement.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.
First, we'll need a new mechanic to implement. Let's tell the player what tool they used every time they break a block.
public class FabricDocsReferenceDatagenAdvancement implements ModInitializer {
@Override
public void onInitialize() {
HashMap<Item, Integer> tools = new HashMap<>();
PlayerBlockBreakEvents.AFTER.register(((world, player, blockPos, blockState, blockEntity) -> {
if (player instanceof ServerPlayerEntity serverPlayer) { // Only triggers on the server side
Item item = player.getMainHandStack().getItem();
Integer usedCount = tools.getOrDefault(item, 0);
usedCount++;
tools.put(item, usedCount);
serverPlayer.sendMessage(Text.of("You've used \"" + item + "\" as a tool " + usedCount + " times!"));
}
}));
}
}
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 Criterion
s. 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:
public class UseToolCriterion extends AbstractCriterion<UseToolCriterion.Conditions> {
@Override
public Codec<Conditions> getConditionsCodec() {
return Conditions.CODEC;
}
public record Conditions(Optional<LootContextPredicate> playerPredicate) implements AbstractCriterion.Conditions {
public static Codec<UseToolCriterion.Conditions> CODEC = LootContextPredicate.CODEC.optionalFieldOf("player")
.xmap(Conditions::new, Conditions::player).codec();
@Override
public Optional<LootContextPredicate> player() {
return playerPredicate;
}
}
}
Whew, that's a lot! Let's break it down.
UseToolCriterion
is an AbstractCriterion
, which Conditions
can apply to.Conditions
has a playerPredicate
field. All Conditions
should have a player predicate (technically a LootContextPredicate
).Conditions
also has a CODEC
. This Codec
is 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
:
public boolean requirementsMet() {
return true; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
Now that we've got a criterion and its conditions, we need a way to trigger it. Add a trigger method to UseToolCriterion
:
public void trigger(ServerPlayerEntity player) {
trigger(player, Conditions::requirementsMet);
}
Almost there! Next, we need an instance of our criterion to work with. Let's put it in a new class, called ModCriteria
.
public class ModCriteria {
public static final UseToolCriterion USE_TOOL = Criteria.register(FabricDocsReference.MOD_ID + "/use_tool", new UseToolCriterion());
}
To make sure that our criteria are initialized at the right time, add a blank init
method:
// :::datagen-advancements:mod-criteria
public static final UseToolCriterion USE_TOOL = Criteria.register(FabricDocsReference.MOD_ID + "/use_tool", new UseToolCriterion());
// :::datagen-advancements:mod-criteria
// :::datagen-advancements:new-mod-criteria
public static final ParameterizedUseToolCriterion PARAMETERIZED_USE_TOOL = Criteria.register(FabricDocsReference.MOD_ID + "/parameterized_use_tool", new ParameterizedUseToolCriterion());
// :::datagen-advancements:mod-criteria
And call it in your mod initializer:
ModCriteria.init();
Finally, we need to trigger our criteria. Add this to where we sent a message to the player in the main mod class.
ModCriteria.USE_TOOL.trigger(serverPlayer);
Your shiny new criterion is now ready to use! Let's add it to our provider:
AdvancementEntry breakBlockWithTool = Advancement.Builder.create()
.parent(getDirt)
.display(
Items.DIAMOND_SHOVEL,
Text.literal("Not a Shovel"),
Text.literal("That's not a shovel (probably)"),
null,
AdvancementFrame.GOAL,
true,
true,
false
)
.criterion("break_block_with_tool", ModCriteria.USE_TOOL.create(new UseToolCriterion.Conditions(Optional.empty())))
.build(consumer, FabricDocsReference.MOD_ID + "/break_block_with_tool");
Run the datagen task again, and you've got your new advancement to play with!
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 Condtions#requirementsMet
method:
public boolean requirementsMet(int totalTimes) {
return totalTimes > requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
requiredTimes
doesn't exist, so make it a parameter of Conditions
:
public record Conditions(Optional<LootContextPredicate> playerPredicate, int requiredTimes) implements AbstractCriterion.Conditions {
@Override
public Optional<LootContextPredicate> player() {
return playerPredicate;
}
// :::datagen-advancements:new-requirements-met
public boolean requirementsMet(int totalTimes) {
return totalTimes > requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
// :::datagen-advancements:new-requirements-met
}
}
Now our codec is erroring. Let's write a new codec for the new changes:
public static Codec<ParameterizedUseToolCriterion.Conditions> CODEC = RecordCodecBuilder.create(instance -> instance.group(
LootContextPredicate.CODEC.optionalFieldOf("player").forGetter(Conditions::player),
Codec.INT.fieldOf("requiredTimes").forGetter(Conditions::requiredTimes)
).apply(instance, Conditions::new));
// :::datagen-advancements:new-parameter
@Override
public Optional<LootContextPredicate> player() {
return playerPredicate;
}
// :::datagen-advancements:new-requirements-met
public boolean requirementsMet(int totalTimes) {
return totalTimes > requiredTimes; // AbstractCriterion#trigger helpfully checks the playerPredicate for us.
}
// :::datagen-advancements:new-requirements-met
}
}
Moving on, we now need to fix our trigger
method:
public void trigger(ServerPlayerEntity player, int totalTimes) {
trigger(player, conditions -> conditions.requirementsMet(totalTimes));
}
If you've made a new criterion, we need to add it to ModCriteria
public static final ParameterizedUseToolCriterion PARAMETERIZED_USE_TOOL = Criteria.register(FabricDocsReference.MOD_ID + "/parameterized_use_tool", new ParameterizedUseToolCriterion());
// :::datagen-advancements:mod-criteria
// :::datagen-advancements:mod-criteria-init
public static void init() {
}
And call it in our main class, right where the old one is:
ModCriteria.PARAMETERIZED_USE_TOOL.trigger(serverPlayer, usedCount);
Add the advancement to your provider:
AdvancementEntry breakBlockWithToolFiveTimes = Advancement.Builder.create()
.parent(breakBlockWithTool)
.display(
Items.GOLDEN_SHOVEL,
Text.literal("Not a Shovel Still"),
Text.literal("That's still not a shovel (probably)"),
null,
AdvancementFrame.GOAL,
true,
true,
false
)
.criterion("break_block_with_tool_five_times", ModCriteria.PARAMETERIZED_USE_TOOL.create(new ParameterizedUseToolCriterion.Conditions(Optional.empty(), 5)))
.build(consumer, FabricDocsReference.MOD_ID + "/break_block_with_tool_five_times");
Run datagen again, and you're finally done!