Custom Machine Recipes [Minecraft 1.21.11 to 26.2] - mezz/JustEnoughItems GitHub Wiki

[!NOTE] Version note: This page covers Minecraft 1.21.11, 26.1.2, and 26.2.

Related pages for other README-supported versions:

Custom Machine Recipes

Use a custom recipe category when your mod has a recipe type that does not fit one of JEI's built-in categories. The normal flow is:

  1. Create one shared IRecipeType<T>.
  2. Register an IRecipeCategory<T> in registerCategories.
  3. Add recipe instances in registerRecipes.
  4. Register the machine block or item as a crafting station in registerRecipeCatalysts.
public static final IRecipeType<CrusherRecipe> CRUSHING =
  IRecipeType.create("examplemod", "crushing", CrusherRecipe.class);

@Override
public void registerCategories(IRecipeCategoryRegistration registration) {
  IGuiHelper guiHelper = registration.getJeiHelpers().getGuiHelper();
  registration.addRecipeCategories(new CrusherRecipeCategory(guiHelper));
}

@Override
public void registerRecipes(IRecipeRegistration registration) {
  registration.addRecipes(CRUSHING, ClientRecipeCache.getCrusherRecipes());
}

@Override
public void registerRecipeCatalysts(IRecipeCatalystRegistration registration) {
  registration.addCraftingStation(CRUSHING, ModBlocks.CRUSHER);
}

Keep the IRecipeType in one place and reuse it everywhere. JEI uses it to connect categories, recipe lists, crafting stations, click areas, transfer handlers, runtime updates, and recipe lookups.

Recipe Category

AbstractRecipeCategory handles the shared type, title, icon, width, and height. Your category still needs to fill the recipe layout.

public final class CrusherRecipeCategory extends AbstractRecipeCategory<CrusherRecipe> {
  public CrusherRecipeCategory(IGuiHelper guiHelper) {
    super(
      ExampleJeiPlugin.CRUSHING,
      Component.translatable("jei.examplemod.crushing"),
      guiHelper.createDrawableItemLike(ModBlocks.CRUSHER),
      116,
      38
    );
  }

  @Override
  public void setRecipe(IRecipeLayoutBuilder builder, CrusherRecipe recipe, IFocusGroup focuses) {
    builder.addInputSlot(1, 10)
      .setStandardSlotBackground()
      .add(recipe.input());

    builder.addOutputSlot(95, 10)
      .setOutputSlotBackground()
      .add(recipe.result());

    builder.addInvisibleIngredients(RecipeIngredientRole.CRAFTING_STATION)
      .add(ModBlocks.CRUSHER);
  }
}

Use slot roles for recipe lookup behavior:

  • INPUT for consumed ingredients.
  • OUTPUT for results.
  • CRAFTING_STATION for required, non-consumed machines or tools.
  • RENDER_ONLY for visuals that should not affect recipe lookup.

Use addInvisibleIngredients when an ingredient matters for lookup but should not be drawn in the layout.

Layout Details

For normal slots, use setStandardSlotBackground. For a result slot, use setOutputSlotBackground.

For fluids, set the tank size on the slot:

builder.addInputSlot(20, 2)
  .setFluidRenderer(1000, true, 16, 34)
  .add(recipe.fluid(), recipe.fluidAmount());

For recipes where input and output lists rotate together, link the slots:

IRecipeSlotBuilder input = builder.addInputSlot(1, 10)
  .setStandardSlotBackground()
  .addItemStacks(recipe.inputs());

IRecipeSlotBuilder output = builder.addOutputSlot(95, 10)
  .setOutputSlotBackground()
  .addItemStacks(recipe.outputs());

builder.createFocusLink(input, output);

Common Mistakes

  • Do not create a new IRecipeType instance in each registration method. Reuse one static value.
  • Do not put all visual data in draw. Ingredients should be added through setRecipe so JEI can search and focus them.
  • Do not use deprecated catalyst methods on these versions. Use addCraftingStation.
  • Do not rely on client-only recipe discovery unless the data really is client-only. JEI recipe lists should match the player's recipe data.