Advancement Generation
Advancements can be generated for a mod by constructing a new AdvancementProvider and providing AdvancementSubProviders. Advancements can either be created and supplied manually or, for convenience, created using Advancement$Builder. The provider must be added to the DataGenerator.
!!! note
Forge provides an extension for the AdvancementProvider called ForgeAdvancementProvider which integrates better for generating advancements. So, this documentation will use ForgeAdvancementProvider along with the sub provider interface ForgeAdvancementProvider$AdvancementGenerator.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
output -> new ForgeAdvancementProvider(
output,
event.getLookupProvider(),
event.getExistingFileHelper(),
// Sub providers which generate the advancements
List.of(subProvider1, subProvider2, /*...*/)
)
);
}
ForgeAdvancementProvider$AdvancementGenerator
A ForgeAdvancementProvider$AdvancementGenerator is responsible for generating advancements, containing a method which takes in a registry lookup, the writer (Consumer<Advancement>), and the existing file helper..
// In some subclass of ForgeAdvancementProvider$AdvancementGenerator or as a lambda reference
@Override
public void generate(HolderLookup.Provider registries, Consumer<Advancement> writer, ExistingFileHelper existingFileHelper) {
// Build advancements here
}
Advancement$Builder
Advancement$Builder is a convenience implementation for creating Advancements to generate. It allows the definition of the parent advancement, the display information, the rewards when the advancement has been completed, and the requirements to unlock the advancement. Only the requirements need to be specified to create an Advancement.
Although not required, there are a number of methods that are important to know of:
| Method | Description |
|---|---|
parent |
Sets the advancement which this advancement is directly linked to. Can either specify the name of the advancement or the advancement itself if its generated by the modder. |
display |
Sets the information to display to the chat, toast, and advancement screen. |
rewards |
Sets the rewards obtained when this advancement is completed. |
addCriterion |
Adds a condition to the advancement. |
requirements |
Specifies if the conditions must all return true or at least one must return true. An additional overload can be used to mix-and-match those operations. |
Once an Advancement$Builder is ready to be built, the #save method should be called which takes in the writer, the registry name of the advancement, and the file helper used to check whether the supplied parent exists.
// In some ForgeAdvancementProvider$AdvancementGenerator#generate(registries, writer, existingFileHelper)
Advancement example = Advancement.Builder.advancement()
.addCriterion("example_criterion", triggerInstance) // How the advancement is unlocked
.save(writer, name, existingFileHelper); // Add data to builder
Datapack Registry Object Generation
Datapack registry objects can be generated for a mod by constructing a new DatapackBuiltinEntriesProvider and providing a RegistrySetBuilder with the new objects to register. The provider must be added to the DataGenerator.
!!! note
DatapackBuiltinEntriesProvider is a Forge extension on top of RegistriesDatapackGenerator which properly handles referencing existing datapack registry objects without exploding the entry. So, this documentation will use DatapackBuiltinEntriesProvider.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
output -> new DatapackBuiltinEntriesProvider(
output,
event.getLookupProvider(),
// The builder containing the datapack registry objects to generate
new RegistrySetBuilder().add(/* ... */),
// Set of mod ids to generate the datapack registry objects of
Set.of(MOD_ID)
)
);
}
RegistrySetBuilder
A RegistrySetBuilder is responsible for building all datapack registry objects to be used within the game. The builder can add a new entry for a registry, which can then register objects to that registry.
First, a new instance of a RegistrySetBuilder can be initialized by calling the constructor. Then, the #add method (which takes in the ResourceKey of the registry, a RegistryBootstrap consumer containing the BootstapContext to register the objects, and an optional Lifecycle argument to indicate the registry's current lifecycle status) can be called to handle a specific registry for registration.
new RegistrySetBuilder()
// Create configured features
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// Register configured features here
})
// Create placed features
.add(Registries.PLACED_FEATURE, bootstrap -> {
// Register placed features here
});
!!! note
Datapack registries created through Forge can also generate their objects using this builder by also passing in the associated ResourceKey.
Registering with BootstapContext
The #register method in the BootstapContext provided by the builder can be used to register objects. It takes in the ResourceKey representing the registry name of the object, the object to register, and an optional Lifecycle argument to indicate the registry object's current lifecycle status.
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
// In some constant location or argument
new RegistrySetBuilder()
// Create configured features
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// Register configured features here
bootstrap.register(
// The resource key for the configured feature
EXAMPLE_CONFIGURED_FEATURE,
new ConfiguredFeature<>(
Feature.ORE, // Create an ore feature
new OreConfiguration(
List.of(), // Does nothing
8 // in veins of at most 8
)
)
);
})
// Create placed features
.add(Registries.PLACED_FEATURE, bootstrap -> {
// Register placed features here
});
Datapack Registry Object Lookup
Sometimes datapack registry objects may want to use other datapack registry objects or tags containing datapack registry objects. In those cases, you can look up another datapack registry using BootstapContext#lookup to get a HolderGetter. From there, you can get a Holder$Reference to the datapack registry object or a HolderSet$Named for the tag via #getOrThrow by passing in the associated key.
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
public static final ResourceKey<PlacedFeature> EXAMPLE_PLACED_FEATURE = ResourceKey.create(
Registries.PLACED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_placed_feature")
);
// In some constant location or argument
new RegistrySetBuilder()
// Create configured features
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// Register configured features here
bootstrap.register(
// The resource key for the configured feature
EXAMPLE_CONFIGURED_FEATURE,
new ConfiguredFeature(/* ... */)
);
})
// Create placed features
.add(Registries.PLACED_FEATURE, bootstrap -> {
// Register placed features here
// Get configured feature registry
HolderGetter<ConfiguredFeature<?, ?>> configured = bootstrap.lookup(Registries.CONFIGURED_FEATURE);
bootstrap.register(
// The resource key for the placed feature
EXAMPLE_PLACED_FEATURE,
new PlacedFeature(
configured.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // Get the configured feature
List.of() // and do nothing to the placement location
)
)
});
Global Loot Modifier Generation
Global Loot Modifiers (GLMs) can be generated for a mod by subclassing GlobalLootModifierProvider and implementing #start. Each GLM can be added generated by calling #add and specifying the name of the modifier and the modifier instance to be serialized. After implementation, the provider must be added to the DataGenerator.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
output -> new MyGlobalLootModifierProvider(output, MOD_ID)
);
}
// In some GlobalLootModifierProvider#start
this.add("example_modifier", new ExampleModifier(
new LootItemCondition[] {
WeatherCheck.weather().setRaining(true).build() // Executes when raining
},
"val1",
10,
Items.DIRT
));
Loot Table Generation
Loot tables can be generated for a mod by constructing a new LootTableProvider and providing LootTableProvider$SubProviderEntrys. The provider must be added to the DataGenerator.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
output -> new MyLootTableProvider(
output,
// Specify registry names of tables that are required to generate, or can leave empty
Collections.emptySet(),
// Sub providers which generate the loot
List.of(subProvider1, subProvider2, /*...*/)
)
);
}
LootTableSubProvider
Each LootTableProvider$SubProviderEntry takes in a supplied LootTableSubProvider, which generates the loot table, for a given LootContextParamSet. The LootTableSubProvider contains a method which takes in the writer (BiConsumer<ResourceLocation, LootTable.Builder>) to generate a table.
public class ExampleSubProvider implements LootTableSubProvider {
// Used to create a factory method for the wrapping Supplier
public ExampleSubProvider() {}
// The method used to generate the loot tables
@Override
public void generate(BiConsumer<ResourceLocation, LootTable.Builder> writer) {
// Generate loot tables here by calling writer#accept
}
}
The table can then be added to LootTableProvider#getTables for any available LootContextParamSet:
// In the list passed into the LootTableProvider constructor
new LootTableProvider.SubProviderEntry(
ExampleSubProvider::new,
// Loot table generator for the 'empty' param set
LootContextParamSets.EMPTY
)
BlockLootSubProvider and EntityLootSubProvider Subclasses
For LootContextParamSets#BLOCK and #ENTITY, there are special types (BlockLootSubProvider and EntityLootSubProvider respectively) which provide additional helper methods for creating and validating that there are loot tables.
The BlockLootSubProvider's constructor takes in a list of items, which are explosion resistant to determine whether the loot table can be generated if a block is exploded, and a FeatureFlagSet, which determines whether the block is enabled so that a loot table is generated for it.
// In some BlockLootSubProvider subclass
public MyBlockLootSubProvider() {
super(Collections.emptySet(), FeatureFlags.REGISTRY.allFlags());
}
The EntityLootSubProvider's constructor takes in a FeatureFlagSet, which determines whether the entity type is enabled so that a loot table is generated for it.
// In some EntityLootSubProvider subclass
public MyEntityLootSubProvider() {
super(FeatureFlags.REGISTRY.allFlags());
}
To use them, all registered objects must be supplied to either BlockLootSubProvider#getKnownBlocks and EntityLootSubProvider#getKnownEntityTypes respectively. These methods are to make sure all objects within the iterable has a loot table.
!!! tip
If DeferredRegister is being used to register a mod's objects, then the #getKnown* methods can be supplied the entries via DeferredRegister#getEntries:
```java
// In some BlockLootSubProvider subclass for some DeferredRegister BLOCK_REGISTRAR
@Override
protected Iterable<Block> getKnownBlocks() {
return BLOCK_REGISTRAR.getEntries() // Get all registered entries
.stream() // Stream the wrapped objects
.flatMap(RegistryObject::stream) // Get the object if available
::iterator; // Create the iterable
}
```
The loot tables themselves can be added by implementing the #generate method.
// In some BlockLootSubProvider subclass
@Override
public void generate() {
// Add loot tables here
}
Loot Table Builders
To generate loot tables, they are accepted by the LootTableSubProvider as a LootTable$Builder. Afterwards, the specified LootContextParamSet is set in the LootTableProvider$SubProviderEntry and then built via #build. Before being built, the builder can specify entries, conditions, and modifiers which affect how the loot table functions.
!!! note The functionality of loot tables is so expansive that it will not be covered by this documentation in its entirety. Instead, a brief description of each component will be mentioned. The specific subtypes of each component can be found using an IDE. Their implementations will be left as an exercise to the reader.
LootTable
Loot tables are the base object and can be transformed into the required LootTable$Builder using LootTable#lootTable. The loot table can be built with a list of pools (via #withPool) applied in the order they are specified along with functions (via #apply) to modify the resulting items of those pools.
LootPool
Loot pools represents a group to perform operations and can generate a LootPool$Builder using LootPool#lootPool. Each loot pool can specify the entries (via #add) which define the operations in the pool, the conditions (via #when) which define if the operations in the pool should be performed, and functions (via #apply) to modify the resulting items of the entries. Each pool can be executed as many times as specified (via #setRolls). Additionally, bonus executions can be specified (via #setBonusRolls) which is modified by the luck of the executor.
LootPoolEntryContainer
Loot entries define the operations to occur when selected, typically generating items. Each entry has an associated, registered LootPoolEntryType. They also have their own associated builders which subtype LootPoolEntryContainer$Builder. Multiple entries can execute at the same time (via #append) or sequentially until one fails (via #then). Additionally, entries can default to another entry on failure (via #otherwise).
LootItemCondition
Loot conditions define requirements which need to be met for some operation to execute. Each condition has an associated, registered LootItemConditionType. They also have their own associated builders which subtype LootItemCondition$Builder. By default, all loot conditions specified must return true for an operation to execute. Loot conditions can also be specified such that only one must return true instead (via #or). Additionally, the resulting output of a condition can be inverted (via #invert).
LootItemFunction
Loot functions modify the result of an execution before passing it to the output. Each function has an associated, registered LootItemFunctionType. They also have their own associated builders which subtype LootItemFunction$Builder.
NbtProvider
NBT providers are a special type of functions defined by CopyNbtFunction. They define where to pull tag information from. Each provider has an associated, registered LootNbtProviderType.
NumberProvider
Number providers determine how many times a loot pool executes. Each provider has an associated, registered LootNumberProviderType.
ScoreboardNameProvider
Scoreboard providers are a special type of number providers defined by ScoreboardValue. They define the name of the scoreboard to pull the number of rolls to execute from. Each provider has an associated, registered LootScoreProviderType.
Recipe Generation
Recipes can be generated for a mod by subclassing RecipeProvider and implementing #buildRecipes. A recipe is supplied for data generation once a FinishedRecipe view is accepted by the consumer. FinishedRecipes can either be created and supplied manually or, for convenience, created using a RecipeBuilder.
After implementation, the provider must be added to the DataGenerator.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
MyRecipeProvider::new
);
}
RecipeBuilder
RecipeBuilder is a convenience implementation for creating FinishedRecipes to generate. It provides basic definitions for unlocking, grouping, saving, and getting the result of a recipe. This is done through #unlockedBy, #group, #save, and #getResult respectively.
!!! important
ItemStack outputs in recipes are not supported within vanilla recipe builders. A FinishedRecipe must be built in a different manner for existing vanilla recipe serializers to generate this data.
!!! warning
The item results being generated must have a valid RecipeCategory specified; otherwise, a NullPointerException will be thrown.
All recipe builders except for [SpecialRecipeBuilder] require an advancement criteria to be specified. All recipes generate a criteria unlocking the recipe if the player has used the recipe previously. However, an additional criteria must be specified that allows the player to obtain the recipe without any prior knowledge. If any of the criteria specified is true, then the played will obtain the recipe for the recipe book.
!!! tip
Recipe criteria commonly use InventoryChangeTrigger to unlock their recipe when certain items are present in the user's inventory.
ShapedRecipeBuilder
ShapedRecipeBuilder is used to generate shaped recipes. The builder can be initialized via #shaped. The recipe group, input symbol pattern, symbol definition of ingredients, and the recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
ShapedRecipeBuilder builder = ShapedRecipeBuilder.shaped(RecipeCategory.MISC, result)
.pattern("a a") // Create recipe pattern
.define('a', item) // Define what the symbol represents
.unlockedBy("criteria", criteria) // How the recipe is unlocked
.save(writer); // Add data to builder
Additional Validation Checks
Shaped recipes have some additional validation checks performed before building:
- A pattern must be defined and take in more than one item.
- All pattern rows must be the same width.
- A symbol cannot be defined more than once.
- The space character (
' ') is reserved for representing no item in a slot and, as such, cannot be defined. - A pattern must use all symbols defined by the user.
ShapelessRecipeBuilder
ShapelessRecipeBuilder is used to generate shapeless recipes. The builder can be initialized via #shapeless. The recipe group, input ingredients, and the recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
ShapelessRecipeBuilder builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, result)
.requires(item) // Add item to the recipe
.unlockedBy("criteria", criteria) // How the recipe is unlocked
.save(writer); // Add data to builder
SimpleCookingRecipeBuilder
SimpleCookingRecipeBuilder is used to generate smelting, blasting, smoking, and campfire cooking recipes. Additionally, custom cooking recipes using the SimpleCookingSerializer can also be data generated using this builder. The builder can be initialized via #smelting, #blasting, #smoking, #campfireCooking, or #cooking respectively. The recipe group and the recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
SimpleCookingRecipeBuilder builder = SimpleCookingRecipeBuilder.smelting(input, RecipeCategory.MISC, result, experience, cookingTime)
.unlockedBy("criteria", criteria) // How the recipe is unlocked
.save(writer); // Add data to builder
SingleItemRecipeBuilder
SingleItemRecipeBuilder is used to generate stonecutting recipes. Additionally, custom single item recipes using a serializer like SingleItemRecipe$Serializer can also be data generated using this builder. The builder can be initialized via #stonecutting or through the constructor respectively. The recipe group and the recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
SingleItemRecipeBuilder builder = SingleItemRecipeBuilder.stonecutting(input, RecipeCategory.MISC, result)
.unlockedBy("criteria", criteria) // How the recipe is unlocked
.save(writer); // Add data to builder
Non-RecipeBuilder Builders
Some recipe builders do not implement RecipeBuilder due to lacking features used by all previously mentioned recipes.
SmithingTransformRecipeBuilder
SmithingTransformRecipeBuilder is used to generate smithing recipes which transform an item. Additionally, custom recipes using a serializer like SmithingTransformRecipe$Serializer can also be data generated using this builder. The builder can be initialized via #smithing or through the constructor respectively. The recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
SmithingTransformRecipeBuilder builder = SmithingTransformRecipeBuilder.smithing(template, base, addition, RecipeCategory.MISC, result)
.unlocks("criteria", criteria) // How the recipe is unlocked
.save(writer, name); // Add data to builder
SmithingTrimRecipeBuilder
SmithingTrimRecipeBuilder is used to generate smithing recipes for armor trims. Additionally, custom upgrade recipes using a serializer like SmithingTrimRecipe$Serializer can also be data generated using this builder. The builder can be initialized via #smithingTrim or through the constructor respectively. The recipe unlock criteria can be specified before saving.
// In RecipeProvider#buildRecipes(writer)
SmithingTrimRecipe builder = SmithingTrimRecipe.smithingTrim(template, base, addition, RecipeCategory.MISC)
.unlocks("criteria", criteria) // How the recipe is unlocked
.save(writer, name); // Add data to builder
SpecialRecipeBuilder
SpecialRecipeBuilder is used to generate empty JSONs for dynamic recipes that cannot easily be constrained to the recipe JSON format (dying armor, firework, etc.). The builder can be initialized via #special.
// In RecipeProvider#buildRecipes(writer)
SpecialRecipeBuilder.special(dynamicRecipeSerializer)
.save(writer, name); // Add data to builder
Conditional Recipes
Conditional recipes can also be data generated via ConditionalRecipe$Builder. The builder can be obtained using #builder.
Conditions for each recipe can be specified by first calling #addCondition and then calling #addRecipe after all conditions have been specified. This process can be repeated as many times as the programmer would like.
After all recipes have been specified, advancements can be added for each recipe at the end using #generateAdvancement. Alternatively, the conditional advancement can be set using #setAdvancement.
// In RecipeProvider#buildRecipes(writer)
ConditionalRecipe.builder()
// Add the conditions for the recipe
.addCondition(...)
// Add recipe to return when conditions are true
.addRecipe(...)
// Add the next conditions for the next recipe
.addCondition(...)
// Add next recipe to return when the next conditions are true
.addRecipe(...)
// Create conditional advancement which uses the conditions
// and unlocking advancement in the recipes above
.generateAdvancement()
.build(writer, name);
IConditionBuilder
To simplify adding conditions to conditional recipes without having to construct the instances of each condition instance manually, the extended RecipeProvider can implement IConditionBuilder. The interface adds methods to easily construct condition instances.
// In ConditionalRecipe$Builder#addCondition
(
// If either 'examplemod:example_item'
// OR 'examplemod:example_item2' exists
// AND
// NOT FALSE
// Methods are defined by IConditionBuilder
and(
or(
itemExists("examplemod", "example_item"),
itemExists("examplemod", "example_item2")
),
not(
FALSE()
)
)
)
Custom Recipe Serializers
Custom recipe serializers can be data generated by creating a builder that can construct a FinishedRecipe. The finished recipe encodes the recipe data and its unlocking advancement, when present, to JSON. Additionally, the name and serializer of the recipe is also specified to know where to write to and what can decode the object when loading. Once a FinishedRecipe is constructed, it simply needs to be passed to the Consumer supplied by RecipeProvider#buildRecipes.
!!! tip
FinishedRecipes are flexible enough that any object transformation can be data generated, not just items.
Tag Generation
Tags can be generated for a mod by subclassing TagsProvider and implementing #addTags. After implementation, the provider must be added to the DataGenerator.
// On the MOD event bus
@SubscribeEvent
public void gatherData(GatherDataEvent event) {
event.getGenerator().addProvider(
// Tell generator to run only when server data are generating
event.includeServer(),
// Extends net.minecraftforge.common.data.BlockTagsProvider
output -> new MyBlockTagsProvider(
output,
event.getLookupProvider(),
MOD_ID,
event.getExistingFileHelper()
)
);
}
TagsProvider
The tags provider has two methods used for generating tags: creating a tag with objects and other tags via #tag, or using tags from other object types to generate the tag data via #getOrCreateRawBuilder.
!!! note
Typically, a provider will not call #getOrCreateRawBuilder directly unless a registry contains a representation of objects from a different registry (blocks have item representations to obtain the blocks in the inventory).
When #tag is called, a TagAppender is created which acts as a chainable consumer of elements to add to the tag:
| Method | Description |
|---|---|
add |
Adds an object to a tag through its resource key. |
addOptional |
Adds an object to a tag through its name. If the object is not present, then the object will be skipped when loading. |
addTag |
Adds a tag to a tag through its tag key. All elements within the inner tag are now a part of the outer tag. |
addOptionalTag |
Adds a tag to a tag through its name. If the tag is not present, then the tag will be skipped when loading. |
replace |
When true, all previously loaded entries added to this tag from other datapacks will be discarded. If a datapack is loaded after this one, then it will still append the entries to the tag. |
remove |
Removes an object or tag from a tag through its name or key. |
// In some TagProvider#addTags
this.tag(EXAMPLE_TAG)
.add(EXAMPLE_OBJECT) // Adds an object to the tag
.addOptional(ResourceLocation.fromNamespaceAndPath("othermod", "other_object")) // Adds an object from another mod to the tag
this.tag(EXAMPLE_TAG_2)
.addTag(EXAMPLE_TAG) // Adds a tag to the tag
.remove(EXAMPLE_OBJECT) // Removes an object from this tag
!!! important If the mod's tags softly depends on another mod's tags (the other mod may or may not be present at runtime), the other mods' tags should be referenced using the optional methods.
Existing Providers
Minecraft contains a few tag providers for certain registries that can be subclassed instead. Additionally, some providers contain additional helper methods to more easily create tags.
| Registry Object Type | Tag Provider |
|---|---|
Block |
BlockTagsProvider* |
Item |
ItemTagsProvider |
EntityType |
EntityTypeTagsProvider |
Fluid |
FluidTagsProvider |
GameEvent |
GameEventTagsProvider |
Biome |
BiomeTagsProvider |
FlatLevelGeneratorPreset |
FlatLevelGeneratorPresetTagsProvider |
WorldPreset |
WorldPresetTagsProvider |
Structure |
StructureTagsProvider |
PoiType |
PoiTypeTagsProvider |
BannerPattern |
BannerPatternTagsProvider |
CatVariant |
CatVariantTagsProvider |
PaintingVariant |
PaintingVariantTagsProvider |
Instrument |
InstrumentTagsProvider |
DamageType |
DamageTypeTagsProvider |
* BlockTagsProvider is a Forge added TagsProvider.
ItemTagsProvider#copy
Blocks have item representations to obtain them in the inventory. As such, many of the block tags can also be an item tag. To easily generate item tags to have the same entries as block tags, the #copy method can be used which takes in the block tag to copy from and the item tag to copy to.
//In ItemTagsProvider#addTags
this.copy(EXAMPLE_BLOCK_TAG, EXAMPLE_ITEM_TAG);
Custom Tag Providers
A custom tag provider can be created via a TagsProvider subclass which takes in the registry key to generate tags for.
public RecipeTypeTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries, ExistingFileHelper fileHelper) {
super(output, Registries.RECIPE_TYPE, registries, MOD_ID, fileHelper);
}
Intrinsic Holder Tags Providers
One special type of TagProviders are IntrinsicHolderTagsProviders. When creating a tag using this provider via #tag, the object itself can be used to add itself to the tag via #add. To do so, a function is provided within the constructor to turn an object into its ResourceKey.
// Subtype of `IntrinsicHolderTagsProvider`
public AttributeTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries, ExistingFileHelper fileHelper) {
super(
output,
ForgeRegistries.Keys.ATTRIBUTES,
registries,
attribute -> ForgeRegistries.ATTRIBUTES.getResourceKey(attribute).get(),
MOD_ID,
fileHelper
);
}