tutorial:datagen_advancements
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revisionLast revisionBoth sides next revision | ||
tutorial:datagen_advancements [2022/09/11 11:00] – created nexus-dino | tutorial:datagen_advancements [2023/09/29 18:08] – Updated pre-custom criterion section for fabric 1.20.2 jmanc3 | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | To get started with this, create | + | ====== 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 [[datagen_setup|Getting | ||
+ | |||
+ | ===== Hooking Up the Provider ===== | ||
+ | |||
+ | To begin making custom advancements, | ||
<code java> | <code java> | ||
- | private | + | import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; |
- | + | import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; | |
- | protected | + | 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:: | ||
+ | } | ||
+ | |||
+ | | ||
+ | protected | ||
+ | super(dataGenerator); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void generateAdvancement(Consumer< | ||
+ | // | ||
+ | // We will create our custom advancements here... | ||
+ | // | ||
+ | } | ||
+ | | ||
} | } | ||
+ | </ | ||
- | // ... | + | * It should be noted that the '' |
- | @Override | + | ===== Simple Advancement ===== |
- | public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { | + | |
- | // ... | + | 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 '' |
- | | + | |
- | // ... | + | <code java> |
+ | // ... (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) | ||
+ | |||
+ | | ||
+ | public void generateAdvancement(Consumer< | ||
+ | | ||
+ | .display( | ||
+ | Items.DIRT, | ||
+ | Text.literal(" | ||
+ | Text.literal(" | ||
+ | new Identifier(" | ||
+ | | ||
+ | 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 ' | ||
+ | | ||
+ | .build(consumer, | ||
+ | } | ||
+ | } | ||
} | } | ||
</ | </ | ||
- | ==== Advancements ==== | + | * Make sure you change the '' |
- | A quick summary, to create an advancement, | + | |
- | TODO: fill more of this... | + | 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' |
+ | If you have a configuration on '' | ||
+ | or you can open your projects root folder on the terminal and run: | ||
+ | |||
+ | <code bash Windows> | ||
+ | gradlew runDatagen | ||
+ | </ | ||
+ | |||
+ | <code bash Linux> | ||
+ | ./gradlew runDatagen | ||
+ | </ | ||
+ | |||
+ | In the '' | ||
+ | |||
+ | <code javascript> | ||
+ | { | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | { | ||
+ | " | ||
+ | " | ||
+ | ] | ||
+ | } | ||
+ | ] | ||
+ | }, | ||
+ | " | ||
+ | } | ||
+ | }, | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | }, | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | }, | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | }, | ||
+ | " | ||
+ | [ | ||
+ | " | ||
+ | ] | ||
+ | ] | ||
+ | } | ||
+ | </ | ||
+ | | ||
+ | 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 '' | ||
+ | |||
+ | * **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 '' | ||
+ | |||
+ | Let's go through the advancement we created step by step and see the options we have. We start by calling the '' | ||
+ | |||
+ | <code java> | ||
+ | AdvancementEntry rootAdvancement = Advancement.Builder.create() | ||
+ | </ | ||
+ | |||
+ | Then we call the chain method ' | ||
+ | |||
+ | <code java> | ||
+ | .display( | ||
+ | /** This is the item that gets used as the icon (You can use any of your mods icons as long as they' | ||
+ | Items.DIRT, | ||
+ | |||
+ | /** This is the text that gets used as the title */ | ||
+ | Text.literal(" | ||
+ | /** This is the text that gets used as the description */ | ||
+ | Text.literal(" | ||
+ | |||
+ | /** This is the background image that is going to be used for the tab in the advancements page. */ | ||
+ | new Identifier(" | ||
+ | |||
+ | /** The type of advancement it should be. */ | ||
+ | AdvancementFrame.TASK, | ||
+ | |||
+ | /** Boolean if when you get the advancement, | ||
+ | true, | ||
+ | /** Boolean if when you get the advancement, | ||
+ | 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 '' | ||
+ | |||
+ | <code java> | ||
+ | .criterion(" | ||
+ | </ | ||
+ | |||
+ | The first argument is a name of type '' | ||
+ | |||
+ | * This name is only ever used by '' | ||
+ | |||
+ | The second argument is the criterion. In our example we use the '' | ||
+ | |||
+ | | PlayerHurtEntityCriterion.class | ImpossibleCriterion.class | Criterion.class | AbstractCriterion.class | VillagerTradeCriterion.class | ||
+ | | PlayerInteractedWithEntityCriterion.class | InventoryChangedCriterion.class | CriterionConditions.class | AbstractCriterionConditions.class | ||
+ | | RecipeUnlockedCriterion.class | ItemCriterion.class | CriterionProgress.class | BeeNestDestroyedCriterion.class | ||
+ | | ShotCrossbowCriterion.class | ItemDurabilityChangedCriterion.class | CuredZombieVillagerCriterion.class | BredAnimalsCriterion.class | ||
+ | | SlideDownBlockCriterion.class | KilledByCrossbowCriterion.class | EffectsChangedCriterion.class | BrewedPotionCriterion.class | ||
+ | | StartedRidingCriterion.class | LevitationCriterion.class | EnchantedItemCriterion.class | ChangedDimensionCriterion.class | ||
+ | | SummonedEntityCriterion.class | LightningStrikeCriterion.class | EnterBlockCriterion.class | ChanneledLightningCriterion.class | ||
+ | | TameAnimalCriterion.class | OnKilledCriterion.class | EntityHurtPlayerCriterion.class | ConstructBeaconCriterion.class | ||
+ | | TargetHitCriterion.class | PlacedBlockCriterion.class | FilledBucketCriterion.class | ConsumeItemCriterion.class | ||
+ | | ThrownItemPickedUpByEntityCriterion.class | PlayerGeneratesContainerLootCriterion.class | FishingRodHookedCriterion.class | Criteria.class | ||
+ | | TickCriterion.class | TravelCriterion.class | UsedEnderEyeCriterion.class | UsedTotemCriterion.class | UsingItemCriterion.class | ||
+ | |||
+ | And then, the last call to our custom advancement was: | ||
+ | |||
+ | <code java> | ||
+ | .build(consumer, | ||
+ | </ | ||
+ | |||
+ | We pass it the '' | ||
+ | |||
+ | * Make sure you change the '' | ||
+ | |||
+ | ===== One More Example ===== | ||
+ | |||
+ | Just to get the hang of it, lets add two more advancements to our example. | ||
+ | |||
+ | |||
+ | <code java> | ||
+ | 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 rootAdvancement = Advancement.Builder.create() | ||
+ | .display( | ||
+ | Items.DIRT, // The display icon | ||
+ | Text.literal(" | ||
+ | Text.literal(" | ||
+ | new Identifier(" | ||
+ | AdvancementFrame.TASK, | ||
+ | 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 ' | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | |||
+ | AdvancementEntry gotOakAdvancement = Advancement.Builder.create().parent(rootAdvancement) | ||
+ | .display( | ||
+ | Items.OAK_LOG, | ||
+ | Text.literal(" | ||
+ | Text.literal(" | ||
+ | null, // children to parent advancements don't need a background set | ||
+ | AdvancementFrame.TASK, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .rewards(AdvancementRewards.Builder.experience(1000)) | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | |||
+ | AdvancementEntry eatAppleAdvancement = Advancement.Builder.create().parent(rootAdvancement) | ||
+ | .display( | ||
+ | Items.APPLE, | ||
+ | Text.literal(" | ||
+ | Text.literal(" | ||
+ | null, // children to parent advancements don't need a background set | ||
+ | AdvancementFrame.CHALLENGE, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .criterion(" | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Don't forget to generate the data (Run the gradle task). | ||
+ | |||
+ | <code bash Windows> | ||
+ | gradlew runDatagen | ||
+ | </ | ||
+ | |||
+ | <code bash 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, | ||
+ | |||
+ | <code java> | ||
+ | .build(consumer, | ||
+ | |||
+ | // .... | ||
+ | |||
+ | .build(consumer, | ||
+ | </ | ||
+ | |||
+ | Another **key** part is that the two advancements we created are calling the parent function and passing it our root advancement. | ||
+ | |||
+ | <code java> | ||
+ | AdvancementEntry gotOakAdvancement = Advancement.Builder.create().parent(rootAdvancement) | ||
+ | |||
+ | // .... | ||
+ | |||
+ | AdvancementEntry eatAppleAdvancement = Advancement.Builder.create().parent(rootAdvancement) | ||
+ | </ | ||
+ | |||
+ | * If an advancement doesn' | ||
+ | |||
+ | We also, of course, changed the titles and descriptions, | ||
+ | |||
+ | ===== 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' | ||
+ | |||
+ | ===== 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 '' | ||
+ | |||
+ | <code java> | ||
+ | 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< | ||
+ | |||
+ | /** | ||
+ | /* Don't forget to change: " | ||
+ | **/ | ||
+ | public static final Identifier ID = new Identifier(" | ||
+ | |||
+ | @Override | ||
+ | protected Condition conditionsFromJson(JsonObject obj, EntityPredicate.Extended playerPredicate, | ||
+ | return new Condition(); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Identifier getId() { | ||
+ | return ID; | ||
+ | } | ||
+ | |||
+ | public void trigger(ServerPlayerEntity player) { | ||
+ | trigger(player, | ||
+ | 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 '' | ||
+ | |||
+ | Let's create an advancement with it now. | ||
+ | |||
+ | <code java> | ||
+ | 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< | ||
+ | |||
+ | @Override | ||
+ | public void accept(Consumer< | ||
+ | Advancement rootAdvancement = Advancement.Builder.create() | ||
+ | .display( | ||
+ | Items.BLUE_BED, | ||
+ | Text.literal(" | ||
+ | Text.literal(" | ||
+ | new Identifier(" | ||
+ | AdvancementFrame.TASK, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Because we've changed the advancement generator code, don't forget to generate the data again. | ||
+ | |||
+ | <code bash> | ||
+ | ./gradlew runDatagen | ||
+ | </ | ||
+ | |||
+ | Before, with the vanilla provided criterions, we would' | ||
+ | |||
+ | <code java> | ||
+ | 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, | ||
+ | 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 '' | ||
+ | |||
+ | * **NOTE:** You must call '' | ||
+ | |||
+ | If you run your game now (changing that fake function to a simple '' | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | ===== 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. | ||
+ | |||
+ | <code java> | ||
+ | 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< | ||
+ | |||
+ | public static final Identifier ID = new Identifier(" | ||
+ | |||
+ | @Override | ||
+ | protected Condition conditionsFromJson(JsonObject obj, EntityPredicate.Extended playerPredicate, | ||
+ | JsonElement cravingTarget = obj.get(" | ||
+ | return new Condition(cravingTarget.getAsString()); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Identifier getId() { | ||
+ | return ID; | ||
+ | } | ||
+ | |||
+ | public void trigger(ServerPlayerEntity player, String wandElement) { | ||
+ | trigger(player, | ||
+ | } | ||
+ | |||
+ | 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(" | ||
+ | return jsonObject; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Our '' | ||
+ | |||
+ | You'll also notice that the '' | ||
+ | |||
+ | Now we could write our advancement like so: | ||
+ | |||
+ | <code java> | ||
+ | Advancement.Builder.create() | ||
+ | .display(Items.FIRE_WAND_1, | ||
+ | Text.literal(" | ||
+ | null, | ||
+ | AdvancementFrame.TASK, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .parent(parentAdvancement) | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | |||
+ | Advancement.Builder.create() | ||
+ | .display(Items.ICE_WAND_1, | ||
+ | Text.literal(" | ||
+ | null, | ||
+ | AdvancementFrame.TASK, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .parent(parentAdvancement) | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | |||
+ | Advancement.Builder.create() | ||
+ | .display(Items.WATER_WAND_1, | ||
+ | Text.literal(" | ||
+ | null, | ||
+ | AdvancementFrame.TASK, | ||
+ | true, | ||
+ | true, | ||
+ | false | ||
+ | ) | ||
+ | .parent(parentAdvancement) | ||
+ | .criterion(" | ||
+ | .build(consumer, | ||
+ | </ | ||
+ | |||
+ | Then we'd make sure to register the criterion (logical server side). | ||
+ | |||
+ | <code java> | ||
+ | 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: | ||
+ | |||
+ | <code java> | ||
+ | if (playerUsedAllFireWands) { | ||
+ | WAND_USED.trigger(player, | ||
+ | } | ||
+ | if (playerUsedAllWaterWands) { | ||
+ | WAND_USED.trigger(player, | ||
+ | } | ||
+ | if (playerUsedAllIceWands) { | ||
+ | WAND_USED.trigger(player, | ||
+ | } | ||
+ | </ |
tutorial/datagen_advancements.txt · Last modified: 2023/10/02 23:11 by jmanc3