I've got a custom machine (a two-input, one output "machine") that I cobbled together out of a tutorial series, starting with this video:
I managed to get everything working (even better than the tutorial demonstrated), and I had the machine's recipes working on a list-based system, which was functional, but ugly. I wanted to make it a bit more elegant, so I copied over the vanilla furnace's recipe system and changed it around a bit so that it would accept ArrayLists instead of ItemStacks as the key for the HashMap that the recipes are stored in. It no longer works, and will crash the game when attempting to cook any valid recipes. I don't know if the Vanilla FurnaceRecipes class has anything special done to make it work properly OUTSIDE of that class, but I have a feeling that there's something like that.
I'd link you to GitHub for this, as these classes already have tons of code, but I don't actually have that set up yet.
Recipe "Handler" Class
package com.exo594.tutorial;
import com.exo594.tutorial.item.ModItems;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
public final class IngotMasherRecipes {
public static final IngotMasherRecipes mashingBase = new IngotMasherRecipes();
private static Map mashingList = new HashMap();
ArrayList<ItemStack> addingKeyList = new ArrayList<ItemStack>();
private static final Item[] ITEMS = new Item[]{Items.bread, ModItems.tutorialItem};
//ArrayList<ItemStack> addingValueList = new ArrayList<ItemStack>();
public static IngotMasherRecipes mashing() {
return mashingBase;
}
private IngotMasherRecipes() {
this.addMashingRecipeWithTwoItems(ModItems.tutorialItem, Items.bread, new ItemStack(ModItems.tutoriumLoaf));
this.addMashingRecipeWithTwoItems(Items.iron_ingot, ModItems.tutorialItem, new ItemStack(ModItems.craftingPendulum));
this.addMashingRecipeWithOneBlock(Blocks.glowstone, Items.diamond, new ItemStack(ModItems.tutoriumGrenade, 1));
}
//used to tell transferItemInStack to play nice with items, apparantly not quite working properly
public static Boolean itemCanBeMashed(ItemStack itemstack) {
if (Arrays.asList(ITEMS).contains(itemstack.getItem())) {
return true;
} else {
return false;
}
}
public void addMashingRecipeWithOneBlock(Block blockIn, Item itemIn, ItemStack itemOut) {
this.addMashingRecipeWithTwoItems(Item.getItemFromBlock(blockIn), itemIn, itemOut);
}
public void addMashingRecipeWithTwoBlocks(Block blockOneIn, Block blockTwoIn, ItemStack itemOut) {
this.addMashingRecipeWithTwoItems(Item.getItemFromBlock(blockOneIn), Item.getItemFromBlock(blockTwoIn), itemOut);
}
public void addMashingRecipeWithTwoItems(Item itemOneIn, Item itemTwoIn, ItemStack itemOut) {
this.addRecipeFinal(new ItemStack(itemOneIn, 1, 0), new ItemStack(itemTwoIn, 1, 0), itemOut);
}
public void addRecipeFinal(ItemStack itemOneIn, ItemStack itemTwoIn, ItemStack itemOut) {
addingKeyList.add(itemOneIn);
addingKeyList.add(itemTwoIn);
//addingValueList.add(itemOut);
IngotMasherRecipes.mashingList.put(addingKeyList, itemOut);
addingKeyList.clear();
//addingValueList.clear();
}
public static ItemStack getMashingResult(ArrayList<ItemStack> list) //restructure to take ArrayLists, not ItemStacks
{
Iterator iterator = mashingList.entrySet().iterator();
Map.Entry entry;
do
{
if (!iterator.hasNext())
{
return null;
}
entry = (Map.Entry)iterator.next();
}
while (!func_151397_a(list, (ArrayList<ItemStack>)entry.getKey()));
return (ItemStack)entry.getValue();
}
private static boolean func_151397_a(ArrayList<ItemStack> list, ArrayList<ItemStack> keyBeingChecked)
{
return (keyBeingChecked.get(0) == list.get(0) && keyBeingChecked.get(0) == list.get(0)) || (keyBeingChecked.get(1) == list.get(0) && keyBeingChecked.get(0) == list.get(1));
}
public Map getMashingList()
{
return this.mashingList;
}
/* //list-based methods for determining recipe outputs, read off of now-deleted lists.
public static ItemStack getMashingResult(Item item, Item item2) {
return getOutput(item, item2);
}
public static ItemStack getOutput(Item item, Item item2) {
for (int i = 0; i < recipeList.length; ++i) {
int j = 0;
if (item == recipeList[i][j] && item2 == recipeList[i][j + 1] || item == recipeList[i][j+1] && item2 == recipeList[i][j]) {
return new ItemStack(recipeList[i][j + 2], amountList[i]);
} else {
return null;
}
}
return null;
}*/
}
TileEntity for this machine
package com.exo594.tutorial.tileentities;
import com.exo594.tutorial.IngotMasherRecipes;
import com.exo594.tutorial.block.IngotMasher;
import java.util.ArrayList;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.inventory.ISidedInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
public class TileEntityIngotMasher extends TileEntity implements ISidedInventory {
ArrayList<ItemStack> mashingResultsList = new ArrayList<ItemStack>();
public ItemStack slots[];
public int dualPower;
public int dualCookTime;
public static final int maxPower = 10000;
public static final int mashingSpeed = 100;
private static final int[] slots_top = new int[]{0, 1};
private static final int[] slots_bottom = new int[]{3};
private static final int[] slots_side = new int[]{2};
private String customName;
public ItemStack result;
public TileEntityIngotMasher() {
slots = new ItemStack[4];
}
@Override
public int getSizeInventory() {
return slots.length;
}
@Override
public ItemStack getStackInSlot(int i) {
return slots[i];
}
@Override
public ItemStack getStackInSlotOnClosing(int i) {
if (slots[i] != null) {
ItemStack itemstack = slots[i];
slots[i] = null;
return itemstack;
} else {
return null;
}
}
@Override
public void setInventorySlotContents(int i, ItemStack itemstack) {
slots[i] = itemstack;
if (itemstack != null && itemstack.stackSize > getInventoryStackLimit()) {
itemstack.stackSize = getInventoryStackLimit();
}
}
@Override
public int[] getAccessibleSlotsFromSide(int i) {
return i == 0 ? slots_bottom : (i == 1 ? slots_top : slots_side);
}
@Override
public int getInventoryStackLimit() {
return 64;
}
@Override
public boolean isUseableByPlayer(EntityPlayer player) {
if (worldObj.getTileEntity(xCoord, yCoord, zCoord) != this) {
return false;
} else {
return player.getDistanceSq((double) xCoord + 0.5D, (double) yCoord + 0.5D, (double) zCoord + 0.5D) <= 64;
}
}
@Override
public void openInventory() {
}
@Override
public void closeInventory() {
}
@Override
public boolean isItemValidForSlot(int i, ItemStack itemstack) {
return i == 3 ? false : (i == 2 ? hasItemPower(itemstack) : true);
}
public static boolean hasItemPower(ItemStack itemstack) {
return getItemPower(itemstack) > 0;
}
private static int getItemPower(ItemStack itemstack) {
if (itemstack == null) {
return 0;
} else {
Item item = itemstack.getItem();
if (item == Items.apple) {
return 1000;
} else {
return 0;
}
}
}
@Override
public ItemStack decrStackSize(int i, int j) {
if (slots[i] != null) {
if (slots[i].stackSize <= j) {
ItemStack itemstack = slots[i];
slots[i] = null;
return itemstack;
}
ItemStack itemstack1 = slots[i].splitStack(j);
if (slots[i].stackSize == 0) {
slots[i] = null;
}
return itemstack1;
} else {
return null;
}
}
@Override
public void readFromNBT(NBTTagCompound nbt) {
super.readFromNBT(nbt);
NBTTagList list = nbt.getTagList("Items", 10);
slots = new ItemStack[getSizeInventory()];
for (int i = 0; i < list.tagCount(); i++) {
NBTTagCompound nbt1 = (NBTTagCompound) list.getCompoundTagAt(i);
byte b0 = nbt1.getByte("Slot");
if (b0 >= 0 && b0 < slots.length) {
slots[b0] = ItemStack.loadItemStackFromNBT(nbt1);
}
}
dualPower = nbt.getShort("PowerTime");
dualCookTime = nbt.getShort("CookTime");
}
@Override
public void writeToNBT(NBTTagCompound nbt) {
super.writeToNBT(nbt);
nbt.setShort("PowerTime", (short) dualPower);
nbt.setShort("CookTime", (short) dualCookTime);
NBTTagList list = new NBTTagList();
for (int i = 0; i < slots.length; i++) {
if (slots[i] != null) {
NBTTagCompound nbt1 = new NBTTagCompound();
nbt1.setByte("Slot", (byte) i);
slots[i].writeToNBT(nbt1);
list.appendTag(nbt1);
}
}
nbt.setTag("Items", list);
}
@Override
public String getInventoryName() {
return "container.ingotMasher";
}
@Override
public boolean canInsertItem(int var1, ItemStack itemstack, int var3) {
return this.isItemValidForSlot(var1, itemstack);
}
@Override
public boolean canExtractItem(int i, ItemStack itemstack, int j) {
return j != 0 || i != 1 || itemstack.getItem() == Items.bucket;
}
@Override
public boolean hasCustomInventoryName() {
return this.customName != null && this.customName.length() > 0;
}
public int getMasherProgressScaled(int i) {
return (dualCookTime * i) / this.mashingSpeed;
}
public int getPowerRemainingScaled(int i) {
return (dualPower * i) / maxPower;
}
private boolean canMash() {
if (slots[0] == null || slots[1] == null) {
return false;
}
if (slots[0] != null && slots[1] != null) {
mashingResultsList.add(new ItemStack(this.slots[0].getItem()));
mashingResultsList.add(new ItemStack(this.slots[1].getItem()));
result = IngotMasherRecipes.getMashingResult(mashingResultsList);
mashingResultsList.clear();
}else{
result = null;
}
if (result == null) {
return false;
}
if (slots[3] == null) {
return true;
}
if (!slots[3].isItemEqual(result)) {
return false;
}
if (slots[3].stackSize < getInventoryStackLimit() && slots[3].stackSize < slots[3].getMaxStackSize()) {
return true;
} else {
return slots[3].stackSize < result.getMaxStackSize();
}
}
private void mashItem() {
if (canMash()) {
if (slots[0] != null && slots[1] != null) {
mashingResultsList.add(new ItemStack(this.slots[0].getItem()));
mashingResultsList.add(new ItemStack(this.slots[1].getItem()));
ItemStack itemstack = IngotMasherRecipes.getMashingResult(mashingResultsList);
mashingResultsList.clear();
if (slots[3] == null) {
slots[3] = itemstack.copy();
} else if (slots[3].isItemEqual(itemstack)) {
slots[3].stackSize += itemstack.stackSize;
}
for (int i = 0; i < 2; i++) {
if (slots[i].stackSize <= 0) {
slots[i] = new ItemStack(slots[i].getItem().setFull3D());
} else {
slots[i].stackSize--;
}
if (slots[i].stackSize <= 0) {
slots[i] = null;
}
}
}
}
}
public boolean hasPower() {
return dualPower > 0;
}
public boolean isMashing() {
return this.dualCookTime > 0;
}
@Override
public void updateEntity() {
boolean flag = this.hasPower();
boolean flag1 = false;
if (hasPower() && this.isMashing()) {
this.dualPower--;
}
if (!worldObj.isRemote) {
if (this.hasItemPower(this.slots[2]) && this.dualPower < (this.maxPower - this.getItemPower(this.slots[2]))) {
this.dualPower += getItemPower(this.slots[2]);
if (this.slots[2] != null) {
flag1 = true;
this.slots[2].stackSize--;
if (this.slots[2].stackSize == 0) {
this.slots[2] = this.slots[2].getItem().getContainerItem(this.slots[2]);
}
}
}
if (hasPower() && canMash()) {
dualCookTime++;
if (this.dualCookTime == this.mashingSpeed) {
this.dualCookTime = 0;
this.mashItem();
flag1 = true;
}
} else {
dualCookTime = 0;
}
if (flag != this.isMashing()) {
flag1 = true;
IngotMasher.updateBlockState(this.dualCookTime > 0, this.worldObj, this.xCoord, this.yCoord, this.zCoord);
}
}
if (flag1) {
this.markDirty();
}
}
}
Container Class
package com.exo594.tutorial.containers;
import com.exo594.tutorial.IngotMasherRecipes;
import com.exo594.tutorial.slot.SlotIngotMasher;
import com.exo594.tutorial.tileentities.TileEntityIngotMasher;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.inventory.ICrafting;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
public class ContainerIngotMasher extends Container {
private TileEntityIngotMasher masher;
private int dualCookTime;
private int dualPower;
private int lastItemBurnTime;
public static final int INPUT_1 = 0, INPUT_2 = 1, FUEL = 2, OUTPUT = 3;
public ContainerIngotMasher(InventoryPlayer invPlayer, TileEntityIngotMasher teIngotMasher) {
dualCookTime = 0;
dualPower = 0;
lastItemBurnTime = 0;
masher = teIngotMasher;
this.addSlotToContainer(new Slot(teIngotMasher, INPUT_1, 45, 17));
this.addSlotToContainer(new Slot(teIngotMasher, INPUT_2, 45, 49));
this.addSlotToContainer(new Slot(teIngotMasher, FUEL, 8, 56));
this.addSlotToContainer(new SlotIngotMasher(invPlayer.player, teIngotMasher, OUTPUT, 113, 33));
//Inventory
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 9; j++) {
this.addSlotToContainer(new Slot(invPlayer, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
}
}
//ActionBar
for (int i = 0; i < 9; i++) {
this.addSlotToContainer(new Slot(invPlayer, i, 8 + i * 18, 142));
}
}
@Override
public void addCraftingToCrafters(ICrafting crafting) {
super.addCraftingToCrafters(crafting);
crafting.sendProgressBarUpdate(this, 0, this.masher.dualCookTime);
crafting.sendProgressBarUpdate(this, 1, this.masher.dualPower);
}
@Override
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 itemstack is in Output stack
if (par2 == OUTPUT) {
// try to place in player inventory / action bar; add 36+1 because mergeItemStack uses < index,
// so the last slot in the inventory won't get checked if you don't add 1
if (!this.mergeItemStack(itemstack1, OUTPUT + 1, OUTPUT + 36 + 1, true)) {
return null;
}
slot.onSlotChange(itemstack1, itemstack);
} // itemstack is in player inventory, try to place in appropriate furnace slot
else if (par2 != FUEL && par2 != INPUT_1 && par2 != INPUT_2) {
// if it can be smelted, place in the input slots
if (IngotMasherRecipes.itemCanBeMashed(itemstack1)) { //DOES NOT LIKE SOME PART OF THIS LINE
// try to place in either Input slot; add 1 to final input slot because mergeItemStack uses < index
if (!this.mergeItemStack(itemstack1, INPUT_1, INPUT_2 + 1, false)) {
return null;
}
} // if it's an energy source, place in Fuel slot
else if (TileEntityIngotMasher.hasItemPower(itemstack1)) {
if (!this.mergeItemStack(itemstack1, FUEL, FUEL + 1, false)) {
return null;
}
} // item in player's inventory, but not in action bar
else if (par2 >= OUTPUT + 1 && par2 < OUTPUT + 28) {
// place in action bar
if (!this.mergeItemStack(itemstack1, OUTPUT + 28, OUTPUT + 37, false)) {
return null;
}
} // item in action bar - place in player inventory
else if (par2 >= OUTPUT + 28 && par2 < OUTPUT + 37 && !this.mergeItemStack(itemstack1, OUTPUT + 1, OUTPUT + 28, false)) {
return null;
}
} // In one of the infuser slots; try to place in player inventory / action bar
else if (!this.mergeItemStack(itemstack1, OUTPUT + 1, OUTPUT + 37, 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;
}
@Override
public boolean canInteractWith(EntityPlayer player) {
return masher.isUseableByPlayer(player);
}
@Override
public void detectAndSendChanges() {
super.detectAndSendChanges();
for (int i = 0; i < this.crafters.size(); i++) {
ICrafting icrafting = (ICrafting) this.crafters.get(i);
if (this.dualCookTime != this.masher.dualCookTime) {
icrafting.sendProgressBarUpdate(this, 0, this.masher.dualCookTime);
}
if (this.dualPower != this.masher.dualPower) {
icrafting.sendProgressBarUpdate(this, 1, this.masher.dualPower);
}
}
this.dualCookTime = this.masher.dualCookTime;
this.dualPower = this.masher.dualPower;
}
@SideOnly(Side.CLIENT)
@Override
public void updateProgressBar(int slot, int newValue) {
if (slot == 0) {
this.masher.dualCookTime = newValue;
}
if (slot == 1) {
this.masher.dualPower = newValue;
}
}
}
Block Class
package com.exo594.tutorial.block;
import com.exo594.tutorial.Main;
import com.exo594.tutorial.tileentities.TileEntityIngotMasher;
import cpw.mods.fml.common.network.internal.FMLNetworkHandler;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.Random;
import net.minecraft.block.Block;
import net.minecraft.block.BlockContainer;
import net.minecraft.block.material.Material;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.IIcon;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
public class IngotMasher extends BlockContainer {
private Random rand;
private final boolean isActive;
private static boolean keepInventory = true;
@SideOnly(Side.CLIENT)
private IIcon iconFront;
public IngotMasher(String unlocalizedName, boolean blockState) {
super(Material.iron);
rand = new Random();
isActive = blockState;
this.setBlockName(unlocalizedName);
}
@SideOnly(Side.CLIENT)
@Override
public void registerBlockIcons(IIconRegister iconRegister) {
this.blockIcon = iconRegister.registerIcon(Main.MODID + ":" + ("tutoriumFurnaceOther"));
this.iconFront = iconRegister.registerIcon(Main.MODID + ":" + (this.isActive ? "IngotMasherFrontOn" : "IngotMasherFrontOff"));
}
@SideOnly(Side.CLIENT)
@Override
public IIcon getIcon(int side, int metadata) {
return metadata == 0 && side == 3 ? this.iconFront : (side == metadata ? this.iconFront : this.blockIcon);
}
@Override
public void onBlockAdded(World world, int x, int y, int z) {
super.onBlockAdded(world, x, y, z);
this.setDefaultDirection(world, x, y, z);
}
private void setDefaultDirection(World world, int x, int y, int z) {
if (!world.isRemote) {
Block block1 = world.getBlock(x, y, z - 1);
Block block2 = world.getBlock(x, y, z + 1);
Block block3 = world.getBlock(x - 1, y, z);
Block block4 = world.getBlock(x + 1, y, z);
byte b0 = 3;
if (block1.func_149730_j() && !block2.func_149730_j()) {
b0 = 3;
}
if (block2.func_149730_j() && !block1.func_149730_j()) {
b0 = 2;
}
if (block3.func_149730_j() && !block4.func_149730_j()) {
b0 = 5;
}
if (block4.func_149730_j() && !block3.func_149730_j()) {
b0 = 4;
}
world.setBlockMetadataWithNotify(x, y, x, b0, 2);
}
}
@Override
public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase entityPlayer, ItemStack itemstack) {
int i = MathHelper.floor_double((double) (entityPlayer.rotationYaw * 4.0F / 360F) + 0.5D) & 3;
if (i == 0) {
world.setBlockMetadataWithNotify(x, y, z, 2, 2);
}
if (i == 1) {
world.setBlockMetadataWithNotify(x, y, z, 5, 2);
}
if (i == 2) {
world.setBlockMetadataWithNotify(x, y, z, 3, 2);
}
if (i == 3) {
world.setBlockMetadataWithNotify(x, y, z, 4, 2);
}
if (itemstack.hasDisplayName()) {
//((TileEntityIngotMasher)world.getTileEntity(x, y, z)).setCustomName(itemstack.getDisplayName());
}
}
@Override
public boolean onBlockActivated(World world, int x, int y, int z, EntityPlayer player, int side, float hitX, float hitY, float hitZ) {
if (world.isRemote) {
return true;
} else if (!player.isSneaking()) {
TileEntityIngotMasher entity = (TileEntityIngotMasher) world.getTileEntity(x, y, z);
if (entity != null) {
FMLNetworkHandler.openGui(player, Main.modInstance, Main.guiIDIngotMasher, world, x, y, z);
}
return true;
} else {
return false;
}
}
@Override
public TileEntity createNewTileEntity(World var1, int var2) {
return new TileEntityIngotMasher();
}
public static void updateBlockState(boolean isMashing, World world, int xCoord, int yCoord, int zCoord) {
int i = world.getBlockMetadata(xCoord, yCoord, zCoord);
TileEntity entity = world.getTileEntity(xCoord, yCoord, zCoord);
keepInventory = true;
if (isMashing) {
world.setBlock(xCoord, yCoord, zCoord, ModBlocks.blockIngotMasherActive);
} else {
world.setBlock(xCoord, yCoord, zCoord, ModBlocks.blockIngotMasherIdle);
}
keepInventory = false;
world.setBlockMetadataWithNotify(xCoord, yCoord, zCoord, i, 2);
if (entity != null) {
entity.validate();
world.setTileEntity(xCoord, yCoord, zCoord, entity);
}
}
@Override
public void breakBlock(World world, int x, int y, int z, Block oldblock, int oldMetadata) {
if (!keepInventory) {
TileEntityIngotMasher tileentity = (TileEntityIngotMasher) world.getTileEntity(x, y, z);
if (tileentity != null) {
for (int i = 0; i < tileentity.getSizeInventory(); i++) {
ItemStack itemstack = tileentity.getStackInSlot(i);
if (itemstack != null) {
float f = this.rand.nextFloat() * 0.8F + 0.1F;
float f1 = this.rand.nextFloat() * 0.8F + 0.1F;
float f2 = this.rand.nextFloat() * 0.8F + 0.1F;
while (itemstack.stackSize > 0) {
int j = this.rand.nextInt(21) + 10;
if (j > itemstack.stackSize) {
j = itemstack.stackSize;
}
itemstack.stackSize -= j;
EntityItem item = new EntityItem(world, (double) ((float) x + f), (double) ((float) y + f1), (double) ((float) z + f2), new ItemStack(itemstack.getItem(), j, itemstack.getItemDamage()));
if (itemstack.hasTagCompound()) {
item.getEntityItem().setTagCompound((NBTTagCompound) itemstack.getTagCompound().copy());
}
world.spawnEntityInWorld(item);
}
}
}
world.func_147453_f(x, y, z, oldblock);
}
}
super.breakBlock(world, x, y, z, oldblock, oldMetadata);
}
}
HerpaDerp, crashlog.