[JAVA] minecraft1.14.4 MOD development memorandum 1 [Original recipe]

Introduction

When I started developing mods for minecraft1.14.4, I struggled with the lack of information about development, so I would like to make a note for myself and (maybe) help those who will develop mods in the future. In this article, I will explain when creating a recipe format unique to MOD.

Development environment

version
OS Windows10
Forge 28.2.0
JDK AdoptOpenJDK 8u242-b08

Environment

There is already an article that explains the environment construction written by another person, so please refer to that.

↓ This is an article that I referred to when building the environment. Creation of Minecraft 1.14.4 Forge Mod Part 1 [Preparation of development environment IntelliJ IDEA] TNT Modders: Environment Construction

Original recipe

This is necessary when you want to do something that cannot be achieved with a normal recipe, such as refining two items at the same time to obtain an alloy.

procedure

  1. Implement the IRecipe interface
  2. Implement a serializer that reads recipes from JSON files
  3. Register the serializer with Forge

The rough procedure is like this. It is necessary to operate tile entities, etc., but this time we will mainly explain reading and registering recipes.

Implementation

I couldn't think of anything else, so my goal is to register a recipe like the one I wrote above.

Let's implement it according to the procedure. First, implement the IRecipe interface. Contents is like this.

IRecipe.class


public interface IRecipe<C extends IInventory> {
    //Do items in your inventory match recipe ingredients?
    boolean matches(C var1, World var2);

    //Returns a copy of the crafting result
    ItemStack getCraftingResult(C var1);

    //I'm not sure about this. Returns basic true
    boolean canFit(int var1, int var2);

    //Returns the result of crafting
    ItemStack getRecipeOutput();

    default NonNullList<ItemStack> getRemainingItems(C p_179532_1_) {
        NonNullList<ItemStack> nonnulllist = NonNullList.withSize(p_179532_1_.getSizeInventory(), ItemStack.EMPTY);

        for(int i = 0; i < nonnulllist.size(); ++i) {
            ItemStack item = p_179532_1_.getStackInSlot(i);
            if (item.hasContainerItem()) {
                nonnulllist.set(i, item.getContainerItem());
            }
        }

        return nonnulllist;
    }

    default NonNullList<Ingredient> getIngredients() {
        return NonNullList.create();
    }

    default boolean isDynamic() {
        return false;
    }

    default String getGroup() {
        return "";
    }

    default ItemStack getIcon() {
        return new ItemStack(Blocks.CRAFTING_TABLE);
    }

    //Returns the recipe ID
    ResourceLocation getId();

    //Returns a serializer that reads and writes the recipe format JSON file to be created
    IRecipeSerializer<?> getSerializer();

    //Returns the recipe type
    IRecipeType<?> getType();
}

Since it is necessary to implement other than default,

  • boolean mathes(C var1, World var2);
  • ItemStack getCraftingResult(C var1);
  • boolean canFit(int var1, int var2);
  • ItemStack getRecipeOutput();
  • ResourceLocation getId();
  • IRecipeSerializer<?> getSerializer();
  • IRecipeType<?> getType();

We will implement the seven. When implemented, the code will look like this.

ExampleRecipe.java


import com.google.common.collect.Lists;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.item.crafting.IRecipeType;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.common.util.RecipeMatcher;

import java.util.List;

public class ExampleRecipe implements IRecipe<IInventory> {
    public static final IRecipeType<ExampleRecipe> RECIPE_TYPE = new IRecipeType<ExampleRecipe>() {
    };
    protected final ResourceLocation id;
    protected NonNullList<Ingredient> ingredients;
    protected ItemStack result;
    protected int cookTime;

    public ExampleRecipe(ResourceLocation id, NonNullList<Ingredient> ingredients, ItemStack result, int cookTime){
        this.id = id;
        this.ingredients = ingredients;
        this.result = result;
        this.cookTime = cookTime;
    }

    //Do items in your inventory match recipe ingredients?
    @Override
    public boolean matches(IInventory inventory, World world) {
        List<ItemStack> inputs = Lists.newArrayList();
        for(int index = 0; index < inventory.getSizeInventory(); ++index){
            ItemStack input = inventory.getStackInSlot(index);
            if(!input.isEmpty()){
                inputs.add(input);
            }
        }
        return RecipeMatcher.findMatches(inputs, this.ingredients) != null;
    }

    //Returns a copy of the crafting result
    @Override
    public ItemStack getCraftingResult(IInventory inventory) {
        return this.result.copy();
    }

    //I'm not sure about this. Returns basic true
    @Override
    public boolean canFit(int i, int i1) {
        return true;
    }

    //Returns the result of crafting
    @Override
    public ItemStack getRecipeOutput() {
        return this.result;
    }

    //Returns the recipe ID (modid:recipe_name)
    @Override
    public ResourceLocation getId() {
        return this.id;
    }

    //Returns the recipe type
    @Override
    public IRecipeType<?> getType() {
        return RECIPE_TYPE;
    }
}

Other than matches () and canFit (), it is a getter function that returns the contents of the recipe, so I don't think it's difficult.

mathes () uses RecipeMatcher.findMatches () to check if the items in the inventory match the recipe.

I don't know when canFit () will be used, but as far as I can see the vanilla code, I haven't done anything other than return true. I also don't know why there are two functions that return the crafting result. This also refers to vanilla and returns a copy of the result.

By the way, I haven't implemented getSerializer () because I haven't implemented the returning serializer yet. Add it when you have finished implementing the serializer.

import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.util.JSONUtils;

public static class Serializer extends ForgeRegistryEntry<IRecipeSerializer<?>> implements IRecipeSerializer<ExampleRecipe>{
        public Serializer(){
            this.setRegistryName(new ResourceLocation(modId, "serializer_name"));
        }

        private static NonNullList<Ingredient> readIngredients(JsonArray array){
            NonNullList<Ingredient> ingredients = NonNullList.create();
            for(JsonElement element: array){
                ingredients.add(CraftingHelper.getIngredient(element));
            }
            if(ingredients.isEmpty()){
                throw new JsonParseException("No ingredients for smelting recipe");
            }
            return ingredients;
        }
        
        @Override
        public ExampleRecipe read(ResourceLocation recipeId, JsonObject jsonObject) {
            ItemStack result = CraftingHelper.getItemStack(JSONUtils.getJsonObject(jsonObject, "result"), true);
            NonNullList<Ingredient> ingredients = readIngredients(JSONUtils.getJsonArray(jsonObject, "ingredients"));
            int cookTime = JSONUtils.getInt(jsonObject, "process_time", 50);
            return new ExampleRecipe(recipeId, ingredients, result, cookTime);
        }

        //For communication with the server
        @Nullable
        @Override
        public ExampleRecipe read(ResourceLocation recipeId, PacketBuffer packetBuffer) {
            final int index = packetBuffer.readVarInt();
            NonNullList<Ingredient> ingredients = NonNullList.withSize(index, Ingredient.EMPTY);
            for(int col = 0; col < index; ++col){
                ingredients.set(col, Ingredient.read(packetBuffer));
            }
            ItemStack result = packetBuffer.readItemStack();
            int cookTime = packetBuffer.readVarInt();
            return new ExampleRecipe(recipeId, ingredients, result, cookTime);
        }

        //For communication with the server
        @Override
        public void write(PacketBuffer packetBuffer, ExampleRecipe exampleRecipe) {
            packetBuffer.writeVarInt(exampleRecipe.ingredients.size());
            for (Ingredient ingredient : exampleRecipe.ingredients) {
                ingredient.write(packetBuffer);
            }
            packetBuffer.writeItemStack(exampleRecipe.result);
            packetBuffer.writeVarInt(exampleRecipe.cookTime);
        }
    }

Here is the serializer code. There are two read (), but the difference is that the first is from JSON and the second is from network packets.

In the constructor, the ID is assigned to this serializer. This ID will be the type you specify when defining the recipe in the JSON file. Replace modId and serializer_name as appropriate.

readIngredients () is a self-made function because JSONUtils does not have a function to read multiple Ingredients.


Caution

One thing to keep in mind is the order in which they are read. Reading from JSON has no order, so you don't have to worry about it. However, if you do not read from the packet in the same order as you wrote it with write (), you will get an error in the communication when logging in to the server. This is because the reading from the packet is performed in order from the beginning. It's fine for playing offline (because it only reads from JSON), but it won't fit on the server where this bug is occurring. This is fatal, so pay close attention to the order.


Now, I would like to put the implemented serializer into ExampleRecipe.java.

ExampleRecipe.java


import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.item.crafting.IRecipeType;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.JSONUtils;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.common.crafting.CraftingHelper;
import net.minecraftforge.common.util.RecipeMatcher;
import net.minecraftforge.registries.ForgeRegistryEntry;

import javax.annotation.Nullable;
import java.util.List;

public class ExampleRecipe implements IRecipe<IInventory> {
    public static final IRecipeType<ExampleRecipe>
    public static final Serializer SERIALIZER = new Serializer();
    RECIPE_TYPE = new IRecipeType<ExampleRecipe>() {
    };
    protected final ResourceLocation id;
    protected NonNullList<Ingredient> ingredients;
    protected ItemStack result;
    protected int cookTime;

    public ExampleRecipe(ResourceLocation id, NonNullList<Ingredient> ingredients, ItemStack result, int cookTime){
        this.id = id;
        this.ingredients = ingredients;
        this.result = result;
        this.cookTime = cookTime;
    }

    //Do items in your inventory match recipe ingredients?
    @Override
    public boolean matches(IInventory inventory, World world) {
        List<ItemStack> inputs = Lists.newArrayList();
        for(int index = 0; index < inventory.getSizeInventory(); ++index){
            ItemStack input = inventory.getStackInSlot(index);
            if(!input.isEmpty()){
                inputs.add(input);
            }
        }
        return RecipeMatcher.findMatches(inputs, this.ingredients) != null;
    }

    //Returns a copy of the crafting result
    @Override
    public ItemStack getCraftingResult(IInventory inventory) {
        return this.result.copy();
    }

    //I'm not sure about this. Returns basic true
    @Override
    public boolean canFit(int i, int i1) {
        return true;
    }

    //Returns the result of crafting
    @Override
    public ItemStack getRecipeOutput() {
        return this.result;
    }

    //Returns the recipe ID (modid:recipe_name)
    @Override
    public ResourceLocation getId() {
        return this.id;
    }

    //Returns the recipe type
    @Override
    public IRecipeType<?> getType() {
        return RECIPE_TYPE;
    }

    //Returns the serializer
    @Override
    public IRecipeSerializer<?> getSerializer() {
        return SERIALIZER;
    }

    public static class Serializer extends ForgeRegistryEntry<IRecipeSerializer<?>> implements IRecipeSerializer<ExampleRecipe>{
        public Serializer(){
            this.setRegistryName(new ResourceLocation(modId, "serializer_name"));
        }

        private static NonNullList<Ingredient> readIngredients(JsonArray array){
            NonNullList<Ingredient> ingredients = NonNullList.create();
            for(JsonElement element: array){
                ingredients.add(CraftingHelper.getIngredient(element));
            }
            if(ingredients.isEmpty()){
                throw new JsonParseException("No ingredients for smelting recipe");
            }
            return ingredients;
        }

        @Override
        public ExampleRecipe read(ResourceLocation recipeId, JsonObject jsonObject) {
            ItemStack result = CraftingHelper.getItemStack(JSONUtils.getJsonObject(jsonObject, "result"), true);
            NonNullList<Ingredient> ingredients = readIngredients(JSONUtils.getJsonArray(jsonObject, "ingredients"));
            int cookTime = JSONUtils.getInt(jsonObject, "process_time", 50);
            return new ExampleRecipe(recipeId, ingredients, result, cookTime);
        }

        @Nullable
        @Override
        public ExampleRecipe read(ResourceLocation recipeId, PacketBuffer packetBuffer) {
            final int index = packetBuffer.readVarInt();
            NonNullList<Ingredient> ingredients = NonNullList.withSize(index, Ingredient.EMPTY);
            for(int col = 0; col < index; ++col){
                ingredients.set(col, Ingredient.read(packetBuffer));
            }
            ItemStack result = packetBuffer.readItemStack();
            int cookTime = packetBuffer.readVarInt();
            return new ExampleRecipe(recipeId, ingredients, result, cookTime);
        }

        @Override
        public void write(PacketBuffer packetBuffer, ExampleRecipe exampleRecipe) {
            packetBuffer.writeVarInt(exampleRecipe.ingredients.size());
            for (Ingredient ingredient : exampleRecipe.ingredients) {
                ingredient.write(packetBuffer);
            }
            packetBuffer.writeItemStack(exampleRecipe.result);
            packetBuffer.writeVarInt(exampleRecipe.cookTime);
        }
    }
}

This is the final form of code that inherits the IRecipe interface and creates your own recipes. Added getSerializer () and variables to ExampleRecipe.java.

After that, register the serializer with Forge and finish.

RegisterRecipeTypes.java


import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod.EventBusSubscriber(modid = modId, bus = Mod.EventBusSubscriber.Bus.MOD)
public class RegisterRecipeTypes {
    @SubscribeEvent
    public static void registerRecipeSerializer(RegistryEvent.Register<IRecipeSerializer<?>> event){
        event.getRegistry().register(ExampleRecipe.SERIALIZER);
    }
}

Define a recipe

Finally, define the recipe in the recipe format created this time.

example.json


{
  "type": "modId:serializer_name",
  "ingredients": [
    {
       "item": "itemId"
    },
    {
       "item": "itemId"
    }
  ]
  "result": "itemId"
  "process_time": 100
}

in conclusion

I don't know how to create a tile entity, so I will end the explanation with a link to a site that seems to be helpful.

Example Mod published by Mr. Cadiboo

↑ This is a repository for MOD tutorials created by foreigners. Although it is in English, the explanation of the code is written, so please take a look.