Recipe Transfer [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:
- Minecraft 1.21 and 1.21.1: Getting Started, Creating Your Plugin, Recipe Categories
- Minecraft 1.18.2, 1.19.2, and 1.20.1: Recipe Transfer Handlers, Essential Extras
- Minecraft 1.16.5: Recipe Transfer Handlers, Essential Extras
- Minecraft 1.12.2: Recipe Transfer Handlers
Recipe Transfer
Recipe transfer lets players click JEI's transfer button to move matching items from their inventory into your menu's recipe slots.
Register transfer handlers in registerRecipeTransferHandlers.
@Override
public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) {
registration.addRecipeTransferHandler(
CrusherMenu.class,
ModMenus.CRUSHER.get(),
ExampleJeiPlugin.CRUSHING,
0,
2,
3,
36
);
}
The simple overload is for normal menus with contiguous slot ranges:
recipeSlotStart: first slot in the machine's recipe area.recipeSlotCount: number of recipe input slots.inventorySlotStart: first player inventory slot the transfer can use.inventorySlotCount: number of player inventory slots the transfer can use.
These numbers are menu slot indexes, not screen coordinates.
Menu Type
Pass the menu type when you have it. It narrows the handler to the exact menu type.
registration.addRecipeTransferHandler(
CrusherMenu.class,
ModMenus.CRUSHER.get(),
ExampleJeiPlugin.CRUSHING,
0,
2,
3,
36
);
If your integration cannot reference the menu type, pass null and rely on the container class.
Non-Contiguous Slots
Use IRecipeTransferInfo when the slots are still normal Slot instances, but the input or inventory slots are not one continuous range.
registration.addRecipeTransferHandler(new CrusherTransferInfo());
Your IRecipeTransferInfo should return:
getContainerClassgetMenuTypegetRecipeTypecanHandlegetRecipeSlotsgetInventorySlots
Optional methods:
- Override
getHandlingErrorwhencanHandleis false and the player should see why the transfer is unavailable. - Override
requireCompleteSetsif the transfer should keep going when one input has fewer complete sets than the others.
Basic Transfer Plus Extra Behavior
Use IRecipeTransferHandlerHelper#createBasicRecipeTransferInfo and createUnregisteredRecipeTransferHandler when your menu still uses normal slots, but you need to do something extra during transfer.
For example, a machine may need a packet that switches the machine to the right tab, screen, or recipe mode before JEI moves items into the slots.
public final class TabbedMachineTransferHandler implements IRecipeTransferHandler<TabbedMachineMenu, MachineRecipe> {
private final IRecipeTransferHandlerHelper transferHelper;
private final IRecipeTransferHandler<TabbedMachineMenu, MachineRecipe> basicTransferHandler;
public TabbedMachineTransferHandler(IRecipeTransferHandlerHelper transferHelper) {
this.transferHelper = transferHelper;
IRecipeTransferInfo<TabbedMachineMenu, MachineRecipe> transferInfo =
transferHelper.createBasicRecipeTransferInfo(
TabbedMachineMenu.class,
ModMenus.TABBED_MACHINE.get(),
ExampleJeiPlugin.TABBED_MACHINE,
0,
2,
3,
36
);
this.basicTransferHandler = transferHelper.createUnregisteredRecipeTransferHandler(transferInfo);
}
@Override
public Class<? extends TabbedMachineMenu> getContainerClass() {
return TabbedMachineMenu.class;
}
@Override
public Optional<MenuType<TabbedMachineMenu>> getMenuType() {
return Optional.of(ModMenus.TABBED_MACHINE.get());
}
@Override
public IRecipeType<MachineRecipe> getRecipeType() {
return ExampleJeiPlugin.TABBED_MACHINE;
}
@Override
@Nullable
public IRecipeTransferError transferRecipe(
TabbedMachineMenu menu,
MachineRecipe recipe,
IRecipeSlotsView recipeSlots,
Player player,
boolean maxTransfer,
boolean doTransfer
) {
if (!menu.canUseRecipe(recipe)) {
return transferHelper.createUserErrorWithTooltip(
Component.translatable("jei.examplemod.transfer.locked")
);
}
IRecipeTransferError error = basicTransferHandler.transferRecipe(
menu,
recipe,
recipeSlots,
player,
maxTransfer,
false
);
if (error != null) {
return error;
}
if (!doTransfer) {
return null;
}
ExamplePackets.sendToServer(new SelectRecipeTabPacket(menu.containerId, recipe.tabId()));
return basicTransferHandler.transferRecipe(
menu,
recipe,
recipeSlots,
player,
maxTransfer,
true
);
}
}
Register only the wrapping handler:
registration.addRecipeTransferHandler(
new TabbedMachineTransferHandler(registration.getTransferHelper()),
ExampleJeiPlugin.TABBED_MACHINE
);
The important details are:
- The basic handler is unregistered. It is only called by your wrapper, so JEI does not run two handlers for the same transfer.
- The first call uses
doTransfer = falseto check for missing ingredients and normal JEI transfer errors before sending extra packets. - Extra packets or menu changes happen only when
doTransferis true. - If your extra behavior must happen after JEI moves the items, call the basic handler with the real
doTransfervalue first, then send your packet only when it returnsnull.
Full Control
Use a fully custom IRecipeTransferHandler only when the default slot-based movement does not match your menu. Examples include encoded patterns, virtual inventories, security checks, or transfer logic that cannot be expressed as recipe slots plus player inventory slots.
In this example, sendCustomTransferRecipe is your packet that handles all server-side item movement.
public final class CrusherTransferHandler implements IRecipeTransferHandler<CrusherMenu, CrusherRecipe> {
private final IRecipeTransferHandlerHelper transferHelper;
public CrusherTransferHandler(IRecipeTransferHandlerHelper transferHelper) {
this.transferHelper = transferHelper;
}
@Override
public Class<? extends CrusherMenu> getContainerClass() {
return CrusherMenu.class;
}
@Override
public Optional<MenuType<CrusherMenu>> getMenuType() {
return Optional.of(ModMenus.CRUSHER.get());
}
@Override
public IRecipeType<CrusherRecipe> getRecipeType() {
return ExampleJeiPlugin.CRUSHING;
}
@Override
@Nullable
public IRecipeTransferError transferRecipe(
CrusherMenu menu,
CrusherRecipe recipe,
IRecipeSlotsView recipeSlots,
Player player,
boolean maxTransfer,
boolean doTransfer
) {
if (!menu.canUseRecipe(recipe)) {
return transferHelper.createUserErrorWithTooltip(
Component.translatable("jei.examplemod.transfer.locked")
);
}
if (doTransfer) {
ExamplePackets.sendCustomTransferRecipe(menu.containerId, recipe.id(), maxTransfer);
}
return null;
}
}
Register it with the helper from IRecipeTransferRegistration.
registration.addRecipeTransferHandler(
new CrusherTransferHandler(registration.getTransferHelper()),
ExampleJeiPlugin.CRUSHING
);
Use IRecipeTransferRegistration#getTransferHelper to create standard errors. If you only need a small check or packet around JEI's normal item movement, use the wrapper pattern above instead of reimplementing transfer logic.
Client And Server Support
Recipe transfer starts on the client, but real item movement must be validated by the server. The client-side handler should be treated as a request builder, not an authority.
Before writing a custom packet, check whether the normal slot-based transfer is enough. It handles common cases and gives players standard JEI error highlighting.