Loot tables & tags | Minecraft 1.19

Published on

15 min read

Part 5 of 5

In this tutorial we will learn how to drop one or more items when breaking/mining our blocks. For example, when we mine our coal infused ore we will have it drop the ore block item and when we place a steel block in the world, we should be able to mine it, to pick it back up.

To achieve this we need to do a couple things, the first is to look at loot tables which tells the game what items to drop when mining particular blocks and the other thing is to add the blocks to the appropriate tags that tell the game which tool(s) must be used to mine it.

Ores

Before we continue, I realised looking through Minecraft that ores have changed since I last played and that ore blocks no longer drop a ore block item, instead they now draw a raw item. For example, iron ore will now drop raw iron and gold ore will drop raw gold. As such let’s quickly add a new raw coal infused iron item to our mod ready for our ore loot table registration later on in this tutorial.

You can get the texture for the new item from here.

Remember to put the texture into the assets folder as we have for other items and blocks, then add a new entry into the ModBlocks class, like so:

Java
public static final RegistryObject<Item> RAW_COAL_INFUSED_IRON =
        ITEMS.register("raw_coal_infused_iron", () -> new Item(new Item.Properties()));

Then update the ModItemModelProvider in the datagen package to include this new item:

Java
simpleItem(ModItems.RAW_COAL_INFUSED_IRON);

Then finally the ModLanguageProvider:

Java
add(ModItems.RAW_COAL_INFUSED_IRON.get(), "Raw Coal Infused Iron");

Now that we’ve done that, let’s continue on with this tutorial.

Loot tables

In the previous tutorial we looked at data generators to replace manually hand crafting JSON files in the assets folder. We will continue on from this and will create some loot table providers, however you can look at the generated files to see what you would need to do if you were hand crafting them yourself instead.

Let’s start by creating a new package called common within the datagen package, within there another package called loot and then within there create a new class called ModBlockLootTables with the following content:

Java
public class ModBlockLootTables extends BlockLootSubProvider {
    public ModBlockLootTables() {
        super(Set.of(), FeatureFlags.REGISTRY.allFlags());
    }
 
    @Override
    protected void generate() {
    }
 
    @Override
    protected @NotNull Iterable<Block> getKnownBlocks() {
        return ModBlocks.BLOCKS.getEntries().stream().map(RegistryObject::get)::iterator;
    }
}

The generate method is where we will register all of our block drops and will be called by our main loot table provider that we will create next.

The getKnownBlocks method is used to tell the data generator about all of the blocks that the block loot table provider will define drop for. This is useful as it will throw an error if a block is returned by this method, but it has no loot table (drops) registered; this ensures that we don’t accidently miss any.

Now lets create our main loot table provider that will include the block loot table provider we just created. Create another class in the same location called ModLootTableProvider with the following content:

Java
public class ModLootTableProvider {
    public static LootTableProvider create(PackOutput packOutput) {
        return new LootTableProvider(packOutput, Set.of(), List.of(
            new LootTableProvider.SubProviderEntry(ModBlockLootTables::new, LootContextParamSets.BLOCK)
        ));
    }
}

The ModBlockLootTables is a sub-provider that will be used by the main ModLootTableProvider which is the provider that will be used by the data generation. Let’s now go and wire it into the main DataGenerators class like all the other providers so far, it should be included in the server section like so:

Java
if (event.includeServer()) {
    generator.addProvider(true, ModLootTableProvider.create(packOutput));
}

All our block loot table drops will be defined within the generate method that will be called when data generation is ran. Let’s add each of our blocks in turn and define what should be dropped when they are mined.

Simple blocks that should drop themselves

For basic blocks such as our steel block, iron frame and steel frame, we just need to tell the game to drop the block item for that block when mined; this will then allow us to mine and pick up that item when it has been placed in the world; just as it does when you place and mine a iron block in the base game.

Let’s add these three blocks by adding the following code into the generate method:

Java
dropSelf(ModBlocks.STEEL_BLOCK.get());
dropSelf(ModBlocks.IRON_FRAME.get());
dropSelf(ModBlocks.STEEL_FRAME.get());

Ore blocks

For ore blocks we actually want to drop a raw item opposed to the block item itself. In this case we will drop raw coal infused iron whenever we mine a coal infused iron ore block. In a future tutorial we will look at processing this raw item into coal and iron dust, but for now we should be able to get the item when mining the block.

Java
add(ModBlocks.COAL_INFUSED_IRON_ORE.get(), (block) -> createOreDrop(block, ModItems.RAW_COAL_INFUSED_IRON.get()));

If you run the data generation now, it should create all of the loot table assets which you can take a look at if you are interested. But at this point we still will not be able to mine our blocks as we need to also add them to the relavant tags, let’s look at this next.

Tags

To make our mod blocks require certain types and levels of a tool, you must add it to a tag. Minecraft uses tags to categorise blocks/items in a way accessible to datapacks. For example, there is a tag for ingots and one for sapplings.

The base game adds tags for each tool type: mineable/pickaxe, mineable/axe, mineable/shovel, mineable/hoe and for each harvest level: needs_stone_tool, needs_iron_tool, needs_diamond_tool. You can add your block to some of these tags to make certain tools mine it and respect the requiresCorrectToolForDrops() which we used with the Block.Properties earlier.

Tags and providers

Let’s create a class to hold all of our item/block tags and a data generator provider to add our items/blocks to those tags for us. Start by creating a new package called tag under the common package and add a new class in there called ModTags. Use the following content to get us started:

Java
public class ModTags {
    public static void init() {
        Items.init();
        Blocks.init();
    }
 
    public static class Items {
        public static void init() {}
 
        private static TagKey<Item> forgeTag(String name) {
            return ItemTags.create(new ResourceLocation("forge", name));
        }
 
        private static TagKey<Item> tag(String name) {
            return ItemTags.create(new ResourceLocation(TutorialMod.MODID, name));
        }
    }
 
    public static class Blocks {
        public static void init() {}
 
        private static TagKey<Block> forgeTag(String name) {
            return BlockTags.create(new ResourceLocation("forge", name));
        }
 
        private static TagKey<Block> tag(String name) {
            return BlockTags.create(new ResourceLocation(TutorialMod.MODID, name));
        }
    }
}

All of our item tags will be added as static final properties, like we did with items and blocks, within the nested Items class and our block tags within the nested Blocks class.

Next, lets create a tag package under the datagen/common package and a class within there called ModBlockTagsProvider with the following:

Java
public class ModBlockTagsProvider extends BlockTagsProvider {
    public ModBlockTagsProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> lookupProvider, ExistingFileHelper existingFileHelper) {
        super(packOutput, lookupProvider, TutorialMod.MODID, existingFileHelper);
    }
 
    @Override
    protected void addTags(HolderLookup.@NotNull Provider pProvider) {
        registerAxeMineable();
        registerPickaxeMineable();
        registerShovelMineable();
        addHarvestingRequirements();
 
        addOreBlocks();
        addStorageBlocks();
 
        checkAllRegisteredForHarvesting();
    }
 
    private void addHarvestingRequirements() {
    }
 
    @SuppressWarnings("unchecked")
    private void addOreBlocks() {
    }
 
    @SuppressWarnings("unchecked")
    private void addStorageBlocks() {
    }
 
    private void registerAxeMineable() {
        IntrinsicTagAppender<Block> tag = tag(BlockTags.MINEABLE_WITH_AXE);
        getBlocksWithMaterial(Material.WOOD).forEach(tag::add);
    }
 
    private void registerPickaxeMineable() {
        IntrinsicTagAppender<Block> tag = tag(BlockTags.MINEABLE_WITH_PICKAXE);
        getBlocksWithMaterial(Material.STONE, Material.METAL).forEach(tag::add);
    }
 
    private void registerShovelMineable() {
        IntrinsicTagAppender<Block> tag = tag(BlockTags.MINEABLE_WITH_SHOVEL);
        getBlocksWithMaterial(Material.DIRT).forEach(tag::add);
    }
 
    private Stream<Block> getBlocksWithMaterial(Material ...materials) {
        return ModBlocks.BLOCKS.getEntries().stream().map(RegistryObject::get)
                .filter(block -> {
                    Material blockMaterial = block.defaultBlockState().getMaterial();
 
                    return Arrays.stream(materials).anyMatch(material -> material == blockMaterial);
                });
    }
 
    @SafeVarargs
    private <T extends Block> void setMiningLevel(Tiers level, Supplier<T>...blocks) {
        TagKey<Block> tag = switch(level) {
            case WOOD -> Tags.Blocks.NEEDS_WOOD_TOOL;
            case STONE -> BlockTags.NEEDS_STONE_TOOL;
            case IRON -> BlockTags.NEEDS_IRON_TOOL;
            case GOLD -> Tags.Blocks.NEEDS_GOLD_TOOL;
            case DIAMOND -> BlockTags.NEEDS_DIAMOND_TOOL;
            case NETHERITE -> Tags.Blocks.NEEDS_NETHERITE_TOOL;
            default -> throw new IllegalArgumentException("No tag available for " + level.name());
        };
 
        Arrays.stream(blocks).forEach(block -> tag(tag).add(block.get()));
    }
 
    @SafeVarargs
    private <T extends Block> void addOresWithSingularRate(TagKey<Block> blockTag, Supplier<T> ...oreBlocks) {
        Arrays.stream(oreBlocks).forEach(oreBlock -> {
            T block = oreBlock.get();
            tag(blockTag).add(block);
            tag(Tags.Blocks.ORE_RATES_SINGULAR).add(block);
        });
    }
 
    @SafeVarargs
    private <T extends Block> void addOresWithDenseRate(TagKey<Block> blockTag, Supplier<T> ...oreBlocks) {
        Arrays.stream(oreBlocks).forEach(oreBlock -> {
            T block = oreBlock.get();
            tag(blockTag).add(block);
            tag(Tags.Blocks.ORE_RATES_DENSE).add(block);
        });
    }
 
    @SafeVarargs
    private <T extends Block> void addOresWithSparseRate(TagKey<Block> blockTag, Supplier<T> ...oreBlocks) {
        Arrays.stream(oreBlocks).forEach(oreBlock -> {
            T block = oreBlock.get();
            tag(blockTag).add(block);
            tag(Tags.Blocks.ORE_RATES_SPARSE).add(block);
        });
    }
 
    private void checkAllRegisteredForHarvesting() {
        List<TagKey<Block>> knownHarvestTags = ImmutableList.of(
                BlockTags.MINEABLE_WITH_AXE,
                BlockTags.MINEABLE_WITH_PICKAXE,
                BlockTags.MINEABLE_WITH_SHOVEL
        );
 
        Set<ResourceLocation> harvestable = knownHarvestTags.stream()
                .map(this::tag)
                .map(TagAppender::getInternalBuilder)
                .flatMap(b -> b.build().stream())
                .map(Object::toString)
                .map(ResourceLocation::tryParse)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
 
        Set<ResourceLocation> registered = ModBlocks.BLOCKS.getEntries().stream()
                .map(RegistryObject::get)
                .map(ForgeRegistries.BLOCKS::getKey)
                .collect(Collectors.toSet());
 
        Set<ResourceLocation> notHarvestable = Sets.difference(registered, harvestable);
 
        if (!notHarvestable.isEmpty()) {
            notHarvestable.forEach(resourceLocation -> TutorialMod.LOGGER.error("Not harvestable: {}", resourceLocation));
            throw new RuntimeException();
        }
    }
}

The addTags method is the main entry method that will be called during data generation to get all our tags and what is assigned to them etc. Here we’ve called several other methods which have been created below the addTags method to organise the generator a little nicer than sticking it all into the one addTags method.

The addHarvestingRequirements method is where we will register what mining levels are required to mind particular blocks. The addOreBlocks and addStorageBlocks pretty much tell you what they are for based on their names. The registerAxeMinable method is looking through all of our mod added blocks to find those that have used a material of WOOD and automatically added them so theat they can be harvested by an axe. The registerPickaxeMinable method is doing something similar but looking for blocks with a material of STONE or METAL and making them harvestable by pickaxes. The registerShovelMinable, yeah you guessed it, is doing a similar thing by looking for blocks with a material of DIRT and making them harvestable by shovel tools.

To make the other methods easier we have a helper method called getBlocksWithMaterial that will return all our mod blocks that have one of the materials we pass into it; you can see it being used in the registerPickaxeMinable method for example.

The setMiningLevel will be used to set what mining level is required for the tool being used to harvest a block. For example we’ve added blocks that have a material of metal to be minable by pickaxe, but we’ve not stated that it should be a iron level pickaxe or higher, that is what this method will help us do.

All of the ore adding methods are helper methods for registering ores where they have difference rates, here we have a helper method for each of the rate types available in Minecraft; singular, dense and sparse.

Lastly, we have a checkAllRegisteredForHarvesting that is called last after all the other methds and it’s purpose is to check over all of our mod blocks and ensure that each and every one of them has been added to the harvesting tags (for pickaxe, axe and shovel in this case) and will throw an error when a block hasn’t been. This is useful as it ensures that all blocks from our mod can be harvested and that we don’t accidently forget to configure one; there may be times when a block shouldn’t be harvestable and can be excluded from this check by using filters (we will cover this in the future if the scenario presents itself).

Along side the class we’ve just created, create another called ModItemTagsProvider with the following:

Java
public class ModItemTagsProvider extends ItemTagsProvider {
    public ModItemTagsProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> lookupProvider, BlockTagsProvider blocks, ExistingFileHelper existingFileHelper) {
        super(packOutput, lookupProvider, blocks, TutorialMod.MODID, existingFileHelper);
    }
 
    @Override
    protected void addTags(HolderLookup.@NotNull Provider pProvider) {
        addOreBlocks();
        addStorageBlocks();
        addIngots();
    }
 
    private void addOreBlocks() {
    }
 
    @SuppressWarnings("unchecked")
    private void addStorageBlocks() {
    }
 
    private void addIngots() {
    }
}

This is very similar to the block tags provider where we will use smaller more specific methods to organise the generator to make things clearer and easier to maintain, but otherwise works in the same way; you will notice that we don’t have any helper methods this time, we don’t really need them at this point.

Now wire it into our main DataGenerators class within the server section with the following:

Java
ModBlockTagsProvider modBlockTagsProvider = new ModBlockTagsProvider(packOutput, lookupProvider, existingFileHelper);
generator.addProvider(true, modBlockTagsProvider);
generator.addProvider(true, new ModItemTagsProvider(packOutput, lookupProvider, modBlockTagsProvider, existingFileHelper));

Since the ModItemTagsProvider requires access to the ModBlockTagsProvider we create it and assign it to a variable that we can then pass into the creation of the ModItemTagsProvider as well as adding the block tags provider.

We now have everything that we need in place to start creating our tags and dealing with data generation for adding our items and blocks to those tags.

Making our blocks harvestable

As mentioned above, to make our bocks harvestable we need to add it to the appropriate tags to tell the game what type of tool is required and the mining level. We have already achived this with the register minable methods within the block tags provider, but we have not yet set the mining levels, so lets do that now.

Within the addHarvestingRequirements method:

Java
setMiningLevel(Tiers.STONE, ModBlocks.COAL_INFUSED_IRON_ORE);
 
setMiningLevel(
        Tiers.IRON,
        ModBlocks.STEEL_BLOCK,
        ModBlocks.IRON_FRAME,
        ModBlocks.STEEL_FRAME
);

What this will have done is tell the game that a stone tier or higher tool must be used to mine our ore (since we registered it as a pickaxe minable, it would need to be a stone pickaxe or higher). We have also told the game that a iron level tool or higher must be used when harvesting our steel block, iron frame and steel frame blocks (all three of these will require a iron pickaxe or higher as we register them also as a pickaxe minable).

Better mod compatibility

As mentioned before Minecraft and Forge add several tags that we can add our items/blocks and custom tags to, in order to associate them. This will help to make our mod much more compatible with other mods that are adding similar items/blocks. For example we are adding a steel ingot, what if another mod also adds a steel ingot (this means we now have two types of steel ingots in the game with both mods loaded), using a tag for steel ingots means that both mods can declare them as steel ingots using the same tag. Ultimately meaning that recipes can ask for a tag as an ingredient (like a steel ingot tag) rather than a specific steel ingot, this will therefore mean that the steel ingot for either mod can be used in that recipe.

Lets add some new item and block tags which we can add to our item and block tag providers afterwards. Within the ModTags class, add the following properties to the nested Items class:

Java
public static final TagKey<Item> ORES_COAL_INFUSED_IRON = forgeTag("ores/coal_infused_iron");
 
public static final TagKey<Item> RAW_COAL_INFUSED_IRON = forgeTag("raw_materials/coal_infused_iron");
 
public static final TagKey<Item> INGOTS_STEEL = forgeTag("ingots/steel");
 
public static final TagKey<Item> STORAGE_BLOCKS_STEEL = forgeTag("storage_blocks/steel");

Now add the following properties to the nested Blocks class:

Java
public static final TagKey<Block> STORAGE_BLOCKS_STEEL = forgeTag("storage_blocks/steel");
 
public static final TagKey<Block> ORES_COAL_INFUSED_IRON = forgeTag("ores/coal_infused_iron");

We now need to add our items and blocks to our custom tags and add our custom tags in some cases to existing Minecraft or Forge tags. Let’s do that now by going into the ModBlockTagsProvider class and adding the following.

To the addOreBlocks method:

Java
addOresWithSingularRate(
        ModTags.Blocks.ORES_COAL_INFUSED_IRON,
        ModBlocks.COAL_INFUSED_IRON_ORE
);
 
tag(Tags.Blocks.ORES_IN_GROUND_STONE).add(ModBlocks.COAL_INFUSED_IRON_ORE.get());
 
tag(Tags.Blocks.ORES).addTag(ModTags.Blocks.ORES_COAL_INFUSED_IRON);

This has added our ore as an ore that has a singular rate, is an ore that is found in the ground within stone and we’ve added our custom coal infused iron ore tag to the Minecraft ores tag so that the game knows that blocks within our ores tag is an ore.

Within the addStorageBlocks method, add the following:

Java
tag(ModTags.Blocks.STORAGE_BLOCKS_STEEL).add(ModBlocks.STEEL_BLOCK.get());
 
tag(Tags.Blocks.STORAGE_BLOCKS).addTag(ModTags.Blocks.STORAGE_BLOCKS_STEEL);

Similar to before, this has told the game that our steel block is part of our custom steel storage block tag and that tag has been added to the Minecraft storage blocks tag.

Now jump over to the ModItemTagsProvider class and add the following to the addOreBlocks method:

Java
copy(ModTags.Blocks.ORES_COAL_INFUSED_IRON, ModTags.Items.ORES_COAL_INFUSED_IRON);

Here we make use of a copy helper method to copy across tag information from our custom block tag to a related item tag.

Within the addStorageBlocks method:

Java
copy(ModTags.Blocks.STORAGE_BLOCKS_STEEL, ModTags.Items.STORAGE_BLOCKS_STEEL);
 
tag(Tags.Items.STORAGE_BLOCKS).addTag(ModTags.Items.STORAGE_BLOCKS_STEEL);

Then within the addIngots method:

Java
tag(ModTags.Items.INGOTS_STEEL).add(ModItems.STEEL_INGOT.get());
 
tag(Tags.Items.INGOTS).addTag(ModTags.Items.INGOTS_STEEL);

Also, we need to add a new method to add our new raw coal infused iron which we’ve not got yet, so add the following new method and call it from the addTags method below the addIngots(); line.

Java
private void addRawMaterials() {
    tag(ModTags.Items.RAW_COAL_INFUSED_IRON).add(ModItems.RAW_COAL_INFUSED_IRON.get());
 
    tag(Tags.Items.RAW_MATERIALS).addTag(ModTags.Items.RAW_COAL_INFUSED_IRON);
}

Finally, we have now created all of our custom tags, added items and blocks to them etc. However we have one final thing left to do, we need to jump over to our main mod class file TutorialMod and add the following to the commonSetup method like so:

Java
private void commonSetup(final FMLCommonSetupEvent event) {
    event.enqueueWork(ModTags::init);
}

This ensures that our tags and items are initialised and available to the game when our mod is loaded; if we don’t do this, none of our tags will exist or work.

Running our game

Before we run the game and give everything a go to ensure that we can now mine our blocks, make sure that you have run the data generation since adding all of our tags and loot tables etc.

Once you have done this, and it was successful, run the game and give it a go.

Assuming everything has gone to plan, you should be able to place each of our blocks and mine them with a iron pickaxe to pick them up again. With the exception of our custom ore which you can use a stone pickaxe to mine and will drop raw coal infused iron instead of the block itself. Another thing you can try is using silk touch enchanted pickaxe and you should be able to mine and pick up the custom ore, but as the ore block item instead of the raw coal infused iron.

I appologise as this tutorial has been rather long and there is a lot of information to take on board. Have a look at the generated tags output folder to understand what is doing and have a fiddle with tags as you build up your mod to get more experience with using them etc.

You can see the final state of the mod, as of the end of this tutorial, by going to my GitHub repository.

Parts in this series

Below are all of the other parts of this tutorial series.

Workspace setup

Learn how to setup your workspace for your new Minecraft 1.19 mod, using Forge, Java 17 and IntelliJ community.

Published on

11 min read

Part 1

Basic items

Learn how to create your very first basic items that will be used in future parts of this tutorial series.

Published on

10 min read

Part 2

Basic Blocks

Learn how to create your very first basic blocks that will be used in future parts of this tutorial series.

Published on

10 min read

Part 3

Data generators

Learn how to use data generators to automatically create your mod assets to avoid having to hand crank them each time.

Published on

9 min read

Part 4

Loot tables & tags

Learn how to drop one or more items when mining your blocks with loot tables and tags.

Published on

15 min read

Part 5

Share on social media platforms.