Ok, this is a topic that seems to get asked a lot even though there are other tutorials that touch on it, so I'm going to go a bit more in-depth with a real example from my own code.
But first, if you've never made even a single-input furnace or other TileEntity-based Container/Gui, please do that before trying to follow this tutorial. I'm not going to cover all aspects of the Gui, Block and other such things here as there are other tutorials on that.
Be sure to check out MrrGingerNinja's great tutorial on setting up a TileEntity-based inventory.
I also recommend checking out Microjunk's here. That is actually where I started this tutorial
You will learn how to make a furnace that can take recipes with any number of inputs to give a single output, and I have included notes in most of the appropriate places on how to go about adding multiple outputs.
Also, the recipes aren't required to have the same length, so you can have simple and complex smelting recipes all handled by one furnace.
On to the tutorial.
Step 1: Make a Container Class
public class ContainerArcaneInscriber extends Container
{
private TileEntityArcaneInscriber inscriber;
private int lastProgressTime;
private int lastBurnTime;
private int lastItemBurnTime;
// NOTE that here you could add as many slots as you want.
// I have a complementary discharge slot array because the items I use as fuel
// are rechargeable and I want to get them back
// The way I use INPUT slots is both for fuel AND to determine the output result
// You'll probably just want to use it for the latter, in which case you'll need to add a FUEL slot
public static final int INPUT[] = {0,1,2,3,4,5,6};
// delete the DISCHARGE slots if you don't need to keep your used up fuel
public static final int DISCHARGE[] = {7,8,9,10,11,12,13};
// BLANK_SCROLL is NOT fuel, but a final requirement to actually inscribe a scroll. This allows me
// to set up a recipe in the rune slots and see what it is first before actually consuming the runes. You
// probably would change this slot to store FUEL instead, like most furnaces do.
// RECIPE just stores the current recipe so I can keep inscribing scrolls even after the runes are used
// up and to display the recipe on screen. You probably don't need this slot.
public static final int RUNE_SLOTS = INPUT.length, BLANK_SCROLL = RUNE_SLOTS*2, RECIPE = BLANK_SCROLL+1, OUTPUT = RECIPE+1, INV_START = OUTPUT+1, INV_END = INV_START+26, HOTBAR_START = INV_END+1, HOTBAR_END= HOTBAR_START+8;
public ContainerArcaneInscriber(InventoryPlayer inventoryPlayer, TileEntityArcaneInscriber par2TileEntityArcaneInscriber)
{
int i;
this.inscriber = par2TileEntityArcaneInscriber;
// ADD CUSTOM SLOTS
for (i = 0; i < RUNE_SLOTS; ++i) {
this.addSlotToContainer(new Slot(par2TileEntityArcaneInscriber, INPUT[i], 43 + (18*i), 15));
}
for (i = 0; i < RUNE_SLOTS; ++i) {
this.addSlotToContainer(new SlotArcaneInscriberDischarge(par2TileEntityArcaneInscriber, DISCHARGE[i], 43 + (18*i), 64));
}
this.addSlotToContainer(new Slot(par2TileEntityArcaneInscriber, BLANK_SCROLL, 63, 39));
this.addSlotToContainer(new SlotArcaneInscriberRecipe(par2TileEntityArcaneInscriber, RECIPE, 17, 35));
this.addSlotToContainer(new SlotArcaneInscriber(inventoryPlayer.player, par2TileEntityArcaneInscriber, OUTPUT, 119, 39));
// ADD PLAYER INVENTORY
for (i = 0; i < 3; ++i)
{
for (int j = 0; j < 9; ++j)
{
this.addSlotToContainer(new Slot(inventoryPlayer, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
}
}
// ADD PLAYER ACTION BAR
for (i = 0; i < 9; ++i)
{
this.addSlotToContainer(new Slot(inventoryPlayer, i, 8 + i * 18, 142));
}
}
public void addCraftingToCrafters(ICrafting iCrafting)
{
super.addCraftingToCrafters(iCrafting);
iCrafting.sendProgressBarUpdate(this, 0, this.inscriber.inscribeProgressTime);
}
/**
* Looks for changes made in the container, sends them to every listener.
*/
public void detectAndSendChanges()
{
super.detectAndSendChanges();
for (int i = 0; i < this.crafters.size(); ++i)
{
ICrafting icrafting = (ICrafting)this.crafters.get(i);
if (this.lastProgressTime != this.inscriber.inscribeProgressTime)
{
icrafting.sendProgressBarUpdate(this, 0, this.inscriber.inscribeProgressTime);
}
}
this.lastProgressTime = this.inscriber.inscribeProgressTime;
}
@SideOnly(Side.CLIENT)
public void updateProgressBar(int par1, int par2)
{
if (par1 == 0)
{
this.inscriber.inscribeProgressTime = par2;
}
}
@Override
public boolean canInteractWith(EntityPlayer entityplayer)
{
return this.inscriber.isUseableByPlayer(entityplayer);
}
/**
* Called when a player shift-clicks on a slot. You must override this or you will crash when someone does that.
*/
public ItemStack transferStackInSlot(EntityPlayer par1EntityPlayer, int par2)
{
ItemStack itemstack = null;
Slot slot = (Slot)this.inventorySlots.get(par2);
if (slot != null && slot.getHasStack())
{
ItemStack itemstack1 = slot.getStack();
itemstack = itemstack1.copy();
// If item is in TileEntity inventory
if (par2 < INV_START)
{
// try to place in player inventory / action bar
if (!this.mergeItemStack(itemstack1, INV_START, HOTBAR_END+1, true))
{
return null;
}
slot.onSlotChange(itemstack1, itemstack);
}
// Item is in player inventory, try to place in inscriber
else if (par2 > OUTPUT)
{
// if it is a charged rune, place in the first open input slot
if (TileEntityArcaneInscriber.isSource(itemstack1))
{
if (!this.mergeItemStack(itemstack1, INPUT[0], INPUT[RUNE_SLOTS-1]+1, false))
{
return null;
}
}
// if it's a blank scroll, place in the scroll slot
else if (itemstack1.itemID == ArcaneLegacy.scrollBlank.itemID)
{
if (!this.mergeItemStack(itemstack1, BLANK_SCROLL, BLANK_SCROLL+1, false))
{
return null;
}
}
// item in player's inventory, but not in action bar
else if (par2 >= INV_START && par2 < HOTBAR_START)
{
// place in action bar
if (!this.mergeItemStack(itemstack1, HOTBAR_START, HOTBAR_END+1, false))
{
return null;
}
}
// item in action bar - place in player inventory
else if (par2 >= HOTBAR_START && par2 < HOTBAR_END+1 && !this.mergeItemStack(itemstack1, INV_START, HOTBAR_START, false))
{
return null;
}
}
// In one of the inscriber slots; try to place in player inventory / action bar
else if (!this.mergeItemStack(itemstack1, INV_START, HOTBAR_END+1, false))
{
return null;
}
if (itemstack1.stackSize == 0)
{
slot.putStack((ItemStack)null);
}
else
{
slot.onSlotChanged();
}
if (itemstack1.stackSize == itemstack.stackSize)
{
return null;
}
slot.onPickupFromSlot(par1EntityPlayer, itemstack1);
}
return itemstack;
}
}
Step 2: Make your Custom Slots, if needed
Most furnace-like containers will only ever need the first slot I make here; the other two are particular to my special case.
public class SlotArcaneInscriber extends Slot
{
/** The player that is using the GUI where this slot resides. */
private EntityPlayer thePlayer;
private int field_75228_b;
public SlotArcaneInscriber(EntityPlayer par1EntityPlayer, IInventory par2IInventory, int par3, int par4, int par5)
{
super(par2IInventory, par3, par4, par5);
this.thePlayer = par1EntityPlayer;
}
/**
* Check if the stack is a valid item for this slot. Always true beside for the armor slots.
*/
public boolean isItemValid(ItemStack par1ItemStack)
{
return false;
}
/**
* Decrease the size of the stack in slot (first int arg) by the amount of the second int arg. Returns the new
* stack.
*/
public ItemStack decrStackSize(int par1)
{
if (this.getHasStack())
{
this.field_75228_b += Math.min(par1, this.getStack().stackSize);
}
return super.decrStackSize(par1);
}
public void onPickupFromSlot(EntityPlayer par1EntityPlayer, ItemStack par2ItemStack)
{
this.onCrafting(par2ItemStack);
super.onPickupFromSlot(par1EntityPlayer, par2ItemStack);
}
/**
* the itemStack passed in is the output - ie, iron ingots, and pickaxes, not ore and wood. Typically increases an
* internal count then calls onCrafting(item).
*/
protected void onCrafting(ItemStack par1ItemStack, int par2)
{
this.field_75228_b += par2;
this.onCrafting(par1ItemStack);
}
/** The itemStack passed in is the output - ie, iron ingots, and pickaxes, not ore and wood. */
protected void onCrafting(ItemStack par1ItemStack)
{
par1ItemStack.onCrafting(this.thePlayer.worldObj, this.thePlayer, this.field_75228_B);
if (!this.thePlayer.worldObj.isRemote)
{
int i = this.field_75228_b;
float f = SpellRecipes.spells().getExperience(par1ItemStack);
int j;
if (f == 0.0F)
{
i = 0;
}
else if (f < 1.0F)
{
j = MathHelper.floor_float((float)i * f);
if (j < MathHelper.ceiling_float_int((float)i * f) && (float)Math.random() < (float)i * f - (float)j)
{
++j;
}
i = j;
}
while (i > 0)
{
j = EntityXPOrb.getXPSplit(i);
i -= j;
this.thePlayer.worldObj.spawnEntityInWorld(new EntityXPOrb(this.thePlayer.worldObj, this.thePlayer.posX, this.thePlayer.posY + 0.5D, this.thePlayer.posZ + 0.5D, j));
}
}
this.field_75228_b = 0;
}
}
/**
* I have this slot so I can get the discharged runes back for re-use rather than consuming them in the process.
* You probably won't need this kind of slot for a furnace.
*/
class SlotArcaneInscriberDischarge extends Slot
{
private int field_75228_b;
public SlotArcaneInscriberDischarge(IInventory par2IInventory, int par3, int par4, int par5)
{
super(par2IInventory, par3, par4, par5);
}
/**
* Check if the stack is a valid item for this slot. Always true beside for the armor slots.
*/
public boolean isItemValid(ItemStack par1ItemStack)
{
return false;
}
/**
* Decrease the size of the stack in slot (first int arg) by the amount of the second int arg. Returns the
* new stack.
*/
public ItemStack decrStackSize(int par1)
{
if (this.getHasStack())
{
this.field_75228_b += Math.min(par1, this.getStack().stackSize);
}
return super.decrStackSize(par1);
}
public void onPickupFromSlot(EntityPlayer par1EntityPlayer, ItemStack par2ItemStack)
{
super.onPickupFromSlot(par1EntityPlayer, par2ItemStack);
}
}
/**
* I made this slot to show the output of the current runes on screen, so player's know what they are about to make
*/
class SlotArcaneInscriberRecipe extends Slot
{
private int field_75228_b;
public SlotArcaneInscriberRecipe(IInventory par2IInventory, int par3, int par4, int par5)
{
super(par2IInventory, par3, par4, par5);
}
/**
* Check if the stack is a valid item for this slot. Always true beside for the armor slots.
*/
public boolean isItemValid(ItemStack par1ItemStack)
{
return false;
}
/**
* Return whether this slot's stack can be taken from this slot.
*/
public boolean canTakeStack(EntityPlayer par1EntityPlayer)
{
return false;
}
/**
* Decrease the size of the stack in slot (first int arg) by the amount of the second int arg. Returns the new
* stack.
*/
public ItemStack decrStackSize(int par1)
{
if (this.getHasStack())
{
this.field_75228_b += Math.min(par1, this.getStack().stackSize);
}
return super.decrStackSize(par1);
}
public void onPickupFromSlot(EntityPlayer par1EntityPlayer, ItemStack par2ItemStack)
{
// can't be picked up so nothing here
}
}
Step 3: Your Tile Entity Class
public class TileEntityArcaneInscriber extends TileEntity implements ISidedInventory
{
private static final int[] slots_top = new int[] {0};
private static final int[] slots_bottom = new int[] {2, 1};
private static final int[] slots_sides = new int[] {1};
/** Array bounds = number of slots in ContainerArcaneInscriber */
private ItemStack[] inscriberInventory = new ItemStack[ContainerArcaneInscriber.INV_START];
/** Time required to scribe a single scroll */
private static final int INSCRIBE_TIME = 100, RUNE_CHARGE_TIME = 400;
/** The number of ticks that the inscriber will keep inscribing */
public int currentInscribeTime;
/** The number of ticks that a charged rune will provide */
public int inscribeTime = 400;
/** The number of ticks that the current scroll has been inscribing for */
public int inscribeProgressTime;
private String displayName = "Arcane Inscriber";
public TileEntityArcaneInscriber() {
}
@Override
public int getSizeInventory() {
return inscriberInventory.length;
}
@Override
public ItemStack getStackInSlot(int slot) {
return this.inscriberInventory[slot];
}
@Override
public ItemStack decrStackSize(int slot, int amt)
{
ItemStack stack = getStackInSlot(slot);
if (stack != null) {
if (stack.stackSize <= amt) {
setInventorySlotContents(slot, null);
} else {
stack = stack.splitStack(amt);
if (stack.stackSize == 0) {
setInventorySlotContents(slot, null);
}
}
}
return stack;
}
@Override
public ItemStack getStackInSlotOnClosing(int slot)
{
ItemStack stack = getStackInSlot(slot);
if (stack != null) { setInventorySlotContents(slot, null); }
return stack;
}
@Override
public void setInventorySlotContents(int slot, ItemStack stack)
{
inscriberInventory[slot] = stack;
if (stack != null && stack.stackSize > getInventoryStackLimit()) {
stack.stackSize = getInventoryStackLimit();
}
}
@Override
public String getInvName() {
return this.isInvNameLocalized() ? this.displayName : "container.arcaneinscriber";
}
/**
* If this returns false, the inventory name will be used as an unlocalized name, and translated into the player's
* language. Otherwise it will be used directly.
*/
public boolean isInvNameLocalized()
{
return this.displayName != null && this.displayName.length() > 0;
}
/**
* Sets the custom display name to use when opening a GUI linked to this tile entity.
*/
public void setGuiDisplayName(String par1Str)
{
this.displayName = par1Str;
}
@Override
public int getInventoryStackLimit() {
return 64;
}
/**
* Returns an integer between 0 and the passed value representing how
* close the current item is to being completely cooked
*/
@SideOnly(Side.CLIENT)
public int getInscribeProgressScaled(int par1)
{
return this.inscribeProgressTime * par1 / INSCRIBE_TIME;
}
/**
* Returns an integer between 0 and the passed value representing how much burn time is left on the current fuel
* item, where 0 means that the item is exhausted and the passed value means that the item is fresh
*/
@SideOnly(Side.CLIENT)
public int getInscribeTimeRemainingScaled(int par1)
{
return this.currentInscribeTime * par1 / this.INSCRIBE_TIME;
}
/**
* Returns true if the furnace is currently burning
*/
public boolean isInscribing()
{
return this.currentInscribeTime > 0;
}
/**
* Allows the entity to update its state. Overridden in most subclasses, e.g. the mob spawner uses this to count
* ticks and creates a new spawn inside its implementation.
*/
public void updateEntity()
{
boolean flag = this.currentInscribeTime > 0;
boolean flag1 = false;
if (this.currentInscribeTime > 0)
{
--this.currentInscribeTime;
// Container recipe doesn't match current non-null InscribingResult
flag1 = (this.inscriberInventory[ContainerArcaneInscriber.RECIPE] != this.getCurrentRecipe() && this.getCurrentRecipe() != null);
// Recipe changed - reset timer and current recipe slot
if (flag1)
{
this.inscriberInventory[ContainerArcaneInscriber.RECIPE] = this.getCurrentRecipe();
this.onInventoryChanged();
this.currentInscribeTime = 0;
}
}
if (!this.worldObj.isRemote)
{
if (this.currentInscribeTime == 0)
{
flag1 = (this.inscriberInventory[ContainerArcaneInscriber.RECIPE] != this.getCurrentRecipe());
this.inscriberInventory[ContainerArcaneInscriber.RECIPE] = this.getCurrentRecipe();
// Recipe changed - update inventory
// (only because I'm showing the output for the current recipe on screen)
if (flag1) { this.onInventoryChanged(); }
if (this.canInscribe()) {
// This is the equivalent of getItemBurnTime from furnace. Note again that I am setting
// my burn time based on an INPUT slot, even though this is generally done with FUEL
this.currentInscribeTime = this.getInscriberChargeTime(this.inscriberInventory[0]);
}
if (this.currentInscribeTime > 0)
{
flag1 = true;
// This is where you decrement your FUEL slot's inventory.
// However, since I use INPUT as FUEL and need to save the used up FUEL in DISCHARGE,
// I will use a for loop to decrement all of the inputs and increment all of the discharge slots
// Yours will probably look much simpler - look at the vanilla Furnace code to see an example
for (int i = 0; i < ContainerArcaneInscriber.RUNE_SLOTS; ++i)
{
if (this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]] != null)
{
--this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]].stackSize;
if (this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]] != null) {
++this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].stackSize;
}
else {
ItemStack discharge = new ItemStack(ArcaneLegacy.runeBasic,1,this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]].getItemDamage());
this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]] = discharge.copy();
}
if (this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]].stackSize == 0)
{
this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]] = this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]].getItem().getContainerItemStack(inscriberInventory[ContainerArcaneInscriber.INPUT[i]]);
}
}
}
}
}
// Everything's good to go, so increment furnace progress until it reaches the required time to smelt your item(s)
if (this.isInscribing() && this.canInscribe())
{
++this.inscribeProgressTime;
if (this.inscribeProgressTime == INSCRIBE_TIME)
{
this.inscribeProgressTime = 0;
this.inscribeScroll();
flag1 = true;
}
}
else
{
this.inscribeProgressTime = 0;
}
if (flag != this.currentInscribeTime > 0)
{
flag1 = true;
BlockArcaneInscriber.updateInscriberBlockState(this.currentInscribeTime > 0, this.worldObj, this.xCoord, this.yCoord, this.zCoord);
}
}
if (flag1)
{
this.onInventoryChanged();
}
}
/**
* Returns true if the inscriber can inscribe a scroll;
* i.e. has a blank scroll, has a charged rune, destination stack isn't full, etc.
* This method will look very different for every furnace depending on whatever requirements
* you decide are necessary for your furnace to smelt an item or items
*/
private boolean canInscribe()
{
boolean canInscribe = true;
// Still time remaining to inscribe current recipe
if (this.isInscribing() && this.inscriberInventory[ContainerArcaneInscriber.RECIPE] != null)
{
canInscribe = (this.inscriberInventory[ContainerArcaneInscriber.BLANK_SCROLL] == null ? false : true);
}
// No charged rune in first input slot
else if (this.inscriberInventory[ContainerArcaneInscriber.INPUT[0]] == null)
{
canInscribe = false;
}
// No blank scrolls to inscribe
else if (this.inscriberInventory[ContainerArcaneInscriber.BLANK_SCROLL] == null)
{
canInscribe = false;
}
// Check if any of the discharge slots are full
else
{
for (int i = 0; i < ContainerArcaneInscriber.RUNE_SLOTS && canInscribe; ++i)
{
if (this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]] != null)
{
// Check if input[i] and discharge[i] are mismatched
if (this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]] != null)
{
canInscribe = ((this.inscriberInventory[ContainerArcaneInscriber.INPUT[i]].getItemDamage() == this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].getItemDamage())
&& this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].stackSize < this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].getMaxStackSize());
}
else
{
canInscribe = this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].stackSize < this.inscriberInventory[ContainerArcaneInscriber.DISCHARGE[i]].getMaxStackSize();
}
}
}
}
// If all of your above requirements are met, then on to the final check. Most of these should
// be the same as I have here regardless of furnace type, but notice I have an extra check for
// a null itemstack because the smelting result might be stored in my recipe slot even if all
// other slots in the furnace are empty.
if (canInscribe) {
ItemStack itemstack = getCurrentRecipe();
if (itemstack == null) { itemstack = this.inscriberInventory[ContainerArcaneInscriber.RECIPE]; }
// Invalid recipe
if (itemstack == null) return false;
// Recipe is different from the current recipe
if (this.inscriberInventory[ContainerArcaneInscriber.RECIPE] != null && !this.inscriberInventory[ContainerArcaneInscriber.RECIPE].isItemEqual(itemstack)) return false;
// Output slot is empty, inscribe away!
if (this.inscriberInventory[ContainerArcaneInscriber.OUTPUT] == null) return true;
// Current scroll in output slot is different than recipe output
if (!this.inscriberInventory[ContainerArcaneInscriber.OUTPUT].isItemEqual(itemstack)) return false;
// Inscribing may surpass stack size limit
int result = inscriberInventory[ContainerArcaneInscriber.OUTPUT].stackSize + itemstack.stackSize;
return (result <= getInventoryStackLimit() && result <= itemstack.getMaxStackSize());
}
else
{
return canInscribe;
}
}
// If you have more than one output per recipe, this will return an ItemStack[] array instead
public ItemStack getCurrentRecipe() {
eturn SpellRecipes.spells().getInscribingResult(this.inscriberInventory);
}
/**
* Inscribe a blank scroll with the last current recipe
*/
public void inscribeScroll()
{
if (this.canInscribe())
{
// If you had multiple outputs, this would be an ItemStack[] array that you would
// then need to iterate through, checking if each index was null and if not,
// finding the correct output slot to try and add to
ItemStack inscribeResult = this.inscriberInventory[ContainerArcaneInscriber.RECIPE];
if (inscribeResult != null)
{
if (this.inscriberInventory[ContainerArcaneInscriber.OUTPUT] == null)
{
this.inscriberInventory[ContainerArcaneInscriber.OUTPUT] = inscribeResult.copy();
}
else if (this.inscriberInventory[ContainerArcaneInscriber.OUTPUT].isItemEqual(inscribeResult))
{
inscriberInventory[ContainerArcaneInscriber.OUTPUT].stackSize += inscribeResult.stackSize;
}
// This is where you'd decrement all your INPUT slots, but for me, I only need to do that for
// BLANK_SCROLL since I used my INPUT as fuel earlier
--this.inscriberInventory[ContainerArcaneInscriber.BLANK_SCROLL].stackSize;
if (this.inscriberInventory[ContainerArcaneInscriber.BLANK_SCROLL].stackSize <= 0)
{
this.inscriberInventory[ContainerArcaneInscriber.BLANK_SCROLL] = null;
}
}
}
}
/**
* Returns the number of ticks that the supplied rune will keep
* the inscriber running, or 0 if the rune isn't charged
*/
public static int getInscriberChargeTime(ItemStack rune)
{
if (rune != null && rune.itemID == ArcaneLegacy.runeCharged.itemID) {
return RUNE_CHARGE_TIME;
} else { return 0; }
}
/**
* Return true if item is an energy source (i.e. a charged rune)
*/
public static boolean isSource(ItemStack itemstack)
{
return getInscriberChargeTime(itemstack) > 0;
}
@Override
public boolean isUseableByPlayer(EntityPlayer player)
{
return worldObj.getBlockTileEntity(xCoord, yCoord, zCoord) == this &&
player.getDistanceSq(xCoord + 0.5, yCoord + 0.5, zCoord + 0.5) < 64;
}
@Override
public void openChest() {}
@Override
public void closeChest() {}
/**
* Returns true if automation is allowed to insert the given stack (ignoring stack size) into the given slot.
*/
public boolean isItemValidForSlot(int slot, ItemStack itemstack)
{
boolean isValid = false;
if (slot >= ContainerArcaneInscriber.INPUT[0] && slot <= ContainerArcaneInscriber.INPUT[ContainerArcaneInscriber.RUNE_SLOTS-1])
{
isValid = itemstack.getItem().itemID == ArcaneLegacy.runeCharged.itemID;
}
else if (slot == ContainerArcaneInscriber.BLANK_SCROLL)
{
isValid = itemstack.getItem().itemID == ArcaneLegacy.scrollBlank.itemID;
}
return isValid;
}
/**
* Returns an array containing the indices of the slots that can be accessed by automation on the given side of this
* block.
*/
public int[] getAccessibleSlotsFromSide(int par1)
{
return par1 == 0 ? slots_bottom : (par1 == 1 ? slots_top : slots_sides);
}
/**
* Returns true if automation can insert the given item in the given slot from the given side.
* Args: Slot, item, side
*/
public boolean canInsertItem(int par1, ItemStack par2ItemStack, int par3)
{
return this.isItemValidForSlot(par1, par2ItemStack);
}
/**
* Returns true if automation can extract the given item in the given slot from the given side.
* Args: Slot, item, side
*/
public boolean canExtractItem(int slot, ItemStack itemstack, int side)
{
return (slot == ContainerArcaneInscriber.OUTPUT || (slot >= ContainerArcaneInscriber.DISCHARGE[0] && slot <= ContainerArcaneInscriber.DISCHARGE[ContainerArcaneInscriber.RUNE_SLOTS-1]));
}
@Override
public void readFromNBT(NBTTagCompound tagCompound)
{
super.readFromNBT(tagCompound);
NBTTagList nbttaglist = tagCompound.getTagList("Items");
this.inscriberInventory = new ItemStack[this.getSizeInventory()];
for (int i = 0; i < nbttaglist.tagCount(); ++i)
{
NBTTagCompound nbttagcompound1 = (NBTTagCompound)nbttaglist.tagAt(i);
int b0 = nbttagcompound1.getInteger("Slot");
f (b0 >= 0 && b0 < this.inscriberInventory.length)
{
this.inscriberInventory[b0] = ItemStack.loadItemStackFromNBT(nbttagcompound1);
}
}
this.currentInscribeTime = tagCompound.getShort("IncribeTime");
this.inscribeProgressTime = tagCompound.getShort("InscribeProgress");
// this.inscribeTime = INSCRIBE_TIME;
if (tagCompound.hasKey("CustomName"))
{
this.displayName = tagCompound.getString("CustomName");
}
}
@Override
public void writeToNBT(NBTTagCompound tagCompound)
{
super.writeToNBT(tagCompound);
tagCompound.setShort("InscribeTime", (short)this.currentInscribeTime);
tagCompound.setShort("InscribeProgress", (short)this.inscribeProgressTime);
NBTTagList nbttaglist = new NBTTagList();
for (int i = 0; i < this.inscriberInventory.length; ++i)
{
if (this.inscriberInventory[i] != null)
{
NBTTagCompound nbttagcompound1 = new NBTTagCompound();
nbttagcompound1.setInteger("Slot", i);
this.inscriberInventory[i].writeToNBT(nbttagcompound1);
nbttaglist.appendTag(nbttagcompound1);
}
}
tagCompound.setTag("Items", nbttaglist);
if (this.isInvNameLocalized())
{
tagCompound.setString("CustomName", this.displayName);
}
}
}
Step 4: Your Recipe class
This is where much of the magic happens.
We'll use HashMaps to store an array of item metadata values to return an ItemStack result. If the items in your recipes will have different Item IDs, then add those to the hashmap as well. I only needed metadata values because all my recipes only involve a single rune item with many subtypes.
public class SpellRecipes
{
private static final SpellRecipes spells = new SpellRecipes();
// This creates a HashMap whose Key is a specific, ordered List of Integers
// If you want multiple outputs from one recipe, change the ItemStack to an ItemStack[]
// (and of course adjust your TileEntity and Container code)
private HashMap<List<Integer>, ItemStack> metaInscribingList = new HashMap<List<Integer>, ItemStack>();
// Same as above except it gives us the experience for each crafting result
private HashMap<List<Integer>, Float> metaExperience = new HashMap<List<Integer>, Float>();
/**
* Used to call methods addInscribing and getInscribingResult.
*/
public static final SpellRecipes spells() {
return spells;
}
/**
* Adds all recipes to the HashMap
*/
private SpellRecipes()
{
// Note that I have defined constant integer values in my ItemRune class
// such that ItemRune.RUNE_NAME returns the appropriate int value for the
// corresponding metadata
// This one only takes 2 Items to craft:
this.addInscribing(Arrays.asList(ItemRune.RUNE_CREATE,ItemRune.RUNE_FIRE),new ItemStack(ArcaneLegacy.scrollCombust), 0.3F);
// This one takes 7 Items to craft (the max number of slots currently in my Arcane Inscriber, but I could easily add more):
this.addInscribing(Arrays.asList(ItemRune.RUNE_AUGMENT,ItemRune.RUNE_AUGMENT,ItemRune.RUNE_CREATE,ItemRune.RUNE_AUGMENT,ItemRune.RUNE_LIFE,ItemRune.RUNE_SPACE,ItemRune.RUNE_TIME),new ItemStack(ArcaneLegacy.scrollHealAuraI), 1.0F);
// Here's a generic format for adding both item ID and metadata:
this.addInscribing(Arrays.asList(Item1.itemID, metadata1, Item2.itemID, metadata2, ... etc.), new ItemStack(craftResult.itemID, stacksize, metadata), XP);
// You could also skip the addInscribing method and add directly to the HashMap:
metaInscribingList.put(Arrays.asList(Item1.itemID, meta1, Item2.itemID, meta2,... ItemN.itemID, metaN), ItemStack(craftResult.itemID,stacksize,metadata));
// Note that XP must be added each time as well, effectively doubling the lines of code in this method
metaExperience.put(Arrays.asList(ItemResult.itemID, ItemResult.getItemDamage()), experience);
}
/**
* Adds an array of runes, the resulting scroll, and experience given
*/
public void addInscribing(List<Integer> runes, ItemStack scroll, float experience)
{
// Check if recipe already exists and print conflict information:
if (metaInscribingList.containsKey(runes))
{
System.out.println("[WARNING] Conflicting recipe: " + runes.toString() + " for " + metaInscribingList.get(runes).toString());
}
else
{
// Add new recipe to the HashMap... wow, it looks so simple like this
metaInscribingList.put(runes, scroll);
metaExperience.put(Arrays.asList(scroll.itemID, scroll.getItemDamage()), experience);
}
}
/**
* Used to get the resulting ItemStack form a source inventory (fed to it by the contents of the slots in your container)
* @param item The Source inventory from your custom furnace input slots
* @return The result ItemStack (NOTE: if you want multiple outputs, change to ItemStack[] and adjust accordingly)
*/
public ItemStack getInscribingResult(ItemStack[] runes)
{
// count the recipe length so we can make the appropriate sized array
int recipeLength = 0;
for (int i = 0; i < runes.length && runes[i] != null && i < ContainerArcaneInscriber.RUNE_SLOTS; ++i)
{
// +1 for metadata value of itemstack, add another +1 if you also need the itemID
++recipeLength;
}
// make the array and fill it with the integer values from the passed in ItemStacks
// Note that I'm only using the metadata value as all my runes have the same itemID
Integer[] idIndex = new Integer[recipeLength];
for (int i = 0; i < recipeLength; ++i) {
// if you need itemID as well put this:
// idIndex[i] = (Integer.valueOf(runes[i].itemID));
// be sure to increment i before you do the metadata if you added an itemID
idIndex[i] = (Integer.valueOf(runes[i].getItemDamage()));
}
// And use it as the key to get the correct result from the HashMap:
return (ItemStack) metaInscribingList.get(Arrays.asList(idIndex));
}
/**
* Grabs the amount of base experience for this item to give when pulled from the furnace slot.
*/
public float getExperience(ItemStack item)
{
if (item == null || item.getItem() == null)
{
return 0;
}
float ret = -1; // value returned by "item.getItem().getSmeltingExperience(item);" when item doesn't specify experience to give
if (ret < 0 && metaExperience.containsKey(Arrays.asList(item.itemID, item.getItemDamage())))
{
ret = metaExperience.get(Arrays.asList(item.itemID, item.getItemDamage()));
}
return (ret < 0 ? 0 : ret);
}
public Map<List<Integer>, ItemStack> getMetaInscribingList()
{
return metaInscribingList;
}
}
And that's it! Congratulations, you can now make a ridiculously flexible furnace.
HashMaps are our friend
Be sure to check out my other tutorials - they are all on github here or follow the forum links below:
Agreed, very nice, well written tutorial. Between this and the other tutorials we all have here, there should be no reason one could not make a furnace in any fashion.
Are you using the BLANK_SCROLL slot as your fuel slot in this example? I am trying to go through your code and make sure I understand it all so I can make my own. I won't need your discharge slots or the recipe slot, but I will need the fuel slot, which I am assuming is the blank scrolls in your mod?
EDIT: I sent you a PM with better information. If you could read it over when you have time, I would greatly appreciate it.
Will do and yes, the blank scroll the INPUT slots are my 'fuel'
EDIT: Actually, sorry, the INPUT slots are what I use to set the 'fuel' timer. I'll try to explain it more clearly in the code, as this was originally more of an illustration of concept than a full on tutorial.
Alright, I updated the notes somewhat in the code. It should be more clear now what all the slots are for.
Thanks a ton for that! I was able to complete my (test) triple input furnace and it worked perfectly after you cleared those things up for me. Now to work on making a dual output
btw, do you or Maze happen to have any links to learn more about hashmaps? My ultimate goal is to be able to create dual output crafting tables but I am not comfortable enough yet with hashmaps to write out an entirely different crafting manager for what it would require.
Thanks a ton for that! I was able to complete my (test) triple input furnace and it worked perfectly after you cleared those things up for me. Now to work on making a dual output
btw, do you or Maze happen to have any links to learn more about hashmaps? My ultimate goal is to be able to create dual output crafting tables but I am not comfortable enough yet with hashmaps to write out an entirely different crafting manager for what it would require.
Hm, I haven't seen any other than basic information like this. However, if you look at my recipe code, that's basically exactly what you'd do for the crafting table as well.
Use HashMap<List<Integer>, ItemStack[]>
The list is the hashmap 'key'. It contains all the item IDs and metadata for the recipe, and returns the ItemStack[] crafting results. I'm not sure how you'd go about shaped recipes other than to include (0,0) for any empty slots as part of the key signature, which would make all of your keys 18 entries (at 2 integers per item) long. Pretty big key
Hm, I haven't seen any other than basic information like this. However, if you look at my recipe code, that's basically exactly what you'd do for the crafting table as well.
Use HashMap<List<Integer>, ItemStack[]>
The list is the hashmap 'key'. It contains all the item IDs and metadata for the recipe, and returns the ItemStack[] crafting results. I'm not sure how you'd go about shaped recipes other than to include (0,0) for any empty slots as part of the key signature, which would make all of your keys 18 entries (at 2 integers per item) long. Pretty big key
Yeah that would be a bit much. Luckily, for the type of crafting table I want to make though, it would only require shapeless recipes.
I havent followed this tutorial,i just created my own furnace. However, in game is your furnace icon backwards? Like on here http://www.minecraft.../#entry24206463
I actually am having this issue currently. I read that thread but didnt see a fix posted. Did you ever figure that out?
It is indeed. Im not sure what would determine the texture positioning of the block icon itself. I have been trying to work this out for a couple days now.
Okay a fix for the backwards block. This is not my code, it was created by shadowbeast007 of the fossil mod revival team ( i was a part of the time when I got the code). So please note that I do not know much about this code at all.....
You will need to make a new class....you can call it furnace render or what ever you need...mine is called RenderHighSpeed. This is a rendering issue imposed by minecraft because of the new way they are rendering blocks.....
import net.minecraft.block.Block;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.world.IBlockAccess;
import org.lwjgl.opengl.GL11;
import bcblocks.highspeed.HighSpeed;
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
public class RenderHighSpeed implements ISimpleBlockRenderingHandler
{
public static final RenderHighSpeed INSTANCE = new RenderHighSpeed();
@Override
public void renderInventoryBlock(Block block, int metadata, int modelID, RenderBlocks renderer)
{
int meta=3;
if(block.blockID == HighSpeed.highSpeedFurnaceActive.blockID)meta=1;
Tessellator tessellator = Tessellator.instance;
block.setBlockBoundsForItemRender();
renderer.setRenderBoundsFromBlock(block);
GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F);
GL11.glTranslatef(-0.5F, -0.5F, -0.5F);
tessellator.startDrawingQuads();
tessellator.setNormal(0.0F, -1.0F, 0.0F);
renderer.renderFaceYNeg(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 0, meta));
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setNormal(0.0F, 1.0F, 0.0F);
renderer.renderFaceYPos(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 1, meta));
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setNormal(0.0F, 0.0F, -1.0F);
renderer.renderFaceZNeg(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 2, meta));
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setNormal(0.0F, 0.0F, 1.0F);
renderer.renderFaceZPos(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 3, meta));
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setNormal(-1.0F, 0.0F, 0.0F);
renderer.renderFaceXNeg(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 4, meta));
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setNormal(1.0F, 0.0F, 0.0F);
renderer.renderFaceXPos(block, 0.0D, 0.0D, 0.0D, renderer.getBlockIconFromSideAndMetadata(block, 5, meta));
tessellator.draw();
GL11.glTranslatef(0.5F, 0.5F, 0.5F);
}
@Override
public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, RenderBlocks renderer)
{
int direction = renderer.blockAccess.getBlockMetadata(x, y, z) & 3;
if(direction>0)
renderer.uvRotateTop = direction-1;
else
renderer.uvRotateTop = 3;
boolean flag = renderer.renderStandardBlock(block, x, y, z);
renderer.uvRotateTop = 0;
return flag;
}
@Override
public boolean shouldRender3DInInventory()
{
return true;
}
@Override
public int getRenderId()
{
return 2105;
}
}
Notice at the bottom the new render id...it is necessary "2105" is what I am using, I have found that they tend to not work if they are lower than 1500. Then you will need to register it in the mod class like this....
Hm, interesting. My 'furnaces' use the top icon, not the front, but when I change it they do indeed render backwards! I'll see if I can figure this out.
We think this works, but this isn't actually called for rendering the block as an item (i.e. in the inventory) because at that time the block doesn't have any metadata, so par2 is always 0.
public void renderBlockAsItem(Block par1Block, int par2, float par3)
{
Tessellator tessellator = Tessellator.instance;
boolean flag = par1Block.blockID == Block.grass.blockID;
if (par1Block == Block.dispenser || par1Block == Block.dropper || par1Block == Block.furnaceIdle)
{
par2 = 3;
}
In there you can see where it sets the side to render for only these blocks. The easiest fix is to just add your block to this list. Worked for me
If you're being a good programmer and not editing base classes, you'll have more work cut out for you. Here's a little class I made that should work:
public class RenderCustomFurnace extends RenderBlocks {
public RenderCustomFurnace(IBlockAccess par1iBlockAccess) {
super(par1iBlockAccess);
// TODO Auto-generated constructor stub
}
public RenderCustomFurnace() {
// probably need to copy/paste some code here
}
@Override
public void renderBlockAsItem(Block par1Block, int par2, float par3)
{
// just pre-set the side as '3'
super.renderBlockAsItem(par1Block, par2, 3);
}
}
Then you'd need to tell your block not to render as a normal block and register this custom render to it somehow (sorry I don't know how to do that part, only done that for tile entity special render before)
Anyway, I only took the easy road as a test and it worked. Like I said, my furnaces use the top
My block shares functionality with furnaces, but it is a table for scribing scrolls so I have the active/idle texture rendering on the top instead of on the side. Therefore, it works by default, as I don't care about the side rendered in the inventory.
However, for testing I set it up just like a furnace so that the active/idle texture is on the side, at which point it was backwards.
I then edited the base class RenderBlocks to include my block in the line with furnaceIdle etc so that the side rendered would be set to '3'. This achieved the desired result of rendering the item block with the correct face forward, but with the downside of having to edit a base class.
See my suggestion on extending RenderBlocks for an idea on how to get around base edits.
Original post:
Looks like a crafting table, because it's not a furnace, it's where I inscribe scrolls. Anyway, I tested it out with the icon in front instead of on top and it worked fine, just like a furnace. The front side rendered in the inventory, instead of getting the back.
So how did you ". The front side rendered in the inventory, instead of getting the back." Without editing base classes or extending RenderBlocks and overriding that method
Please read my earlier post carefully. I explicitly state that I DID edit base classes to test it out, and ONLY for testing, because I don't have any custom furnaces that sit on their sides. However, I temporarily changed my scroll table to sit sideways, like a furnace, and it worked with the base edit.
I then gave an idea of how to go about getting the correct result without editing base classes, but it is only a start, NOT a complete answer as I said. And the idea is exactly what you said, extend RenderBlocks and override the method.
Apparently my post to which you are replying is too confusing, so I will edit it for clarity.
Sorry, I just realized that my post may look rather rude... let me clarify that the emphasis and caps were added only to make it as clear as I could to avoid any further confusion. My apologies if it seemed otherwise.
The Recipe table is keyed by your LIST of ItemID ints,
and thats just not going to fly in 172.
I have been making adjustments as needed with the rest of your codes,
but is it reasonable to make a hash keyed to that LIST as upto 7 ITEMSTACKS rather than ints?
I'm going to try an 'overloaded' variant of your Recipe methods
that just shove the array of itemstacks as a LIST<ITEMSTACK> to key off of.
If I do it right with the addInscribing() calls, passing a ItemStack[] list
maybe the lookup will be as simple as using the container passed array directly
But first, if you've never made even a single-input furnace or other TileEntity-based Container/Gui, please do that before trying to follow this tutorial. I'm not going to cover all aspects of the Gui, Block and other such things here as there are other tutorials on that.
Be sure to check out MrrGingerNinja's great tutorial on setting up a TileEntity-based inventory.
I also recommend checking out Microjunk's here. That is actually where I started this tutorial
You will learn how to make a furnace that can take recipes with any number of inputs to give a single output, and I have included notes in most of the appropriate places on how to go about adding multiple outputs.
Also, the recipes aren't required to have the same length, so you can have simple and complex smelting recipes all handled by one furnace.
On to the tutorial.
Step 1: Make a Container Class
Most furnace-like containers will only ever need the first slot I make here; the other two are particular to my special case.
This is where much of the magic happens.
We'll use HashMaps to store an array of item metadata values to return an ItemStack result. If the items in your recipes will have different Item IDs, then add those to the hashmap as well. I only needed metadata values because all my recipes only involve a single rune item with many subtypes.
HashMaps are our friend
Be sure to check out my other tutorials - they are all on github here or follow the forum links below:
- Making an EventHandler and Event type explanations: http://www.minecraft...e-explanations/
- Custom Inventories in Items and Players: http://www.minecraft...s-an-inventory/
- Rendering a custom item texture: http://www.minecraft...m-item-texture/
- How to properly override shift-clicking: http://www.minecraft...shift-clicking/
- Enchanted Book Crafting Recipes: http://www.minecraft...-tag-compounds/
- Using potions in crafting recipes: http://www.minecraft...afting-recipes/
- See Mazetar's List for the best Minecraft tutorials from around the web!
Lastly, don't forget to up the green arrow on any posts that help you!
Agreed, very nice, well written tutorial. Between this and the other tutorials we all have here, there should be no reason one could not make a furnace in any fashion.
Find out how I generate....coolAlias...world structure generation and rotation tool...
Thanks to you for reminding me I can return ItemStack arrays from a HashMap! Couldn't believe I missed that!
<3 HashMaps <3
Are you using the BLANK_SCROLL slot as your fuel slot in this example? I am trying to go through your code and make sure I understand it all so I can make my own. I won't need your discharge slots or the recipe slot, but I will need the fuel slot, which I am assuming is the blank scrolls in your mod?
EDIT: I sent you a PM with better information. If you could read it over when you have time, I would greatly appreciate it.
yes, the blank scrollthe INPUT slots are my 'fuel'EDIT: Actually, sorry, the INPUT slots are what I use to set the 'fuel' timer. I'll try to explain it more clearly in the code, as this was originally more of an illustration of concept than a full on tutorial.
Thanks a ton for that! I was able to complete my (test) triple input furnace and it worked perfectly after you cleared those things up for me. Now to work on making a dual output
btw, do you or Maze happen to have any links to learn more about hashmaps? My ultimate goal is to be able to create dual output crafting tables but I am not comfortable enough yet with hashmaps to write out an entirely different crafting manager for what it would require.
Hm, I haven't seen any other than basic information like this. However, if you look at my recipe code, that's basically exactly what you'd do for the crafting table as well.
Use HashMap<List<Integer>, ItemStack[]>
The list is the hashmap 'key'. It contains all the item IDs and metadata for the recipe, and returns the ItemStack[] crafting results. I'm not sure how you'd go about shaped recipes other than to include (0,0) for any empty slots as part of the key signature, which would make all of your keys 18 entries (at 2 integers per item) long. Pretty big key
Yeah that would be a bit much. Luckily, for the type of crafting table I want to make though, it would only require shapeless recipes.
I actually am having this issue currently. I read that thread but didnt see a fix posted. Did you ever figure that out?
It is indeed. Im not sure what would determine the texture positioning of the block icon itself. I have been trying to work this out for a couple days now.
You will need to make a new class....you can call it furnace render or what ever you need...mine is called RenderHighSpeed. This is a rendering issue imposed by minecraft because of the new way they are rendering blocks.....
Notice at the bottom the new render id...it is necessary "2105" is what I am using, I have found that they tend to not work if they are lower than 1500. Then you will need to register it in the mod class like this....
This goes in the load method in the mod class....Have fun. It should not face correctly in the player inventory..
Find out how I generate....coolAlias...world structure generation and rotation tool...
We think this works, but this isn't actually called for rendering the block as an item (i.e. in the inventory) because at that time the block doesn't have any metadata, so par2 is always 0.
In there you can see where it sets the side to render for only these blocks. The easiest fix is to just add your block to this list. Worked for me
If you're being a good programmer and not editing base classes, you'll have more work cut out for you. Here's a little class I made that should work:
Then you'd need to tell your block not to render as a normal block and register this custom render to it somehow (sorry I don't know how to do that part, only done that for tile entity special render before)
Anyway, I only took the easy road as a test and it worked. Like I said, my furnaces use the top
My block shares functionality with furnaces, but it is a table for scribing scrolls so I have the active/idle texture rendering on the top instead of on the side. Therefore, it works by default, as I don't care about the side rendered in the inventory.
However, for testing I set it up just like a furnace so that the active/idle texture is on the side, at which point it was backwards.
I then edited the base class RenderBlocks to include my block in the line with furnaceIdle etc so that the side rendered would be set to '3'. This achieved the desired result of rendering the item block with the correct face forward, but with the downside of having to edit a base class.
See my suggestion on extending RenderBlocks for an idea on how to get around base edits.
Original post:
Looks like a crafting table, because it's not a furnace, it's where I inscribe scrolls. Anyway, I tested it out with the icon in front instead of on top and it worked fine, just like a furnace. The front side rendered in the inventory, instead of getting the back.
Please read my earlier post carefully. I explicitly state that I DID edit base classes to test it out, and ONLY for testing, because I don't have any custom furnaces that sit on their sides. However, I temporarily changed my scroll table to sit sideways, like a furnace, and it worked with the base edit.
I then gave an idea of how to go about getting the correct result without editing base classes, but it is only a start, NOT a complete answer as I said. And the idea is exactly what you said, extend RenderBlocks and override the method.
Apparently my post to which you are replying is too confusing, so I will edit it for clarity.
The Recipe table is keyed by your LIST of ItemID ints,
and thats just not going to fly in 172.
I have been making adjustments as needed with the rest of your codes,
but is it reasonable to make a hash keyed to that LIST as upto 7 ITEMSTACKS rather than ints?
I'm going to try an 'overloaded' variant of your Recipe methods
that just shove the array of itemstacks as a LIST<ITEMSTACK> to key off of.
If I do it right with the addInscribing() calls, passing a ItemStack[] list
maybe the lookup will be as simple as using the container passed array directly