public class BackpacksContent {
	
	public static ItemBackpack BACKPACK;
	
	public BackpacksContent() {
		if (WearableBackpacks.CONFIG.backpack.enabled.get())
			BACKPACK = new ItemBackpack();
	}
	
	@SubscribeEvent
	public void onRegisterBlocks(RegistryEvent.Register<Block> event) {
		if (BACKPACK != null) {
			event.getRegistry().register(
				new BlockBackpack().setRegistryName(WearableBackpacks.MOD_ID, "backpack"));
			GameRegistry.registerTileEntity(TileEntityBackpack.class, new ResourceLocation(WearableBackpacks.MOD_ID, "backpack"));
		}
	}
	
	@SubscribeEvent
	public void onRegisterItems(RegistryEvent.Register<Item> event) {
		if (BACKPACK != null) {
			event.getRegistry().register(
				BACKPACK.setRegistryName(WearableBackpacks.MOD_ID, "backpack"));
			
			BackpackRegistry.registerEntity("minecraft:zombie",        RenderOptions.DEFAULT);
			BackpackRegistry.registerEntity("minecraft:skeleton",      RenderOptions.DEFAULT);
			BackpackRegistry.registerEntity("minecraft:zombie_pigman", RenderOptions.DEFAULT);
			
			String backpack  = BACKPACK.getRegistryName().toString();
			String idDefault = WearableBackpacks.MOD_ID + ":default";
			String idColored = WearableBackpacks.MOD_ID + ":colored";
			String lootTable = ItemBackpack.LOOT_TABLE.toString();
			
			BackpackRegistry.registerBackpack("minecraft:zombie",        idDefault, backpack,   800, lootTable, null);
			BackpackRegistry.registerBackpack("minecraft:zombie",        idColored, backpack,  8000, lootTable, ColorRange.DEFAULT);
			BackpackRegistry.registerBackpack("minecraft:skeleton",      idDefault, backpack,  1200, lootTable, null);
			BackpackRegistry.registerBackpack("minecraft:skeleton",      idColored, backpack, 12000, lootTable, ColorRange.DEFAULT);
			BackpackRegistry.registerBackpack("minecraft:zombie_pigman", idColored, backpack,  1000, lootTable, ColorRange.DEFAULT);
			
			// TODO: Register all loot tables mentioned in config file?
			LootTableList.register(ItemBackpack.LOOT_TABLE);
		}
		
		// BackpackRegistry.registerBackpackEntity(EntityEnderman.class, ENDER_BACKPACK, 1.0 / 80);
	}
	
	public void registerRecipes() {
		ForgeRegistries.RECIPES.register(new RecipeDyeableItem()
			.setRegistryName(WearableBackpacks.MOD_ID, "dyeable_item"));
	}
	
} @Mod(modid = WearableBackpacks.MOD_ID, name = WearableBackpacks.MOD_NAME,
     version = WearableBackpacks.VERSION, dependencies = "required-after:forge@[14.21.0.2375,)",
     guiFactory = "net.mcft.copy.backpacks.client.BackpacksGuiFactory")
public class WearableBackpacks {
	
	public static final String MOD_ID   = "wearablebackpacks";
	public static final String MOD_NAME = "Wearable Backpacks";
	public static final String VERSION  = "@VERSION@";
	
	@Instance
	public static WearableBackpacks INSTANCE;
	
	@SidedProxy(serverSide = "net.mcft.copy.backpacks.ProxyCommon",
	            clientSide = "net.mcft.copy.backpacks.ProxyClient")
	public static ProxyCommon PROXY;
	
	public static Logger LOG;
	public static BackpacksChannel CHANNEL;
	public static BackpacksConfig CONFIG;
	public static BackpacksContent CONTENT;
	
	@EventHandler
	public void preInit(FMLPreInitializationEvent event) {
		LOG = event.getModLog();
		CHANNEL = new BackpacksChannel();
		
		CONFIG = new BackpacksConfig(event.getSuggestedConfigurationFile());
		CONFIG.load();
		CONFIG.save();
		
		CONTENT = new BackpacksContent();
		CONTENT.registerRecipes();
		
		PROXY.preInit();
	}
	
	@EventHandler
	public void init(FMLInitializationEvent event) {
		CONFIG.init();
		PROXY.init();
	}
	
} public final class BackpackHelper {
	
	private BackpackHelper() {  }
	
	
	/** The maximum distance from which an equipped backpack can be opened. */
	public static double INTERACT_MAX_DISTANCE = 1.8;
	/** The maximum angle from which an equipped backpack can be opened. */
	public static double INTERACT_MAX_ANGLE = 110;
	
	/** Controlled by a WearableBackpacks config setting. Don't change this, please. */
	public static boolean equipAsChestArmor = true;
	
	public static Logger LOG = LogManager.getLogger("wearablebackpacks:api");
	
	
	/** Returns the entity's backpack capability, or null if the
	 *  entity either can't or currently doesn't have one equipped. */
	public static IBackpack getBackpack(Entity entity) {
		if (entity == null) return null;
		IBackpack backpack = entity.getCapability(IBackpack.CAPABILITY, null);
		return ((backpack != null) && !backpack.getStack().isEmpty()) ? backpack : null;
	}
	
	/** Returns the tile entity's backpack capability. */
	public static IBackpack getBackpack(TileEntity entity)
		{ return (entity != null) ? entity.getCapability(IBackpack.CAPABILITY, null) : null; }
	
	/** Returns the backpack type of an item stack, or null if it isn't a backpack. */
	public static IBackpackType getBackpackType(ItemStack stack)
		{ return !stack.isEmpty() ? getBackpackType(stack.getItem()) : null; }
	/** Returns the backpack type of an item, or null if it doesn't implement IBackpackType. */
	public static IBackpackType getBackpackType(Item item)
		{ return (item instanceof IBackpackType) ? (IBackpackType)item : null; }
	
	/** Returns if the entity can equip a backpack right now.
	 *  Requires the entity to be able to wear backpacks, not currently have a backpack equipped, and
	 *  if {@link equipAsChestArmor} is true and the entity is a player, an empty chest armor slot. */
	public static boolean canEquipBackpack(EntityLivingBase entity) {
		return (entity.getCapability(IBackpack.CAPABILITY, null) != null) // Has backpack capability.
			&& (getBackpack(entity) == null)                              // Doesn't currently have backpack equipped.
			&& !(equipAsChestArmor && (entity instanceof EntityPlayer)    // Isn't wearing a chestplate while equipAsChestArmor is on.
				&& !entity.getItemStackFromSlot(EntityEquipmentSlot.CHEST).isEmpty());
		// FIXME: How does this work with non-player entities? Do / should they always wear backpacks as armor or what?
	}
	
	/** Sets the entity's equipped backpack and data. */
	public static void setEquippedBackpack(EntityLivingBase entity, ItemStack stack,
	                                       IBackpackData backpackData) {
		IBackpackType backpackType = getBackpackType(stack);
		if (!stack.isEmpty() && (backpackType == null))
			throw new IllegalArgumentException("Backpack item isn't an IBackpackType.");
		
		IBackpack backpack = entity.getCapability(IBackpack.CAPABILITY, null);
		backpack.setStack(stack);
		backpack.setData(backpackData);
	}
	
	/** Checks if a player can open an entity's equipped backpack.
	 *  Returns if the player stands close enough to and behind the carrier.
	 *  Always returns true if player and carrier are the same entity. */
	public static boolean canInteractWithEquippedBackpack(EntityPlayer player, EntityLivingBase carrier) {
		IBackpack backpack = getBackpack(carrier);
		if ((backpack == null) || !player.isEntityAlive() || !carrier.isEntityAlive()) return false;
		if (player == carrier) return true;
		
		double distance = player.getDistance(carrier);
		// Calculate angle between player and carrier.
		double angle = Math.toDegrees(Math.atan2(carrier.posZ - player.posZ, carrier.posX - player.posX));
		// Calculate difference between angle and the direction the carrier entity is looking.
		angle = ((angle - carrier.renderYawOffset - 90) % 360 + 540) % 360 - 180;
		return ((distance <= INTERACT_MAX_DISTANCE) && (Math.abs(angle) < INTERACT_MAX_ANGLE / 2));
	}
	
	/** Equips a backpack from a tile entity, returns if successful. */
	public static boolean equipBackpack(EntityLivingBase entity, TileEntity tileEntity) {
		if ((tileEntity == null) || !canEquipBackpack(entity)) return false;
		IBackpack backpack = tileEntity.getCapability(IBackpack.CAPABILITY, null);
		if (backpack == null) return false;
		ItemStack stack = backpack.getStack();
		IBackpackType type = getBackpackType(stack);
		if (type == null) return false;
		type.onEquip(entity, tileEntity, backpack);
		if (!entity.world.isRemote) {
			BackpackHelper.setEquippedBackpack(entity, stack, backpack.getData());
			backpack.setStack(ItemStack.EMPTY);
			backpack.setData(null);
		}
		return true;
	}
	
	/** Attempts to place down a backpack, unequipping it
	 *  if the specified entity is currently wearing it. */
	public static boolean placeBackpack(World world, BlockPos pos, ItemStack stack,
	                                    EntityLivingBase entity, boolean ignoreEntities) {
		
		EntityPlayer player = ((entity instanceof EntityPlayer) ? (EntityPlayer)entity : null);
		if ((player != null) && !player.canPlayerEdit(pos, EnumFacing.UP, stack))
			return false;
		// TODO: Should permission things be handled outside the API method?
		// TODO: For mobs, use mob griefing gamerule?
		
		Item item = stack.getItem();
		// Would use this instead, but gotta avoid depending on the rest of WearableBackpacks.
		//Block block = MiscUtils.getBlockFromItem(item);
		Block block = Block.REGISTRY.getObject(item.getRegistryName());
		if (ignoreEntities ? !block.canPlaceBlockAt(world, pos)
		                   : !world.mayPlace(block, pos, false, EnumFacing.UP, null))
			return false;
		
		// Actually go ahead and try to set the block in the world.
		IBlockState state = block.getStateForPlacement(
			world, pos, EnumFacing.UP, 0.5F, 0.5F, 0.5F,
			item.getMetadata(stack.getMetadata()), player, EnumHand.MAIN_HAND);
		if (!world.setBlockState(pos, state, 3) ||
		    (world.getBlockState(pos).getBlock() != block)) return false;
		block.onBlockPlacedBy(world, pos, state, entity, stack);
		
		double x = pos.getX() + 0.5;
		double y = pos.getY() + 0.5;
		double z = pos.getZ() + 0.5;
		SoundType sound = block.getSoundType(state, world, pos, entity);
		world.playSound(x, y, z, sound.getPlaceSound(), SoundCategory.BLOCKS,
		                (sound.getVolume() + 1.0F) / 2.0F, sound.getPitch() * 0.8F, false);

		// This is used to transfer the BlockEntityTag from Ctrl+pick-blocking to the world.
		boolean alreadyConfigured = ItemBlock.setTileEntityNBT(world, player, pos, stack);

		TileEntity tileEntity = world.getTileEntity(pos);
		if (tileEntity == null) return true;
		IBackpack placedBackpack = BackpackHelper.getBackpack(tileEntity);
		if (placedBackpack == null) return true;
		
		IBackpack carrierBackpack = BackpackHelper.getBackpack(entity);
		boolean isEquipped = ((carrierBackpack != null) && (carrierBackpack.getStack() == stack));
		
		ItemStack stackOrig = stack;
		// Create a copy of the stack with stackSize set to 1 and transfer it.
		stack = stack.copy();
		stack.setCount(1);
		if (stack.hasTagCompound() && stack.getTagCompound().hasKey("BlockEntityTag")) {
			stack.getTagCompound().removeTag("BlockEntityTag");
		}
		placedBackpack.setStack(stack);
		
		if (!alreadyConfigured) {
			// If the carrier had the backpack equipped, transfer data and unequip.
			if (isEquipped) {
				
				IBackpackType type = carrierBackpack.getType();
				IBackpackData data = carrierBackpack.getData();
				if ((data == null) && !world.isRemote) {
					LOG.error("Backpack data was null when placing down equipped backpack");
					data = type.createBackpackData(stack);
				}
				
				placedBackpack.setData(data);
				
				if (!world.isRemote)
					BackpackHelper.setEquippedBackpack(entity, ItemStack.EMPTY, null);
				
				type.onUnequip(entity, tileEntity, placedBackpack);
				
			// Otherwise create a fresh backpack data on the server.
			} else if (!world.isRemote) placedBackpack.setData(
				placedBackpack.getType().createBackpackData(stack));
		}
		
		// We only shrink the original stack here instead of earlier
		// as its information is still needed for other checks, and
		// shrinking it from 1 to 0 would effectively empty the stack.
		stackOrig.shrink(1);
		return true;
		
	}
	
	/** Updates the lid ticks for some backpack properties.
	 *  Plays a sound when the backpack is being opened or closed. */
	@SideOnly(Side.CLIENT)
	public static void updateLidTicks(IBackpack properties, double x, double y, double z) {
		World world = Minecraft.getMinecraft().world;
		boolean usedByPlayer = (properties.getPlayersUsing() > 0);
		int prevLidTicks = properties.getLidTicks();
		IBackpackType backpackType = getBackpackType(properties.getStack());
		int maxLidTicks = ((backpackType != null) ? backpackType.getLidMaxTicks() : 0);
		
		int lidTicks = Math.max(-1, Math.min(maxLidTicks, prevLidTicks + (usedByPlayer ? +1 : -1)));
		properties.setLidTicks(lidTicks);
		
		// Play sound when backpack is being opened.
		if ((lidTicks > 0) && (prevLidTicks <= 0))
			world.playSound(x, y, z, SoundEvents.BLOCK_SNOW_BREAK, SoundCategory.PLAYERS, 1.0F, 0.6F, false);
		// Play sound when backpack is being closed.
		if ((lidTicks < (maxLidTicks / 5)) && (prevLidTicks >= (maxLidTicks / 5)))
			world.playSound(x, y, z, SoundEvents.BLOCK_SNOW_BREAK, SoundCategory.PLAYERS, 0.8F, 0.4F, false);
	}
	
} public final class BackpackRegistry {
	
	private BackpackRegistry() {  }
	
	/** Registers an entity as possible backpack carrier, meaning
	 *  they'll get constructed with an IBackpack capability.
	 *  Must be called in pre-initialization phase (or before). */
	public static void registerEntity(String entityID, RenderOptions renderOptions) {
		if (entityID == null) throw new NullPointerException("entityID must not be null");
		if (getDefaultEntityEntry(entityID) != null)
			throw new IllegalArgumentException("entityID '" + entityID + "' has already been registered");
		if (Loader.instance().getLoaderState().compareTo(LoaderState.PREINITIALIZATION) > 0)
			throw new IllegalStateException("Must be called during (or before) pre-initialization phase.");
		_defaultEntities.add(new BackpackEntityEntry(entityID, renderOptions, new ArrayList<>(), true));
	}
	
	/** Registers a backpack to randomly spawn on the specified entity.
	 *  Must be called after registerEntity, in pre-initialization phase (or before).
	 * 
	 * @param entityID   The entity to register to spawn with this backpack.
	 * 
	 * @param entryID    String uniquely identifying this entry for this entity.
	 *                   For example "wearblebackpacks:default".
	 * @param backpack   Backpack item ID to spawn on the entity.
	 *                   For example "wearablebackpacks:backpack".
	 * @param chance     Chance in 1 out of X. For example 100 = 1% and 1000 = 0.1%.
	 * @param lootTable  Loot table for the backpack when spawned on this mob (if any).
	 * @param colorRange A range of colors to spawn the backpack with, or null if default. 
	 **/
	public static void registerBackpack(String entityID,
	                                    String entryID, String backpack, int chance,
	                                    String lootTable, ColorRange colorRange) {
		if (entityID == null) throw new NullPointerException("entityID must not be null");
		if (entryID == null) throw new NullPointerException("entryID must not be null");
		BackpackEntityEntry entityEntry = getDefaultEntityEntry(entityID);
		if (entityEntry == null) new IllegalStateException("entityID '" + entityID + "' has not been registered yet");
		if (entityEntry._backpackEntries.stream().anyMatch(e -> e.id.equals(entryID)))
			throw new IllegalArgumentException("entryID '" + entryID + "' has already been used for entityID '" + entityID + "'");
		if (Loader.instance().getLoaderState().compareTo(LoaderState.PREINITIALIZATION) > 0)
			throw new IllegalStateException("Must be called during (or before) pre-initialization phase.");
		entityEntry._backpackEntries.add(new BackpackEntry(entryID, backpack, chance, lootTable, colorRange, true));
	}
	
	/** Returns if the specified entity should be able to wear backpacks.
	 *  This affects whether the entity will be constructed with an IBackpack capability. */
	public static boolean canEntityWearBackpacks(Entity entity) {
		return (entity != null) && EntityLivingBase.class.isAssignableFrom(entity.getClass())
			&& (getEntityEntry(entity.getClass().asSubclass(EntityLivingBase.class)) != null);
	}
	
	
	// Internal / semi-internal stuff
	
	private static final List<BackpackEntityEntry> _defaultEntities = new ArrayList<>();
	private static final List<BackpackEntityEntry> _entities = new ArrayList<>();
	private static final Map<String, Optional<Class<? extends EntityLivingBase>>> _entityClassLookupCache = new HashMap<>();
	private static Map<Class<? extends EntityLivingBase>, BackpackEntityEntry> _entityEntryLookupCache = null;
	
	public static List<BackpackEntityEntry> getEntityEntries()
		{ return Collections.unmodifiableList(_entities); }
	public static List<BackpackEntityEntry> getDefaultEntityEntries()
		{ return Collections.unmodifiableList(_defaultEntities); }
	
	public static BackpackEntityEntry getEntityEntry(String entityID)
		{ return _entities.stream().filter(e -> e.entityID.equals(entityID)).findAny().orElse(null); }
	public static BackpackEntityEntry getDefaultEntityEntry(String entityID)
		{ return _defaultEntities.stream().filter(e -> e.entityID.equals(entityID)).findAny().orElse(null); }
	
	public static BackpackEntityEntry getEntityEntry(Class<? extends EntityLivingBase> entityClass) {
		if (EntityPlayer.class.isAssignableFrom(entityClass))
			return BackpackEntityEntry.PLAYER;
		if (_entityEntryLookupCache == null)
			_entityEntryLookupCache = ForgeRegistries.ENTITIES.getEntries().stream()
				.map(e -> new AbstractMap.SimpleEntry<>(e.getValue().getEntityClass(), getEntityEntry(e.getKey().toString())))
				.filter(e -> EntityLivingBase.class.isAssignableFrom(e.getKey()) && (e.getValue() != null))
				.collect(Collectors.toMap(e -> e.getKey().asSubclass(EntityLivingBase.class), Map.Entry::getValue));
		return _entityEntryLookupCache.get(entityClass);
	}
	
	public static void updateEntityEntries(List<BackpackEntityEntry> value) {
		mergeEntityEntriesWithDefault(_entities, value);
		_entityEntryLookupCache = null;
	}
	public static List<BackpackEntityEntry> mergeEntityEntriesWithDefault(List<BackpackEntityEntry> value)
		{ return mergeEntityEntriesWithDefault(new ArrayList<>(), value); }
	public static List<BackpackEntityEntry> mergeEntityEntriesWithDefault(
			List<BackpackEntityEntry> dest, List<BackpackEntityEntry> value) {
		dest.clear();
		_defaultEntities.stream().map(BackpackEntityEntry::new).forEach(dest::add);
		
		for (BackpackEntityEntry entityEntry : value) {
			BackpackEntityEntry defaultEntityEntry = dest.stream()
				.filter(e -> e.entityID.equals(entityEntry.entityID))
				.findAny().orElse(null);
			if (defaultEntityEntry != null) {
				for (BackpackEntry backpackEntry : entityEntry._backpackEntries) {
					int index = IntStream.range(0, defaultEntityEntry._backpackEntries.size())
						.filter(i -> defaultEntityEntry._backpackEntries.get(i).id.equals(backpackEntry.id))
						.findFirst().orElse(-1);
					if (index >= 0) {
						BackpackEntry e = defaultEntityEntry._backpackEntries.get(index);
						defaultEntityEntry._backpackEntries.set(index, new BackpackEntry(
							e.id, e.backpack, backpackEntry.chance, e.lootTable, e.colorRange, e.isDefault));
					} else defaultEntityEntry._backpackEntries.add(backpackEntry);
				}
			} else dest.add(entityEntry);
		}
		
		return dest;
	}
	
	public static List<BackpackEntry> getBackpackEntries(Class<? extends EntityLivingBase> entityClass) {
		if (entityClass == null) throw new NullPointerException("entityClass must not be null");
		BackpackEntityEntry entityEntry = getEntityEntry(entityClass);
		return (entityEntry != null) ? entityEntry.getEntries() : Collections.emptyList();
	}
	
	
	public static final class BackpackEntityEntry {
		
		public static final BackpackEntityEntry PLAYER = new BackpackEntityEntry(
			"<player>", RenderOptions.DEFAULT, Collections.emptyList(), true);
		
		public final String entityID;
		public final RenderOptions renderOptions;
		public final boolean isDefault;
		
		private final List<BackpackEntry> _backpackEntries;
		
		public BackpackEntityEntry(String entityID, RenderOptions renderOptions,
		                           List<BackpackEntry> entries, boolean isDefault) {
			if (entityID == null) throw new NullPointerException("entityID must not be null");
			if (renderOptions == null) throw new NullPointerException("renderOptions must not be null");
			if (entries == null) throw new NullPointerException("entries must not be null");
			this.entityID      = entityID;
			this.renderOptions = renderOptions;
			this.isDefault     = isDefault;
			_backpackEntries   = entries;
		}
		public BackpackEntityEntry(BackpackEntityEntry value) {
			this(value.entityID, value.renderOptions,
			     new ArrayList<>(value._backpackEntries), value.isDefault);
		}
		
		public Class<? extends EntityLivingBase> getEntityClass() {
			return _entityClassLookupCache.computeIfAbsent(entityID, id ->
				Optional.ofNullable(ForgeRegistries.ENTITIES.getValue(new ResourceLocation(id)))
					.map(EntityEntry::getEntityClass)
					.filter(EntityLivingBase.class::isAssignableFrom)
					.map(c -> c.asSubclass(EntityLivingBase.class))
				).orElse(null);
		}
		
		public List<BackpackEntry> getEntries()
			{ return Collections.unmodifiableList(_backpackEntries); }
		
		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof BackpackEntityEntry)) return false;
			BackpackEntityEntry other = (BackpackEntityEntry)obj;
			return Objects.equals(other.entityID, entityID)
				&& other.renderOptions.equals(renderOptions)
				&& other._backpackEntries.equals(_backpackEntries);
		}
		
	}
	
	public static final class RenderOptions {
		
		public static final RenderOptions DEFAULT = new RenderOptions(0, 2.5, 0.0, 0.8);
		
		public final double y, z;
		public final double rotate, scale;
		
		public RenderOptions(double y, double z, double rotate, double scale)
			{ this.y = y; this.z = z; this.rotate = rotate; this.scale = scale; }
		
		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof RenderOptions)) return false;
			RenderOptions other = (RenderOptions)obj;
			return (other.y == y) && (other.z == z) &&
			       (other.rotate == rotate) && (other.scale == scale);
		}
		
	}
	
	public static final class BackpackEntry {
		
		public static final BackpackEntry DEFAULT = new BackpackEntry(
			null, "wearablebackpacks:backpack", 1000, "wearablebackpacks:backpack/default", null, false);
		
		public final String id;
		public final String backpack;
		public final int chance;
		public final String lootTable;
		public final ColorRange colorRange; // TODO: Check if color is supported?
		public final boolean isDefault;
		
		public BackpackEntry(String id, String backpack, int chance,
		                     String lootTable, ColorRange colorRange, boolean isDefault) {
			if (backpack == null) throw new NullPointerException("backpack must not be null");
			if (chance < 0) throw new NullPointerException("chance must not be negative");
			
			this.id         = id;
			this.backpack   = backpack;
			this.chance     = chance;
			this.lootTable  = lootTable;
			this.colorRange = colorRange;
			this.isDefault  = isDefault;
		}
		
		@SuppressWarnings("unchecked")
		public <T extends Item & IBackpackType> T getBackpackItem() {
			Item item = Item.getByNameOrId(backpack);
			return (item instanceof IBackpackType) ? (T)item : null;
		}
		
		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof BackpackEntry)) return false;
			BackpackEntry other = (BackpackEntry)obj;
			return Objects.equals(other.id, id)
				&& Objects.equals(other.backpack, backpack)
				&& (other.chance == chance)
				&& Objects.equals(other.lootTable, lootTable)
				&& Objects.equals(other.colorRange, colorRange);
		}
		
	}
	
	public static final class ColorRange {
		
		public static final ColorRange DEFAULT = new ColorRange(0x202020, 0xD0D0D0);
		private static final Random rnd = new Random();
		
		public final int min, max;
		
		public ColorRange(int min, int max)
			{ this.min = min; this.max = max; }
		
		public boolean isValid() {
			int minR = min >> 16 & 0xFF;
			int minG = min >> 8 & 0xFF;
			int minB = min & 0xFF;
			int maxR = max >> 16 & 0xFF;
			int maxG = max >> 8 & 0xFF;
			int maxB = max & 0xFF;
			return (minR <= maxR) && (minG <= maxG) && (minB <= maxB);
		}
		
		public int getRandom() {
			int minR = min >> 16 & 0xFF;
			int minG = min >> 8 & 0xFF;
			int minB = min & 0xFF;
			int maxR = max >> 16 & 0xFF;
			int maxG = max >> 8 & 0xFF;
			int maxB = max & 0xFF;
			int r = (maxR - minR + 1 >= 0) ? minR + rnd.nextInt(maxR - minR + 1) : minR;
			int g = (maxG - minG + 1 >= 0) ? minG + rnd.nextInt(maxG - minG + 1) : minG;
			int b = (maxB - minB + 1 >= 0) ? minB + rnd.nextInt(maxB - minB + 1) : minB;
			return r << 16 | g << 8 | b;
		}
		
		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof ColorRange)) return false;
			ColorRange other = (ColorRange)obj;
			return (other.min == min) && (other.max == max);
		}
		
	}
	
} public interface IBackpack {
	
	@CapabilityInject(IBackpack.class)
	Capability<IBackpack> CAPABILITY = null;
	
	ItemStack getStack();
	void setStack(ItemStack value);
	
	IBackpackData getData();
	void setData(IBackpackData value);
	
	int getPlayersUsing();
	void setPlayersUsing(int value);
	
	int getLidTicks();
	int getPrevLidTicks();
	void setLidTicks(int value);
	
	default IBackpackType getType() {
		return BackpackHelper.getBackpackType(getStack());
	}
	
	
	/** A dummy implementation of IBackpack. */
	public static class Impl implements IBackpack {
		
		protected ItemStack stack    = ItemStack.EMPTY;
		protected IBackpackData data = null;
		protected int playersUsing   = 0;
		protected int lidTicks       = 0;
		protected int prevLidTicks   = 0;
		
		public ItemStack getStack() { return stack; }
		public void setStack(ItemStack value) { stack = value; }
		
		public IBackpackData getData() { return data; }
		public void setData(IBackpackData value) { data = value; }
		
		public int getPlayersUsing() { return playersUsing; }
		public void setPlayersUsing(int value) { playersUsing = value; }
		
		public int getLidTicks() { return lidTicks; }
		public int getPrevLidTicks() { return prevLidTicks; }
		public void setLidTicks(int value)
			{ prevLidTicks = lidTicks; lidTicks = value; }
		
	}
	
} public static final class BackpackEntityEntry {
		
	public static final BackpackEntityEntry PLAYER = new BackpackEntityEntry(
		"<player>", RenderOptions.DEFAULT, Collections.emptyList(), true);
		
	public final String entityID;
	public final RenderOptions renderOptions;
	public final boolean isDefault;
		
	private final List<BackpackEntry> _backpackEntries;
		
	public BackpackEntityEntry(String entityID, RenderOptions renderOptions,
	                           List<BackpackEntry> entries, boolean isDefault) {
		if (entityID == null) throw new NullPointerException("entityID must not be null");
		if (renderOptions == null) throw new NullPointerException("renderOptions must not be null");
		if (entries == null) throw new NullPointerException("entries must not be null");
		this.entityID      = entityID;
		this.renderOptions = renderOptions;
		this.isDefault     = isDefault;
		_backpackEntries   = entries;
	}
	public BackpackEntityEntry(BackpackEntityEntry value) {
		this(value.entityID, value.renderOptions,
		     new ArrayList<>(value._backpackEntries), value.isDefault);
	}
		
	public Class<? extends EntityLivingBase> getEntityClass() {
		return _entityClassLookupCache.computeIfAbsent(entityID, id ->
			Optional.ofNullable(ForgeRegistries.ENTITIES.getValue(new ResourceLocation(id)))
				.map(EntityEntry::getEntityClass)
				.filter(EntityLivingBase.class::isAssignableFrom)
				.map(c -> c.asSubclass(EntityLivingBase.class))
			).orElse(null);
	}
		
	public List<BackpackEntry> getEntries()
		{ return Collections.unmodifiableList(_backpackEntries); }
		
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof BackpackEntityEntry)) return false;
		BackpackEntityEntry other = (BackpackEntityEntry)obj;
		return Objects.equals(other.entityID, entityID)
			&& other.renderOptions.equals(renderOptions)
			&& other._backpackEntries.equals(_backpackEntries);
	}
		
} public class ProxyCommon {

	public void preInit() {
		MinecraftForge.EVENT_BUS.register(this);
		MinecraftForge.EVENT_BUS.register(WearableBackpacks.CONFIG);
		MinecraftForge.EVENT_BUS.register(WearableBackpacks.CONTENT);
		MinecraftForge.EVENT_BUS.register(new DyeWashingHandler());

		// FIXME: BackpackCapability requires an entity for its constructor. WHAT WAS I THINKING?
		CapabilityManager.INSTANCE.register(IBackpack.class,
			new BackpackCapability.Storage(), BackpackCapability::new);
	}

	public void init() {  }

	/** Intializes the backpack layers on the client-side.
	 *  Called when spawn setting is loaded / changed. */
	public void initBackpackLayers() {  }

	// Attaching / sending capability

	@SubscribeEvent
	public void onAttachCapabilitiesEntity(AttachCapabilitiesEvent<Entity> event) {
		if (!BackpackRegistry.canEntityWearBackpacks(event.getObject())) return;
		// Give entities that can wear backpacks the backpack capability.
		event.addCapability(BackpackCapability.IDENTIFIER,
			new BackpackCapability.Provider((EntityLivingBase)event.getObject()));
	}

	@SubscribeEvent
	public void onPlayerLogin(PlayerLoggedInEvent event)
		{ sendBackpackStack(event.player, event.player); }
	@SubscribeEvent
	public void onPlayerChangedDimensionEvent(PlayerChangedDimensionEvent event)
		{ sendBackpackStack(event.player, event.player); }
	@SubscribeEvent
	public void onPlayerRespawn(PlayerRespawnEvent event)
		{ sendBackpackStack(event.player, event.player); }
	@SubscribeEvent
	public void onPlayerStartTracking(PlayerEvent.StartTracking event)
		{ sendBackpackStack(event.getTarget(), event.getEntityPlayer()); }

	private void sendBackpackStack(Entity carrier, EntityPlayer player) {
		BackpackCapability backpack = (BackpackCapability)BackpackHelper.getBackpack(carrier);
		if (backpack != null) WearableBackpacks.CHANNEL.sendTo(
			MessageBackpackUpdate.stack(carrier, backpack.stack), player);
	}

	// Backpack interactions / events

	@SubscribeEvent
	public void onCheckSpawn(CheckSpawn event) {
		// When a mob is about to spawn, see if it has a chance to wear a backpack.
		if (!(event.isSpawner() ? WearableBackpacks.CONFIG.entity.spawnFromSpawners
		                        : WearableBackpacks.CONFIG.entity.spawnNaturally).get()) return;
		EntityLivingBase entity = event.getEntityLiving();

		for (BackpackEntry entry : BackpackRegistry.getBackpackEntries(entity.getClass())) {
			if ((entry.chance == 0) || (entity.world.rand.nextDouble() > (1.0 / entry.chance))) continue;
			BackpackCapability backpack = (BackpackCapability)entity.getCapability(IBackpack.CAPABILITY, null);
			// Set the backpack capability of the entity to spawn with the specified backpack.
			// This will be delayed until the first update tick to fire after armor has been generated.
			backpack.spawnWith = entry;
			break;
		}
	}
	/** Called when a mob spawns with a backpack with a 1 tick delay. */
	private void onSpawnedWith(EntityLivingBase entity, BackpackCapability backpack, BackpackEntry entry) {
		ItemStack stack = new ItemStack(entry.getBackpackItem());

		if (entry.colorRange != null) // Set a random color!
			NbtUtils.set(stack, entry.colorRange.getRandom(), "display", "color");

		// Set damage to a random amount (25% - 75%).
		int maxDamage = stack.getMaxDamage();
		int damage = maxDamage / 4 + ((maxDamage / 2 > 0)
			? entity.world.rand.nextInt(maxDamage / 2) : 0);
		stack.setItemDamage(damage);

		if (BackpackHelper.equipAsChestArmor) {
			// If the entity spawned with enchanted armor,
			// then move over all compatible enchantments.
			ItemStack armor = entity.getItemStackFromSlot(EntityEquipmentSlot.CHEST);
			if ((armor != null) && armor.isItemEnchanted()) {
				NBTTagList enchList = armor.getEnchantmentTagList();
				for (int i = 0; i < enchList.tagCount(); ++i) {
					NBTTagCompound enchTag = enchList.getCompoundTagAt(i);
					Enchantment enchantment = Enchantment.getEnchantmentByID(enchTag.getShort("id"));
					// If the enchantment doesn't work with the backpack, remove it.
					if (!enchantment.canApply(stack)) enchList.removeTag(i--);
				}
				if (enchList.tagCount() > 0)
					NbtUtils.set(stack, enchList, "ench");
			}
		}

		IBackpackType type = entry.getBackpackItem();
		IBackpackData data = type.createBackpackData(stack);
		BackpackHelper.setEquippedBackpack(entity, stack, data);
		type.onSpawnedWith(entity, backpack, entry.lootTable);
		backpack.spawnWith  = null;
		backpack.mayDespawn = true;
	}

	private boolean cancelOffHand = false;
	@SubscribeEvent
	public void onPlayerInteractBlock(PlayerInteractEvent.RightClickBlock event) {

		// This event is fired twice, once for each hand. Unfortunately there
		// is no way to set the result of the main hand interaction to SUCCESS
		// so the off hand one will be skipped. So: Hacky code!

		if (cancelOffHand) {
			cancelOffHand = false;
			if (event.getHand() == EnumHand.OFF_HAND)
				{ event.setCanceled(true); return; }
		}

		// When players sneak-right-click the ground with an
		// empty hand, place down their equipped backpack.

		EntityPlayer player = event.getEntityPlayer();
		World world = event.getWorld();
		if (!player.isSneaking() || (event.getHand() != EnumHand.MAIN_HAND) ||
		    !player.getHeldItem(EnumHand.MAIN_HAND).isEmpty()) return;

		IBackpack backpack = BackpackHelper.getBackpack(player);
		if (backpack == null) return;

		// Try place the equipped backpack on the ground by using it. Also takes
		// care of setting the tile entity stack and data as well as unequipping.
		// See ItemBackpack.onItemUse.
		player.inventory.mainInventory.set(player.inventory.currentItem, backpack.getStack());
		if (backpack.getStack().onItemUse(
				player, world, event.getPos(), EnumHand.MAIN_HAND,
				event.getFace(), 0.5F, 0.5F, 0.5F) == EnumActionResult.SUCCESS) {

			player.swingArm(EnumHand.MAIN_HAND);
			event.setCanceled(true);
			cancelOffHand = true;

		} else player.inventory.mainInventory.set(player.inventory.currentItem, ItemStack.EMPTY);

	}

	@SubscribeEvent
	public void onEntityInteract(PlayerInteractEvent.EntityInteract event) {

		// When players right-click equipped backpacks, interact with them.

		if (!WearableBackpacks.CONFIG.enableEquippedInteraction.get() ||
		    !(event.getTarget() instanceof EntityLivingBase)) return;
		EntityPlayer player = event.getEntityPlayer();
		EntityLivingBase target = (EntityLivingBase)event.getTarget();

		BackpackCapability backpack = (BackpackCapability)target.getCapability(IBackpack.CAPABILITY, null);
		if ((backpack == null) || !BackpackHelper.canInteractWithEquippedBackpack(player, target)) return;

		IBackpackType type = backpack.getType();
		if (type == null) {
			WearableBackpacks.LOG.error("Backpack type was null when accessing equipped backpack");
			return;
		}
		if (!player.world.isRemote && (backpack.getData() == null)) {
			IBackpackData data = type.createBackpackData(backpack.getStack());
			if (data != null) {
				// Only show this error message if the backpack type is supposed to have backpack data.
				// Some backpacks might not need any to function, for example an ender backpack.
				WearableBackpacks.LOG.error("Backpack data was null when accessing equipped backpack");
				backpack.setData(data);
			}
		}
		type.onEquippedInteract(player, target, backpack);

	}

	@SubscribeEvent
	public void onLivingUpdate(LivingUpdateEvent event) {

		// Update equipped backpacks and check
		// if they've been removed somehow.

		EntityLivingBase entity = event.getEntityLiving();
		BackpackCapability backpack = (BackpackCapability)entity
			.getCapability(IBackpack.CAPABILITY, null);
		if (backpack == null) return;

		if (backpack.spawnWith != null)
			onSpawnedWith(entity, backpack, backpack.spawnWith);
		boolean hasBackpack = !backpack.getStack().isEmpty();

		if (backpack.isChestArmor()) {
			if (entity instanceof EntityPlayer)
				SlotBackpackWrapper.replace((EntityPlayer)entity, backpack.getStack());

			if (!hasBackpack) {
				// Backpack has been removed somehow.
				backpack.getType().onFaultyRemoval(entity, backpack);
				backpack.setStack(ItemStack.EMPTY);
				return;
			}
		} else if (!hasBackpack) return;

		backpack.getType().onEquippedTick(entity, backpack);
		if (entity.world.isRemote)
			BackpackHelper.updateLidTicks(backpack, entity.posX, entity.posY + 1.0, entity.posZ);

	}

	@SubscribeEvent
	public void onLivingDeath(LivingDeathEvent event) {
		// If an entity wearing a backpack dies, try
		// to place it as a block, or drop the items.

		EntityLivingBase entity = event.getEntityLiving();
		World world = entity.world;
		if (world.isRemote) return;

		BackpackCapability backpack = (BackpackCapability)entity
			.getCapability(IBackpack.CAPABILITY, null);
		if ((backpack == null) || backpack.getStack().isEmpty()) return;

		// If keep inventory (or other item keeping method) is on,
		// keep the backpack capability so we can copy it over to the new player
		// entity in onPlayerClone.
		if (MiscUtils.shouldKeepItem(entity, backpack.getStack())) return;

		// Attempt to place the backpack as a block instead of dropping the items.
		if (WearableBackpacks.CONFIG.dropAsBlockOnDeath.get()) {

			List<BlockCoord> coords = new ArrayList<BlockCoord>();
			for (int x = -2; x <= 2; x++)
				for (int z = -2; z <= 2; z++)
					coords.add(new BlockCoord(entity, x, z));

			// Try to place the backpack on the ground nearby,
			// or look for a ground above or below to place it.

			Collections.sort(coords, new Comparator<BlockCoord>() {
				@Override public int compare(BlockCoord o1, BlockCoord o2) {
					if (o1.distance < o2.distance) return -1;
					else if (o1.distance > o2.distance) return 1;
					else return 0;
				}
			});
			while (!coords.isEmpty()) {
				Iterator<BlockCoord> iter = coords.iterator();
				while (iter.hasNext()) {
					BlockCoord coord = iter.next();
					// Attempt to place and unequip the backpack at
					// this coordinate. If successful, we're done here.
					if (BackpackHelper.placeBackpack(world, coord, backpack.getStack(), entity, true)) {
						// TODO: I'm aware that this is not the cleanest solution.
						((TileEntityBackpack)world.getTileEntity(coord)).setPlacedOnDeath(backpack.mayDespawn);
						return;
					}
					boolean replacable = world.getBlockState(coord).getBlock().isReplaceable(world, coord);
					coord.add(0, (replacable ? -1 : 1), 0);
					coord.moved += (replacable ? 1 : 5);
					if ((coord.getY() <= 0) || (coord.getY() > world.getHeight()) ||
						(coord.moved > 24 - coord.distance * 4)) iter.remove();
				}
			}

		}

		// In the case of regular backpacks, this causes their contents to be dropped.
		backpack.getType().onDeath(entity, backpack);

		// Drop the backpack as an item and remove it from the entity.
		if (!backpack.getStack().isEmpty())
			WorldUtils.dropStackFromEntity(entity, backpack.getStack(), 4.0F);
		BackpackHelper.setEquippedBackpack(entity, ItemStack.EMPTY, null);
	}

	// Would use a method local class but "extractRangemapReplacedMain" gradle task doesn't like that.
	private static class BlockCoord extends MutableBlockPos {
		public double distance;
		public int moved = 0;
		public BlockCoord(Entity entity, int x, int z) {
			super((int)entity.posX + x, (int)entity.posY, (int)entity.posZ + z);
			distance = Math.sqrt(Math.pow(getX() + 0.5 - entity.posX, 2) +
			                     Math.pow(getY() + 0.5 - entity.posY, 2) +
			                     Math.pow(getZ() + 0.5 - entity.posZ, 2));
		}
	}

	@SubscribeEvent
	public void onPlayerClone(PlayerEvent.Clone event) {
		// This comes into play when the "keepInventory" gamerule is on.
		// In that case, onLivingDeath will keep the backpack information,
		// so we can transfer it to the new player entity.
		IBackpack originalBackpack = BackpackHelper.getBackpack(event.getOriginal());
		if (originalBackpack == null) return;

		EntityPlayer player = event.getEntityPlayer();
		IBackpack clonedBackpack = player.getCapability(IBackpack.CAPABILITY, null);
		clonedBackpack.setStack(originalBackpack.getStack());
		clonedBackpack.setData(originalBackpack.getData());
	}

} public class TileEntityBackpack extends TileEntity implements ITickable, IBackpack {
	
	/** If any player is within this distance of the backpack, _despawnTimer is paused.' */
	private static final int DESPAWN_DISTANCE = 20;
	private static final int DESPAWN_TIME = 5 * 60 * 20; // 5 minutes
	
	public static final String TAG_AGE    = "age";
	public static final String TAG_STACK  = "stack";
	public static final String TAG_DATA   = "data";
	public static final String TAG_FACING = "facing";
	public static final String TAG_DESPAWN_TIMER = "despawnTimer";
	
	private int _age = 0;
	private ItemStack _stack = ItemStack.EMPTY;
	private IBackpackData _data = null;
	private int _playersUsing = 0;
	private int _lidTicks = 0;
	private int _prevLidTicks = 0;
	private int _despawnTimer = -1;
	
	public EnumFacing facing = EnumFacing.NORTH;
	
	/** Returns the age of this backpack tile entity in ticks. */
	public int getAge() { return _age; }
	
	@Override
	public void update() {
		_age++;
		if (world.isRemote)
			BackpackHelper.updateLidTicks(this,
				pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
		else if (_despawnTimer >= 0) {
			// TODO: Slower than it needs to be; just want to check if there's ANY player nearby.
			EntityPlayer player = world.getClosestPlayer(
				pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, DESPAWN_DISTANCE, false);
			// If no player is within DESPAWN_DISTANCE blocks, increase the despawn timer
			// and if this timer is above the despawn time, destroy the tile entity.
			if ((player == null) && (++_despawnTimer > DESPAWN_TIME)) {
				_stack = ItemStack.EMPTY; _data = null;
				world.setBlockToAir(pos);
			}
		}
	}
	
	@Override
	public boolean receiveClientEvent(int id, int type) {
		if (id == 0) { _playersUsing = type; return true; }
		return false;
	}
	
	/** Called when the backpack is placed upon death. */
	public void setPlacedOnDeath(boolean mayDespawn) {
		// Set the age to a negative value - this prevents
		// backpacks from exploding right after being dropped.
		_age = -BlockBackpack.EXPLOSION_RESIST_TICKS;
		// If the backpack may despawn, set the despawn
		// timer to 0 so it'll be active, -1 otherwise.
		_despawnTimer = (mayDespawn ? 0 : -1);
	}
	
	// Reading/writing, loading/saving, update packets
	
	public void readNBT(NBTTagCompound compound, boolean isClient) {
		_age = (!isClient ? compound.getInteger(TAG_AGE) : 0);
		facing = EnumFacing.byIndex(NbtUtils.get(compound, (byte)0, TAG_FACING) + 2);
		
		_stack = NbtUtils.readItem(compound.getCompoundTag(TAG_STACK));
		if (_stack.isEmpty() || isClient) { _data = null; return; }
		
		_data = BackpackHelper.getBackpackType(_stack).createBackpackData(_stack);
		NBTBase dataTag = compound.getTag(TAG_DATA);
		if (dataTag != null) _data.deserializeNBT(dataTag);
		
		_despawnTimer = (compound.hasKey(TAG_DESPAWN_TIMER)
			? compound.getInteger(TAG_DESPAWN_TIMER) : -1);
	}
	
	public NBTTagCompound writeNBT(NBTTagCompound compound, boolean isClient) {
		NbtUtils.addToCompound(compound,
			TAG_AGE, (!isClient ? _age : null),
			TAG_FACING, (byte)(facing.ordinal() - 2),
			TAG_STACK, (!_stack.isEmpty() ? _stack.serializeNBT() : null),
			TAG_DATA, (((_data != null) && !isClient) ? _data.serializeNBT() : null),
			TAG_DESPAWN_TIMER, (((_despawnTimer >= 0) && !isClient) ? _despawnTimer : null));
		return compound;
	}
	
	
	@Override
	public final SPacketUpdateTileEntity getUpdatePacket() {
		return new SPacketUpdateTileEntity(getPos(), getBlockMetadata(),
			writeNBT(new NBTTagCompound(), true));
	}
	
	@Override
	public final NBTTagCompound getUpdateTag()
		{ return writeNBT(super.writeToNBT(new NBTTagCompound()), true); }
	
	@Override
	public final void onDataPacket(NetworkManager net, SPacketUpdateTileEntity pkt)
		{ readNBT(pkt.getNbtCompound(), true); }
	
	@Override
	public void readFromNBT(NBTTagCompound compound) {
		super.readFromNBT(compound);
		readNBT(compound, false);
	}
	
	@Override
	public NBTTagCompound writeToNBT(NBTTagCompound compound) {
		return writeNBT(super.writeToNBT(compound), false);
	}
	
	// IBackpack implementation
	
	@Override
	public ItemStack getStack() { return _stack; }
	@Override
	public void setStack(ItemStack value) { _stack = value; }
	
	@Override
	public IBackpackData getData() { return _data; }
	@Override
	public void setData(IBackpackData value) { _data = value; }
	
	@Override
	public int getPlayersUsing() { return _playersUsing; }
	@Override
	public void setPlayersUsing(int value) {
		if ((value > 0) != (_playersUsing > 0))
			world.addBlockEvent(pos, getBlockType(), 0, (value > 0) ? 1 : 0);
		_playersUsing = value;
		_despawnTimer = -1;
	}
	
	@Override
	public int getLidTicks() { return _lidTicks; }
	@Override
	public int getPrevLidTicks() { return _prevLidTicks; }
	@Override
	public void setLidTicks(int value)
		{ _prevLidTicks = _lidTicks; _lidTicks = value; }
	
	// Capability overrides
	
	@Override
	public boolean hasCapability(Capability<?> capability, EnumFacing facing) {
		return (capability == IBackpack.CAPABILITY)
			|| ((capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
				&& WearableBackpacks.CONFIG.enableMachineInteraction.get()
				&& (getData() instanceof BackpackDataItems))
			|| super.hasCapability(capability, facing);
	}
	
	@Override
	public <T> T getCapability(Capability<T> capability, EnumFacing facing) {
		if (!hasCapability(capability, facing)) return null;
		return (capability == IBackpack.CAPABILITY)
				? IBackpack.CAPABILITY.cast(this)
			: ((capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY))
				? CapabilityItemHandler.ITEM_HANDLER_CAPABILITY.cast(((BackpackDataItems)getData()).getItems())
			: super.getCapability(capability, facing);
	}
} @SideOnly(Side.CLIENT)
public class BakedModelDefaultTexture implements IBakedModel
{
	private final IBakedModel _baseModel;
	private final List<BakedQuad>[] _sideQuads;
	private final List<BakedQuad> _defaultQuads;
	
	@SuppressWarnings("unchecked")
	public BakedModelDefaultTexture(IBakedModel base) {
		_baseModel = base;
		_sideQuads = Arrays.stream(EnumFacing.VALUES)
			.map((side) -> filterQuads(_baseModel.getQuads(null, side, 0)))
			.toArray((length) -> new List[length]);
		_defaultQuads = filterQuads(_baseModel.getQuads(null, null, 0));
	}
	private static List<BakedQuad> filterQuads(List<BakedQuad> source) {
		return source.stream()
			.filter((quad) -> !quad.getSprite().getIconName().contains("overlay"))
			.collect(Collectors.toList());
	}
	
	public List<BakedQuad> getQuads(IBlockState state, EnumFacing side, long rand) {
		return ((side != null) ? _sideQuads[side.getIndex()] : _defaultQuads);
	}
	
	public boolean isAmbientOcclusion() { return _baseModel.isAmbientOcclusion(); }
	
	public boolean isGui3d() { return _baseModel.isGui3d(); }
	
	public boolean isBuiltInRenderer() { return _baseModel.isBuiltInRenderer(); }
	
	public TextureAtlasSprite getParticleTexture() { return _baseModel.getParticleTexture(); }
	
	@Deprecated
	public ItemCameraTransforms getItemCameraTransforms() { return _baseModel.getItemCameraTransforms(); }
	
	public ItemOverrideList getOverrides() { return _baseModel.getOverrides(); }
} @SideOnly(Side.CLIENT)
public class KeyBindingHandler {
	
	public static final String CATEGORY = WearableBackpacks.MOD_NAME;
	
	public static final KeyBinding openBackpack = new KeyBinding(
		"key." + WearableBackpacks.MOD_ID + ".open",
		KeyConflictContext.IN_GAME, Keyboard.KEY_B, CATEGORY);
	
	public KeyBindingHandler() {
		ClientRegistry.registerKeyBinding(openBackpack);
	}
	
	@SubscribeEvent
	public void onKey(KeyInputEvent event) {
		if (openBackpack.isPressed() &&
		    (BackpackHelper.getBackpack(ClientUtils.getPlayer()) != null) &&
		    WearableBackpacks.CONFIG.enableSelfInteraction.get())
			WearableBackpacks.CHANNEL.sendToServer(MessageOpenBackpack.create());
	}
	
} @SideOnly(Side.CLIENT)
public final class RendererBackpack {
	
	private RendererBackpack() {  }
	
	private static void renderBackpack(IBackpack backpack, float ticks, boolean renderStraps) {
		ItemStack stack = backpack.getStack();
		int color = ProxyClient.ITEM_COLOR.colorMultiplier(stack, 0);
		
		BlockModelRenderer renderer = Minecraft.getMinecraft().getBlockRendererDispatcher().getBlockModelRenderer();
		Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE);
		
		float r = (color >> 16 & 0xFF) / 255.0F;
		float g = (color >> 8 & 0xFF) / 255.0F;
		float b = (color & 0xFF) / 255.0F;
		renderModel(backpack, renderer, ticks, renderStraps, r, g, b, false);
		
		if (stack.isItemEnchanted())
			renderEnchanted(backpack, renderer, ticks, renderStraps);
	}
	
	// TODO: See if this can be changed back to FastTESR?
	//       Forge apparently has an animation API? asie says to ask fry.
	public static class TileEntity extends TileEntitySpecialRenderer<TileEntityBackpack> {
		
		@Override
		public void render(TileEntityBackpack entity, double x, double y, double z,
		                   float partialTicks, int destroyStage, float alpha) {
			IBackpack backpack = BackpackHelper.getBackpack(entity);
			if (backpack == null) return;
			float angle = entity.facing.getHorizontalAngle();
			
			GlStateManager.pushMatrix();
			GlStateManager.translate(x + 0.5, y + 0.5, z + 0.5);
			GlStateManager.rotate(angle, 0.0F, -1.0F, 0.0F);
			GlStateManager.translate(-0.5, -0.5, -0.5);
			renderBackpack(backpack, entity.getAge() + partialTicks, true);
			GlStateManager.popMatrix();
		}
		
	}
	
	public static class Layer implements LayerRenderer<EntityLivingBase> {
		
		private static IBackpack _overrideBackpack = null;
		private static RenderOptions _overrideRenderOptions = null;
		
		protected final RenderLivingBase<?> livingEntityRenderer;
		
		public Layer(RenderLivingBase<?> livingEntityRendererIn)
	    {
	        this.livingEntityRenderer = livingEntityRendererIn;
	    }
		
		public boolean shouldCombineTextures() { return false; }
		
		public void doRenderLayer(EntityLivingBase entity, float limbSwing, float limbSwingAmount,
		                          float partialTicks, float ageInTicks, float netHeadYaw, float headPitch, float scale) {
			
			IBackpack backpack;
			RenderOptions renderOptions;
			if (_overrideBackpack != null) {
				backpack = _overrideBackpack;
				renderOptions = _overrideRenderOptions;
			} else {
				backpack = BackpackHelper.getBackpack(entity);
				BackpackEntityEntry entry = BackpackRegistry.getEntityEntry(entity.getClass());
				renderOptions = (entry != null) ? entry.renderOptions : null;
			}
			if ((backpack == null) || (renderOptions == null)) return;
			
			GlStateManager.pushMatrix();
			
			if (entity.isChild()) {
				GlStateManager.scale(0.5F, 0.5F, 0.5F);
				GlStateManager.translate(0.0F, 24.0F * scale, 0.0F);
			}
			
			ModelBase modelBase = this.livingEntityRenderer.getMainModel();
			if (modelBase instanceof ModelBiped) {
				ModelRenderer bipedBody = ((ModelBiped) modelBase).bipedBody;
				
				if (entity.isSneaking()) {
					// FIXME: Can be sneaking while flying with the elytra, then backpack is misaligned..?
					GlStateManager.translate(0.0F, 0.2F, 0.0F);
				}
				
				bipedBody.postRender(scale);
			} else {
				// Fallback to the custom way of transforming the backpack.
				// Make backpack swing with body as players swing their arms.
				float swingProgress = entity.getSwingProgress(partialTicks);
				float swingAngle = MathHelper.sin(MathHelper.sqrt(swingProgress) * ((float)Math.PI * 2.0F)) * 0.2F;
				if ((entity.swingingHand == EnumHand.OFF_HAND) ^
				    (entity.getPrimaryHand() == EnumHandSide.LEFT)) swingAngle *= -1;
				if (swingAngle != 0) GlStateManager.rotate((float)Math.toDegrees(swingAngle), 0.0F, 1.0F, 0.0F);
				
				// Rotate backpack if entity is sneaking.
				if (entity.isSneaking()) {
					// FIXME: Can be sneaking while flying with the elytra, then backpack is misaligned..?
					GlStateManager.translate(0.0F, 0.2F, 0.0F);
					GlStateManager.rotate(90.0F / (float)Math.PI, 1.0F, 0.0F, 0.0F);
				}
			}
			
			GlStateManager.scale(renderOptions.scale, renderOptions.scale, renderOptions.scale);
			GlStateManager.translate(8.0F * scale, renderOptions.y * scale, renderOptions.z * scale);
			GlStateManager.rotate(180.0F, 0.0F, 0.0F, 1.0F);
			GlStateManager.rotate((float)renderOptions.rotate, 1.0F, 0.0F, 0.0F);
			GlStateManager.translate(0, ProxyClient.MODEL_BACKPACK_BOX.maxY * scale * -16,
			                            ProxyClient.MODEL_BACKPACK_BOX.minZ * scale * -16);
			
			renderBackpack(backpack, entity.ticksExisted + partialTicks, false);
			
			GlStateManager.popMatrix();
			
		}
		
		public static void setOverride(IBackpack backpack, RenderOptions renderOptions)
			{ _overrideBackpack = backpack; _overrideRenderOptions = renderOptions; }
		public static void resetOverride() { setOverride(null, null); }
		
	}
	
	private static void renderModel(IBackpack backpack, BlockModelRenderer renderer,
	                                float ticks, boolean renderStraps,
	                                float r, float g, float b, boolean useEnch) {
		
		IBakedModel baseModel = (useEnch ? ProxyClient.MODEL_BACKPACK_ENCH : ProxyClient.MODEL_BACKPACK);
		renderer.renderModelBrightnessColor(baseModel, 1.0F, r, g, b);
		if (renderStraps)
			renderer.renderModelBrightnessColor(ProxyClient.MODEL_BACKPACK_STRAPS, 1.0F, r, g, b);
		
		float lidAngle = 0.0F;
		IBackpackType type = backpack.getType();
		if (type != null) {
			int lidTicks = backpack.getLidTicks();
			int prevLidTicks = backpack.getPrevLidTicks();
			lidAngle = type.getLidAngle(lidTicks, prevLidTicks, ticks % 1);
		}
		
		// FIXME: Allow for custom, adjustable lid position.
		float lidYOffset = 9 / 16.0F;
		float lidZOffset = 5 / 16.0F;

		GlStateManager.pushMatrix();
		GlStateManager.translate(0.0F, lidYOffset, lidZOffset);
		GlStateManager.rotate(lidAngle, -1.0F, 0.0F, 0.0F);
		GlStateManager.translate(0.0F, -lidYOffset, -lidZOffset);
		IBakedModel topModel = (useEnch ? ProxyClient.MODEL_BACKPACK_ENCH_TOP : ProxyClient.MODEL_BACKPACK_TOP);
		renderer.renderModelBrightnessColor(topModel, 1.0F, r, g, b);
		GlStateManager.popMatrix();
		
	}
	
	private static final ResourceLocation ENCHANTED_ITEM_GLINT =
		new ResourceLocation("textures/misc/enchanted_item_glint.png");
	// Based on LayerArmorBase.renderEnchantedGlint.
	private static void renderEnchanted(IBackpack backpack, BlockModelRenderer renderer,
	                                    float ticks, boolean renderStraps) {
		
		Minecraft mc = Minecraft.getMinecraft();
		float glintStrength = WearableBackpacks.CONFIG.cosmetic.enchantEffectOpacity.get().floatValue();
		if (glintStrength <= 0) return;
		float glintScale   = 1.5F;
		float animProgress = ticks / 10;
		
		int color = IntermodUtils.getRuneColor(backpack.getStack());
		float r = (color >> 16 & 0xFF) / 255.0F * glintStrength;
		float g = (color >>  8 & 0xFF) / 255.0F * glintStrength;
		float b = (color       & 0xFF) / 255.0F * glintStrength;
		
		mc.getTextureManager().bindTexture(ENCHANTED_ITEM_GLINT);
		mc.entityRenderer.setupFogColor(true);
		
		GlStateManager.disableLighting();
		GlStateManager.depthMask(false);
		GlStateManager.depthFunc(GL11.GL_EQUAL);
		GlStateManager.enableBlend();
		GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_COLOR, GlStateManager.DestFactor.ONE);
		
		for (int i = 0; i < 2; ++i) {
			GlStateManager.matrixMode(GL11.GL_TEXTURE);
			GlStateManager.loadIdentity();
			GlStateManager.scale(glintScale, glintScale, glintScale);
			GlStateManager.rotate(30.0F - i * 60.0F, 0.0F, 0.0F, 1.0F);
			GlStateManager.translate(0.0F, animProgress * (0.001F + i * 0.003F) * 20.0F, 0.0F);
			GlStateManager.matrixMode(GL11.GL_MODELVIEW);
			renderModel(backpack, renderer, ticks, renderStraps, r, g, b, true);
		}
		
		GlStateManager.matrixMode(GL11.GL_TEXTURE);
		GlStateManager.loadIdentity();
		GlStateManager.matrixMode(GL11.GL_MODELVIEW);
		
		GlStateManager.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
		GlStateManager.disableBlend();
		GlStateManager.depthFunc(GL11.GL_LEQUAL);
		GlStateManager.depthMask(true);
		GlStateManager.enableLighting();
		
		mc.entityRenderer.setupFogColor(false);
		
	}
	
} public class ItemBackpack extends Item implements IBackpackType, IDyeableItem, ISpecialArmor {
	
	public static final int DEFAULT_COLOR = 0xA06540;
	public static final ResourceLocation LOOT_TABLE =
		new ResourceLocation(WearableBackpacks.MOD_ID, "backpack/default");
	
	public static final String[] TAG_CUSTOM_ARMOR = { "backpack", "armor" };
	public static final String[] TAG_CUSTOM_SIZE  = { "backpack", "size" };
	
	
	public ItemBackpack() {
		setTranslationKey("wearablebackpacks.backpack");
		setMaxStackSize(1);
		setCreativeTab(CreativeTabs.TOOLS); // TODO: Use our own creative tab?
	}
	
	public int getArmorDamageReductionAmount(ItemStack stack) {
		int defaultArmor = WearableBackpacks.CONFIG.backpack.armor.get();
		return NbtUtils.get(stack, defaultArmor, TAG_CUSTOM_ARMOR);
	}
	
	public float getArmorToughness(ItemStack stack) { return 0; }
	
	// Item properties
	
	@Override
	public boolean getIsRepairable(ItemStack toRepair, ItemStack repair) {
		if (repair.isEmpty()) return false;

		// Returns is the specified repair item is
		// registered in the ore dictionary as leather.
		int leatherOreID = OreDictionary.getOreID("leather");
		return ArrayUtils.contains(OreDictionary.getOreIDs(repair), leatherOreID);
	}
	
	@Override
	public boolean isEnchantable(ItemStack stack) {
		// Only enchantable if backpack is equipped as chest armor.
		// Otherwise unbreaking and protection enchantments are useless.
		return BackpackHelper.equipAsChestArmor;
		// Also needs to be overridden because otherwise 0
		// durability considers the backpack unenchantable.
	}
	
	@Override
	public int getItemEnchantability() { return 12; }
	
	@Override
	@SideOnly(Side.CLIENT)
	public void addInformation(ItemStack stack, World worldIn, List<String> tooltip, ITooltipFlag flagIn) {
		
		IBackpack backpack = BackpackHelper.getBackpack(Minecraft.getMinecraft().player);
		boolean isEquipped = ((backpack != null) && (backpack.getStack() == stack));
		
		// If the shift key is held down, display equip / unequip hints,
		// otherwise just display "Hold SHIFT for more info" message.
		if (LangUtils.tooltipIsShiftKeyDown(tooltip)) {
			boolean equipAsChestArmor = WearableBackpacks.CONFIG.equipAsChestArmor.get();
			boolean enableSelfInteraction = WearableBackpacks.CONFIG.enableSelfInteraction.get();
			
			// If own backpacks can be interacted with while equipped and one is either
			// currently equipped or won't be equipped as chest armor, display open hint.
			// Does not display anything if key is unbound.
			if (enableSelfInteraction && (isEquipped || !equipAsChestArmor))
				LangUtils.formatTooltipKey(tooltip, "openHint", KeyBindingHandler.openBackpack);
			
			// If the backpack is the player's currently equipped backpack, display unequip hint.
			if (isEquipped) LangUtils.formatTooltip(tooltip, "unequipHint");
			// If not equipped, display the equip hint. If equipAsChestArmor is off,
			// use extended tooltip, which also explains how to unequip the backpack.
			else LangUtils.formatTooltip(tooltip, "equipHint" + (!equipAsChestArmor ? ".extended" : ""));
		}
		
		// If someone's using the player's backpack right now, display it in the tooltip.
		if (isEquipped && (backpack.getPlayersUsing() > 0))
			LangUtils.formatTooltipPrepend(tooltip, "\u00A8o", "used");
		
		
		// Only display the following information if advanced tooltips are enabled.
		if (!flagIn.isAdvanced()) return;
		
		NBTBase customSize = NbtUtils.get(stack, TAG_CUSTOM_SIZE);
		if (customSize != null)
			try { tooltip.add("Custom Size: " + BackpackSize.parse(customSize)); }
			catch (Exception ex) {  } // Ignore NBT parse exceptions - they're already logged in createBackpackData.
		
	}
	
	// Item events
	
	@Override
	public EnumActionResult onItemUse(EntityPlayer player, World worldIn, BlockPos pos, EnumHand hand,
	                                  EnumFacing facing, float hitX, float hitY, float hitZ) {
		IBlockState state = worldIn.getBlockState(pos);
		// If the block is replaceable, keep the placing position
		// the same but check the block below for solidity.
		if (state.getBlock().isReplaceable(worldIn, pos))
			state = worldIn.getBlockState(pos.offset(EnumFacing.DOWN));
		// Otherwise make sure the top side is used, and
		// change the placing position to the block above.
		else if (facing == EnumFacing.UP)
			pos = pos.offset(EnumFacing.UP);
		else return EnumActionResult.FAIL;
		
		// Check if the side is solid and try to place the backpack.
		return (state.isSideSolid(worldIn, pos, EnumFacing.UP) &&
		        BackpackHelper.placeBackpack(worldIn, pos, player.getHeldItem(hand), player, false))
			? EnumActionResult.SUCCESS : EnumActionResult.FAIL;
	}
	
	@Override
	public boolean itemInteractionForEntity(ItemStack stack, EntityPlayer playerIn,
	                                        EntityLivingBase target, EnumHand hand) {
		// When right clicking a non-player entity with a backpack in
		// creative, make the target entity equip the held backpack.
		if (playerIn.world.isRemote || !playerIn.isCreative() ||
		    !BackpackRegistry.canEntityWearBackpacks(target) ||
		    (target instanceof EntityPlayer)) return false;
		
		// If the target entity is already wearing a backpack, call
		// onFaultyRemoval, which may for example drop the backpack's items.
		IBackpack backpack = BackpackHelper.getBackpack(target);
		if (backpack != null) backpack.getType().onFaultyRemoval(target, backpack);
		
		stack = stack.splitStack(1); // This reduces the held stack's size while
		                             // giving us a copy with a stack size of 1.
		// (Not necessary since in creative, but this is the right way to do things!)
		IBackpackData data = BackpackHelper.getBackpackType(stack).createBackpackData(stack);
		BackpackHelper.setEquippedBackpack(target, stack, data);
		
		return true;
	}
	
	@Override
	public boolean canApplyAtEnchantingTable(ItemStack stack, Enchantment enchantment) {
		// Allow armor enchantments on top of anything supported by default.
		return (super.canApplyAtEnchantingTable(stack, enchantment) ||
		        ((enchantment.type == EnumEnchantmentType.ARMOR) && BackpackHelper.equipAsChestArmor));
	}
	
	// IBackpackType implementation
	
	@Override
	public void onSpawnedWith(EntityLivingBase entity, IBackpack backpack, String lootTable) {
		
		// If backpack is equipped in chestplate slot, set drop chance to 100%.
		if ((entity instanceof EntityLiving) &&
		    (entity.getItemStackFromSlot(EntityEquipmentSlot.CHEST) == backpack.getStack()))
			((EntityLiving)entity).setDropChance(EntityEquipmentSlot.CHEST, 1.0F);
		
		// Set backpack's loot table.
		IBackpackData data = backpack.getData();
		if ((lootTable != null) && (data instanceof BackpackDataItems))
			((BackpackDataItems)data).setLootTable(lootTable, entity.world.rand.nextLong());
		
	}
	
	@Override
	public void onEquip(EntityLivingBase entity, TileEntity tileEntity, IBackpack backpack) {  }
	
	@Override
	public void onUnequip(EntityLivingBase entity, TileEntity tileEntity, IBackpack backpack) {  }
	
	@Override
	public void onPlacedInteract(EntityPlayer player, TileEntity tileEntity, IBackpack backpack) {
		if (player.world.isRemote) return;
		new ContainerBackpack(player, backpack) {
			@Override public boolean canInteractWith(EntityPlayer player) {
				return player.isEntityAlive()
					&& !tileEntity.isInvalid()
					&& (player.world.getTileEntity(tileEntity.getPos()) == tileEntity)
					&& (player.getDistanceSq(tileEntity.getPos()) <= 64);
			}
		}.open();
	}
	
	@Override
	public void onEquippedInteract(EntityPlayer player, EntityLivingBase target, IBackpack backpack) {
		if (player.world.isRemote) return;
		new ContainerBackpack(player, backpack) {
			@Override public boolean canInteractWith(EntityPlayer player)
				{ return BackpackHelper.canInteractWithEquippedBackpack(player, target); }
		}.open();
	}
	
	@Override
	public void onEquippedTick(EntityLivingBase entity, IBackpack backpack) {  }
	
	@Override
	public void onDeath(EntityLivingBase entity, IBackpack backpack) {
		if (!(backpack.getData() instanceof BackpackDataItems)) return;
		BackpackDataItems dataItems = (BackpackDataItems)backpack.getData();
		WorldUtils.dropStacksFromEntity(entity, dataItems.getItems(entity.world, null), 4.0F);
	}
	
	@Override
	public void onEquippedBroken(EntityLivingBase entity, IBackpack backpack)
		{ onDeath(entity, backpack); }
	
	@Override
	public void onFaultyRemoval(EntityLivingBase entity, IBackpack backpack)
		{ onDeath(entity, backpack); }
	
	@Override
	public void onBlockBreak(TileEntity tileEntity, IBackpack backpack) {
		if (!(backpack.getData() instanceof BackpackDataItems)) return;
		BackpackDataItems dataItems = (BackpackDataItems)backpack.getData();
		WorldUtils.dropStacksFromBlock(tileEntity, dataItems.getItems(tileEntity.getWorld(), null));
	}
	
	@Override
	public IBackpackData createBackpackData(ItemStack stack) {
		BackpackSize size = WearableBackpacks.CONFIG.backpack.size.get();
		
		// Custom size can be specified in stack's NBT data.
		NBTBase customSize = NbtUtils.get(stack, TAG_CUSTOM_SIZE);
		if (customSize != null)
			try { size = BackpackSize.parse(customSize); }
			catch (Exception ex) { WearableBackpacks.LOG.error(
				"Error trying to deserialize backpack size from custom size NBT tag.", ex); }
		
		return new BackpackDataItems(size);
	}
	
	// ISpecialArmor implementation
	
	public ArmorProperties getProperties(EntityLivingBase player, ItemStack armor,
	                                     DamageSource source, double damage, int slot) {
		ArmorProperties prop = new ArmorProperties(0, 0.0, 0);
		ItemBackpack backpack = (ItemBackpack)armor.getItem();
		prop.Armor       = backpack.getArmorDamageReductionAmount(armor);
		prop.Toughness   = backpack.getArmorToughness(armor);
		prop.AbsorbRatio = prop.Armor / 25.0;
		prop.AbsorbMax   = Math.max(1, armor.getMaxDamage() - armor.getItemDamage() + 1);
		return prop;
	}
	
	public int getArmorDisplay(EntityPlayer player, ItemStack armor, int slot)
		{ return ((ItemBackpack)armor.getItem()).getArmorDamageReductionAmount(armor); }
	
	public void damageArmor(EntityLivingBase entity, ItemStack stack,
	                        DamageSource source, int damage, int slot) {
		stack.damageItem(damage, entity);
		if (!stack.isEmpty()) return;
		// If backpack breaks while equipped, call onEquippedBroken.
		IBackpack backpack = BackpackHelper.getBackpack(entity);
		if (backpack == null) return;
		backpack.getType().onEquippedBroken(entity, backpack);
	}
	
	// Can't really use attribute modifiers and ISpecialArmor together T_T
	// All we need from ISpecialArmor is damageArmor. A similar damageItem method on
	// Item which includes the entity that is wearing the backpack would work, too.
	/*
	@Override
	public Multimap<String, AttributeModifier> getAttributeModifiers(EntityEquipmentSlot slot, ItemStack stack) {
		Multimap<String, AttributeModifier> modifiers = HashMultimap.create();
		boolean equipAsChestArmor = WearableBackpacks.CONFIG.equipAsChestArmor.get();
		if (equipAsChestArmor && (slot == EntityEquipmentSlot.CHEST)) {
			ItemBackpack backpack = (ItemBackpack)stack.getItem();
			modifiers.put(SharedMonsterAttributes.ARMOR.getName(), new AttributeModifier(
				UUID.fromString("E8B05077-6027-49D5-A895-9E37D20E45DC"),
				"Armor modifier", backpack.getArmorDamageReductionAmount(stack), 0));
			modifiers.put(SharedMonsterAttributes.ARMOR_TOUGHNESS.getName(), new AttributeModifier(
				UUID.fromString("5131DA7B-E863-4E7F-B9A1-493F72A55DE2"),
				"Armor toughness", backpack.getArmorToughness(stack), 0));
		}
		return modifiers;
	}
	*/
	
	// When changing the maximum backpack durability in WBs' config to 0,
	// any already damaged backpacks would simply turn invalid due to their
	// damage value being above zero.
	// 
	// This little hack makes it so when durability is set to 0, it fakes
	// the durability to be very high, instead. And any attempt to get or
	// set damage will simply return or set it to 0.
	
	// TODO: This won't work with multiple backpack types. Somehow associate item with backpack category?
	
	@Override
	public boolean isDamageable()
		{ return !isInvulnurable(); }
	@Override
	public boolean isDamaged(ItemStack stack)
		{ return isInvulnurable() ? false : super.isDamaged(stack); }
	
	@Override
	public int getMaxDamage(ItemStack stack)
		{ return isInvulnurable() ? Short.MAX_VALUE : super.getMaxDamage(stack); }
	@Override
	public int getDamage(ItemStack stack)
		{ return isInvulnurable() ? 0 : super.getDamage(stack); }
	@Override
	public void setDamage(ItemStack stack, int damage) {
		if (isInvulnurable()) damage = 0;
		super.setDamage(stack, damage);
	}
	
	/** Returns if this backpack can't be damaged. */
	private boolean isInvulnurable()
		{ return (WearableBackpacks.CONFIG.backpack.durability.get() == 0); }
	
} public final class MiscUtils {

	private MiscUtils() {  }


	/** Returns the block associated with the specified resource location. */
	public static Block getBlockFromName(ResourceLocation location) { return Block.REGISTRY.getObject(location); }
	/** Returns the block associated with the specified resource location name (for example "minecraft:cobblestone"). */
	public static Block getBlockFromName(String name) { return getBlockFromName(new ResourceLocation(name)); }
	/** Returns the block associated with the specified item. */
	public static Block getBlockFromItem(Item item) { return getBlockFromName(item.getRegistryName()); }

	/** Returns the item associated with the specified resource location. */
	public static Item getItemFromName(ResourceLocation location) { return Item.REGISTRY.getObject(location); }
	/** Returns the item associated with the specified resource location name (for example "minecraft:stick"). */
	public static Item getItemFromName(String name) { return getItemFromName(new ResourceLocation(name)); }
	/** Returns the item associated with the specified block. */
	public static Item getItemFromBlock(Block block) { return Item.REGISTRY.getObject(block.getRegistryName()); }

	public static boolean shouldKeepItem(Entity entity, ItemStack stack) {
		EntityPlayer player = ((entity instanceof EntityPlayer) ? (EntityPlayer)entity : null);
		boolean keepInventory = entity.getEntityWorld().getGameRules().getBoolean("keepInventory");
		if ((player != null) && keepInventory) return true;

		// Ender IO: Check for Soulbound
		// (at least when you stop rearranging items, that is)

		/* Enchantment soulboundEnchantment = ForgeRegistries.ENCHANTMENTS.getValue(new ResourceLocation("enderio:soulbound"));
		if (soulboundEnchantment != null && EnchantmentHelper.getEnchantmentLevel(soulboundEnchantment, stack) > 0) return true; */

		return false;
	}
} public final class NbtUtils {
	
	private NbtUtils() {  }
	
	
	// TODO: Update to use enum instead and make methods "safer" by checking the type before casting.
	public static final class NbtType {
		private NbtType() {  }
		public static final int END = 0;
		public static final int BYTE = 1;
		public static final int SHORT = 2;
		public static final int INT = 3;
		public static final int LONG = 4;
		public static final int FLOAT = 5;
		public static final int DOUBLE = 6;
		public static final int BYTE_ARRAY = 7;
		public static final int STRING = 8;
		public static final int LIST = 9;
		public static final int COMPOUND = 10;
		public static final int INT_ARRAY = 11;
	}
	
	
	public static final String TAG_INDEX = "index";
	public static final String TAG_STACK = "stack";
	
	
	// Utility ItemStack / NBT manipulation methods
	
	/** Gets an NBT tag of the specified item stack, or null if it doesn't exist.
	 *  Example: <pre>{@code StackUtils.get(stack, "display", "color") }</pre> */
	public static NBTBase get(ItemStack stack, String... tags) {
		return get(stack.getTagCompound(), tags);
	}
	/** Gets a child NBT tag of the specified compound tag, or null if it doesn't exist.
	 *  Example: <pre>{@code StackUtils.get(compound, "display", "color") }</pre> */
	public static NBTBase get(NBTTagCompound compound, String... tags) {
		if (compound == null) return null;
		String tag = null;
		for (int i = 0; i < tags.length; i++) {
			tag = tags[i];
			if (!compound.hasKey(tag)) return null;
			if (i == tags.length - 1) break;
			compound = compound.getCompoundTag(tag);
		}
		return compound.getTag(tag);
	}
	
	/** Gets a value from the specified item stack's compound tag, or the default if it doesn't exist.
	 *  Example: <pre>{@code StackUtils.get(stack, -1, "display", "color") }</pre> */
	public static <T> T get(ItemStack stack, T defaultValue, String... tags) {
		return get(stack.getTagCompound(), defaultValue, tags);
	}
	/** Gets a value from the specified compound tag, or the default if it doesn't exist.
	 *  Example: <pre>{@code StackUtils.get(compound, -1, "display", "color") }</pre> */
	public static <T> T get(NBTTagCompound compound, T defaultValue, String... tags) {
		NBTBase tag = get(compound, tags);
		return ((tag != null) ? getTagValue(tag) : defaultValue);
	}
	
	/** Returns if the specified item stack's compound tag has a certain NBT tag.
	 *  Example: <pre>{@code StackUtils.has(stack, "display", "color") }</pre> */
	public static boolean has(ItemStack stack, String... tags) {
		return has(stack.getTagCompound(), tags);
	}
	/** Returns if the specified compound tag has a certain child NBT tag.
	 *  Example: <pre>{@code StackUtils.has(compound, "display", "color") }</pre> */
	public static boolean has(NBTTagCompound compound, String... tags) {
		return (get(compound, tags) != null);
	}
	
	/** Adds or replaces a tag on the specified item stack's compound tag, creating it and any parent compound tags if necessary.
	 *  Example: <pre>{@code StackUtils.set(stack, new NBTTagInt(0xFF0000), "display", "color") }</pre> */
	public static void set(ItemStack stack, NBTBase nbtTag, String... tags) {
		if (stack.isEmpty())
			throw new IllegalArgumentException("stack is empty");
		NBTTagCompound compound = stack.getTagCompound();
		if (compound == null) stack.setTagCompound(compound = new NBTTagCompound());
		set(compound, nbtTag, tags);
	}
	/** Adds or replaces a tag on the specified compound tag, creating any parent compound tags if necessary.
	 *  Example: <pre>{@code StackUtils.set(compound, new NBTTagInt(0xFF0000), "display", "color") }</pre> */
	public static void set(NBTTagCompound compound, NBTBase nbtTag, String... tags) {
		if (compound == null)
			throw new IllegalArgumentException("compound is null");
		String tag = null;
		for (int i = 0; i < tags.length; i++) {
			tag = tags[i];
			if (i == tags.length - 1) break;
			if (!compound.hasKey(tag)) {
				NBTTagCompound child = new NBTTagCompound();
				compound.setTag(tag, child);
				compound = child;
			} else compound = compound.getCompoundTag(tag);
		}
		compound.setTag(tag, nbtTag);
	}
	
	/** Adds or replaces a value on the specified item stack's compound tag, creating it and any parent compound tags if necessary.
	 *  Example: <pre>{@code StackUtils.set(stack, 0xFF0000, "display", "color") }</pre> */
	public static <T> void set(ItemStack stack, T value, String... tags) {
		set(stack, createTag(value), tags);
	}
	/** Adds or replaces a value on the specified compound tag, creating any parent compound tags if necessary.
	 *  Example: <pre>{@code StackUtils.set(compound, 0xFF0000, "display", "color") }</pre> */
	public static <T> void set(NBTTagCompound compound, T value, String... tags) {
		set(compound, createTag(value), tags);
	}
	
	/** Removes a certain NBT tag from the specified item stack's compound tag.
	 *  Example: <pre>{@code StackUtils.remove(stack, "display", "color") }</pre> */
	public static void remove(ItemStack stack, String... tags) {
		if (tags.length == 0) throw new IllegalArgumentException(
			"tags should have at least one element");
		if (!stack.hasTagCompound()) return;
		
		NBTTagCompound compound = stack.getTagCompound();
		remove(compound, tags);
		// If compound is empty, remove it from the stack.
		if (compound.isEmpty())
			stack.setTagCompound(null);
	}
	/** Removes a certain NBT tag from the specified compound tag.
	 *  Example: <pre>{@code StackUtils.remove(compound, "display", "color") }</pre> */
	public static void remove(NBTTagCompound compound, String... tags) {
		if (tags.length == 0) throw new IllegalArgumentException(
			"tags should have at least one element");
		if (compound == null) return;
		
		if (tags.length > 1) {
			NBTBase tag = compound.getTag(tags[0]);
			if (!(tag instanceof NBTTagCompound)) return;
			NBTTagCompound subCompound = (NBTTagCompound)tag;
			remove(subCompound, (String[])Arrays.copyOfRange(tags, 1, tags.length));
			// If subCompound is empty, remove it from the parent compound.
			if (!subCompound.isEmpty()) return;
		}
		compound.removeTag(tags[0]);
	}
	
	
	// Compound / List creation
	
	/** Creates an NBT compound from the name-value pairs in the parameters.
	 *  Doesn't add any null values to the resulting compound tag.
	 *  Example: <pre>{@code NbtUtils.createCompound("id", 1, "name", "copygirl") }</pre> */
	public static NBTTagCompound createCompound(Object... nameValuePairs) {
		return addToCompound(new NBTTagCompound(), nameValuePairs);
	}
	/** Adds entries to an ItemStack's NBT data from the name-value pairs in the parameters.
	 *  Doesn't add any null values to the ItemStack's compound tag.
	 *  Example: <pre>{@code NbtUtils.createCompound("id", 1, "name", "copygirl") }</pre> */
	public static NBTTagCompound add(ItemStack stack, Object... nameValuePairs) {
		if (stack.isEmpty()) throw new IllegalArgumentException("stack is empty");
		if (!stack.hasTagCompound()) stack.setTagCompound(new NBTTagCompound());
		return addToCompound(stack.getTagCompound(), nameValuePairs);
	}
	/** Adds entries to an NBT compound from the name-value pairs in the parameters.
	 *  Doesn't add any null values to the specified compound tag.
	 *  Example: <pre>{@code NbtUtils.addToCompound(compound, "id", 1, "name", "copygirl") }</pre> */
	public static NBTTagCompound addToCompound(NBTTagCompound compound, Object... nameValuePairs) {
		if (compound == null) throw new IllegalArgumentException("compound is null");
		for (int i = 0; i < nameValuePairs.length; i += 2) {
			String name = (String)nameValuePairs[i];
			Object value = nameValuePairs[i + 1];
			if (value == null) continue;
			compound.setTag(name, createTag(value));
		}
		return compound;
	}
	
	/** Creates an NBT list with the values, all of the single type.
	 *  Doesn't add any null values to the resulting list tag. */
	public static NBTTagList createList(Object... values) {
		return addToList(new NBTTagList(), values);
	}
	/** Adds values to an NBT list. Doesn't add any null values to the specified list tag. */
	public static NBTTagList addToList(NBTTagList list, Object... values) {
		if (list == null) throw new IllegalArgumentException("list is null");
		for (Object value : values) {
			if (value == null) continue;
			list.appendTag(createTag(value));
		}
		return list;
	}
	
	
	// Reading / writing ItemStacks
	
	/** Writes an item stack to an NBT compound. */
	public static NBTTagCompound writeItem(ItemStack item) {
		return writeItem(item, true);
	}
	/** Writes an item stack to an NBT compound. */
	public static NBTTagCompound writeItem(ItemStack item, boolean writeNullAsEmptyCompound) {
		return (!item.isEmpty()) ? item.writeToNBT(new NBTTagCompound())
			: (writeNullAsEmptyCompound ? new NBTTagCompound() : null);
	}
	
	/** Reads an item stack from an NBT compound. */
	public static ItemStack readItem(NBTTagCompound compound) {
		return ((compound != null) && !compound.isEmpty())
				? new ItemStack(compound) : ItemStack.EMPTY;
	}
	
	
	/** Writes an item stack array to an NBT list. */
	public static NBTTagList writeItems(ItemStack[] items) {
		NBTTagList list = new NBTTagList();
		for (int i = 0; i < items.length; i++)
			if (items[i] != null)
				list.appendTag(createCompound(
						TAG_INDEX, (short)i,
						TAG_STACK, writeItem(items[i])));
		return list;
	}
	
	/** Reads items from an NBT list to an item stack array. */
	public static ItemStack[] readItems(NBTTagList list, ItemStack[] items) {
		return readItems(list, items, null);
	}
	/** Reads items from an NBT list to an item stack array.
	 *  Any items falling outside the range of the items array
	 *  will get added to the invalid list if that's non-null. */
	public static ItemStack[] readItems(NBTTagList list, ItemStack[] items, List<ItemStack> invalid) {
		for (int i = 0; i < list.tagCount(); i++) {
			NBTTagCompound compound = list.getCompoundTagAt(i);
			int index = compound.getShort(TAG_INDEX);
			ItemStack stack = readItem(compound.getCompoundTag(TAG_STACK));
			if ((index >= 0) || (index < items.length))
				items[index] = stack;
			else if (invalid != null)
				invalid.add(stack);
		}
		return items;
	}
	
	
	// Other utility functions
	
	/** Returns the primitive value of a tag, casted to the return type. */
	@SuppressWarnings("unchecked")
	public static <T> T getTagValue(NBTBase tag) {
		if (tag == null) throw new IllegalArgumentException("tag is null");
		
		if (tag instanceof NBTTagByte)      return (T)(Object)((NBTTagByte)tag).getByte();
		if (tag instanceof NBTTagShort)     return (T)(Object)((NBTTagShort)tag).getShort();
		if (tag instanceof NBTTagInt)       return (T)(Object)((NBTTagInt)tag).getInt();
		if (tag instanceof NBTTagLong)      return (T)(Object)((NBTTagLong)tag).getLong();
		if (tag instanceof NBTTagFloat)     return (T)(Object)((NBTTagFloat)tag).getFloat();
		if (tag instanceof NBTTagDouble)    return (T)(Object)((NBTTagDouble)tag).getDouble();
		if (tag instanceof NBTTagString)    return (T)(Object)((NBTTagString)tag).getString();
		if (tag instanceof NBTTagByteArray) return (T)((NBTTagByteArray)tag).getByteArray();
		if (tag instanceof NBTTagIntArray)  return (T)((NBTTagIntArray)tag).getIntArray();
		
		throw new IllegalArgumentException(NBTBase.NBT_TYPES[tag.getId()] + " isn't a primitive NBT tag");
	}
	
	/** Creates and returns a primitive NBT tag from a value.
	 *  If the value already is an NBT tag, it is returned instead. */
	public static NBTBase createTag(Object value) {
		if (value == null)
			throw new IllegalArgumentException("value is null");
		
		if (value instanceof NBTBase) return (NBTBase)value;
		if (value instanceof INBTSerializable)
			return ((INBTSerializable<?>)value).serializeNBT();
		if (value instanceof Collection) return ((Collection<?>)value).stream()
			.map(NbtUtils::createTag).collect(toList());
		
		if (value instanceof Byte)    return new NBTTagByte((Byte)value);
		if (value instanceof Short)   return new NBTTagShort((Short)value);
		if (value instanceof Integer) return new NBTTagInt((Integer)value);
		if (value instanceof Long)    return new NBTTagLong((Long)value);
		if (value instanceof Float)   return new NBTTagFloat((Float)value);
		if (value instanceof Double)  return new NBTTagDouble((Double)value);
		if (value instanceof String)  return new NBTTagString((String)value);
		if (value instanceof byte[])  return new NBTTagByteArray((byte[])value);
		if (value instanceof int[])   return new NBTTagIntArray((int[])value);
		
		throw new IllegalArgumentException("Can't create an NBT tag of value: " + value);
	}
	
	
	/** Returns the specified NBT serializable value
	 *  instance deserialized from the specified NBT tag. */
	public static <N extends NBTBase, T extends INBTSerializable<N>> T getTagValue(N tag, T value) {
		if (tag == null) throw new IllegalArgumentException("tag is null");
		if (value == null) throw new IllegalArgumentException("value is null");
		value.deserializeNBT(tag);
		return value;
	}
	
	/** Returns a list of NBT serializable values instantiated
	 *  using the value supplier from the specified NBT list. */
	@SuppressWarnings("unchecked")
	public static <N extends NBTBase, T extends INBTSerializable<N>> List<T> getTagList(
		NBTTagList list, Supplier<T> valueSupplier) {
		return stream(list)
			.map(tag -> getTagValue((N)tag, valueSupplier.get()))
			.collect(Collectors.toList());
	}
	
	
	// Iterable / Stream related functions
	
	/** Returns an iterable of NBT tags in the specified NBT list. */
	@SuppressWarnings("unchecked")
	public static <T extends NBTBase> Iterable<T> iterate(NBTTagList list) {
		return new Iterable<T>() {
			@Override public Iterator<T> iterator() {
				return new Iterator<T>() {
					private int _index = 0;
					@Override public boolean hasNext() { return (_index < list.tagCount()); }
					@Override public T next() { return (T)list.get(_index++); }
				};
			}
		};
	}
	/** Returns an iterable of entries in the specified NBT compound. */
	public static Iterable<CompoundEntry> iterate(NBTTagCompound compound) {
		return Iterables.transform(compound.getKeySet(),
			key -> new CompoundEntry(key, compound.getTag(key)));
	}
	
	/** Returns a stream of NBT tags in the specified NBT list. */
	public static <T extends NBTBase> Stream<T> stream(NBTTagList list) {
		return StreamSupport.stream(NbtUtils.<T>iterate(list).spliterator(), false);
	}
	/** Returns a stream of entries in the specified NBT compound. */
	public static Stream<CompoundEntry> stream(NBTTagCompound compound) {
		return StreamSupport.stream(NbtUtils.iterate(compound).spliterator(), false);
	}
	
	/** Returns a collector that accumulates the the input elements into a new NBT list. */
	public static <T> Collector<T, NBTTagList, NBTTagList> toList() {
		return Collector.of(NBTTagList::new,
			(list, element) ->
				list.appendTag(createTag(element)),
			(left, right) -> {
				for (NBTBase tag : iterate(right))
					left.appendTag(tag);
				return left;
			}, Characteristics.IDENTITY_FINISH);
	}
	
	/** Returns a collector that accumulates the the input NBT tags into a new NBT list. */
	public static <T> Collector<T, NBTTagCompound, NBTTagCompound> toCompound(
		Function<T, String> keyMapper, Function<T, NBTBase> tagMapper) {
		return Collector.of(NBTTagCompound::new,
			(compound, element) ->
				compound.setTag(keyMapper.apply(element), tagMapper.apply(element)),
			(left, right) -> {
				for (String key : right.getKeySet())
					left.setTag(key, right.getTag(key));
				return left;
			}, Characteristics.IDENTITY_FINISH);
	}
	
	public static class CompoundEntry {
		public final String key;
		public final NBTBase tag;
		public CompoundEntry(String key, NBTBase tag)
			{ this.key = key; this.tag = tag; }
	}
	
} public final class ReflectUtils {
	
	private ReflectUtils() {  }
	
	
	// Fields
	
	private static Map<Pair<Class<?>, String>, Field> _cachedFields =
		new HashMap<Pair<Class<?>, String>, Field>();
	
	/** Returns a field on the specified class with the specified SRG
	 *  or MCP names. The field is cached, speeding up future calls. */
	public static Field findField(Class<?> clazz, String mcpName, String srgName) {
		return _cachedFields.computeIfAbsent(Pair.of(clazz, mcpName), key -> {
			Field field = null;
			for (String name : new String[]{ srgName, mcpName }) {
				// Attempt to find the field on the class.
				try { field = clazz.getDeclaredField(name); }
				catch (NoSuchFieldException ex) {  }
				// If not found, try next name or exit the loop.
				if (field == null) continue;
				// Set field to be accessible, allowing its value to be get/set.
				try { field.setAccessible(true); }
				catch (SecurityException ex) { throw new RuntimeException(ex); }
				return field;
			}
			throw new RuntimeException(String.format("Could not find field '%s'/'%s' for class %s",
			                                         mcpName, srgName, clazz.getName()));
		});
	}
	
	/** Gets the value of the field with the specified SRG or MCP names
	 *  on the specified class and instance (may be null if static). */
	@SuppressWarnings("unchecked")
	public static <T, V> V get(Class<? extends T> clazz, T instance, String mcpName, String srgName) {
		try { return (V)findField(clazz, mcpName, srgName).get(instance); }
		catch (ReflectiveOperationException ex) { throw new RuntimeException(ex); }
	}
	/** Gets the value of the static field with the specified SRG or MCP names on the specified class. */
	public static <T, V> V get(Class<T> clazz, String mcpName, String srgName) {
		return get(clazz, null, mcpName, srgName);
	}
	/** Gets the value of the field with the specified SRG or MCP names on the specified instance. */
	public static <T, V> V get(T instance, String mcpName, String srgName) {
		return get(instance.getClass(), instance, mcpName, srgName);
	}
	
	/** Sets the value of the field with the specified SRG or MCP names
	 *  on the specified class and instance (may be null if static). */
	public static <T, V> void set(Class<? extends T> clazz, T instance, String mcpName, String srgName, V value) {
		try { findField(clazz, mcpName, srgName).set(instance, value); }
		catch (ReflectiveOperationException ex) { throw new RuntimeException(ex); }
	}
	/** Sets the value of the static field with the specified SRG or MCP names on the specified class. */
	public static <T, V> void set(Class<T> clazz, String mcpName, String srgName, V value) {
		set(clazz, null, mcpName, srgName, value);
	}
	/** Sets the value of the field with the specified SRG or MCP names on the specified instance. */
	public static <T, V> void set(T instance, String mcpName, String srgName, V value) {
		set(instance.getClass(), instance, mcpName, srgName, value);
	}
	
	
	// Methods
	
	private static Map<Triple<Class<?>, String, Class<?>[]>, Method> _cachedMethods =
		new HashMap<Triple<Class<?>, String, Class<?>[]>, Method>();
	
	/** Returns a method on the specified class with the specified SRG
	 *  or MCP names. The method is cached, speeding up future calls. */
	public static Method findMethod(Class<?> clazz, String mcpName, String srgName, Class<?>... parameterTypes) {
		return _cachedMethods.computeIfAbsent(Triple.of(clazz, mcpName, parameterTypes), key -> {
			Method method;
			for (String name : new String[]{ srgName, mcpName }) {
				// Attempt to find the method on the class.
				try { method = clazz.getDeclaredMethod(name, parameterTypes); }
				catch (NoSuchMethodException ex) { continue; }
				// If not found, try next name or exit the loop.
				if (method == null) continue;
				// Set method to be accessible, allowing it to be called.
				try { method.setAccessible(true); }
				catch (SecurityException ex) { throw new RuntimeException(ex); }
				return method;
			}
			throw new RuntimeException(String.format("Could not find method '%s'/'%s' for class %s",
			                                         mcpName, srgName, clazz.getName()));
		});
	}
	
	// TODO: Add invoke helper methods?
	
}
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.IBakedModel;
import net.minecraft.client.renderer.block.model.ModelResourceLocation;
import net.minecraft.client.renderer.color.IItemColor;
import net.minecraft.client.renderer.color.IBlockColor;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderLivingBase;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EnumPlayerModelParts;
import net.minecraft.item.ItemStack;
import net.minecraft.network.datasync.DataParameter;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraftforge.client.event.ModelBakeEvent;
import net.minecraftforge.client.event.ModelRegistryEvent;
import net.minecraftforge.client.event.RenderPlayerEvent;
import net.minecraftforge.client.model.IModel;
import net.minecraftforge.client.model.ModelLoader;
import net.minecraftforge.client.model.ModelLoaderRegistry;
import net.minecraftforge.client.model.pipeline.IVertexConsumer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.client.registry.ClientRegistry;
import net.minecraftforge.fml.common.eventhandler.EventPriority;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import net.mcft.copy.backpacks.BackpacksContent;
import net.mcft.copy.backpacks.WearableBackpacks;
import net.mcft.copy.backpacks.api.BackpackHelper;
import net.mcft.copy.backpacks.api.BackpackRegistry;
import net.mcft.copy.backpacks.api.IBackpack;
import net.mcft.copy.backpacks.api.BackpackRegistry.BackpackEntityEntry;
import net.mcft.copy.backpacks.ProxyCommon;
import net.mcft.copy.backpacks.block.entity.TileEntityBackpack;
import net.mcft.copy.backpacks.client.BakedModelDefaultTexture;
import net.mcft.copy.backpacks.client.KeyBindingHandler;
import net.mcft.copy.backpacks.client.RendererBackpack;
import net.mcft.copy.backpacks.item.ItemBackpack;
import net.mcft.copy.backpacks.misc.util.MiscUtils;
import net.mcft.copy.backpacks.misc.util.NbtUtils;
import net.mcft.copy.backpacks.misc.util.ReflectUtils;
package net.mcft.copy.backpacks;





@SideOnly(Side.CLIENT)
public class ProxyClient extends ProxyCommon {
	
	public static IBakedModel MODEL_BACKPACK;
	public static IBakedModel MODEL_BACKPACK_TOP;
	public static IBakedModel MODEL_BACKPACK_STRAPS;
	
	public static IBakedModel MODEL_BACKPACK_ENCH;
	public static IBakedModel MODEL_BACKPACK_ENCH_TOP;
	
	public static AxisAlignedBB MODEL_BACKPACK_BOX;
	
	@Override
	public void preInit() {
		super.preInit();
		MinecraftForge.EVENT_BUS.register(new KeyBindingHandler());
	}
	
	@Override
	public void init() {
		super.init();
		Minecraft mc = Minecraft.getMinecraft();
		
		if (BackpacksContent.BACKPACK != null) {
			mc.getBlockColors().registerBlockColorHandler(BLOCK_COLOR, MiscUtils.getBlockFromItem(BackpacksContent.BACKPACK));
			mc.getItemColors().registerItemColorHandler(ITEM_COLOR, BackpacksContent.BACKPACK);
			ClientRegistry.bindTileEntitySpecialRenderer(TileEntityBackpack.class, new RendererBackpack.TileEntity());