java/squeek/spiceoflife/foodtracker/foodgroups/FoodGroupRegistry.java
public class FoodGroupRegistry
{
	private static Map<String, FoodGroup> foodGroups = new HashMap<String, FoodGroup>();
	/**
	 * Does not deal with food group exclusions; that must be handled separately
	 * See {@link #getFoodGroupsForFood(ItemStack)}
	 */
	private static Map<Integer, Set<FoodGroup>> foodToIncludedFoodGroups = new HashMap<Integer, Set<FoodGroup>>();
	private static boolean hasBlacklist = false;

	public static FoodGroup getFoodGroup(String identifier)
	{
		return foodGroups.get(identifier);
	}

	public static Collection<FoodGroup> getFoodGroups()
	{
		return foodGroups.values();
	}

	public static int numFoodGroups()
	{
		return getFoodGroups().size();
	}

	public static void addFoodGroup(FoodGroup foodGroup)
	{
		foodGroups.put(foodGroup.identifier, foodGroup);

		if (foodGroup.blacklist)
			hasBlacklist = true;
	}

	public static boolean foodGroupExists(String identifier)
	{
		return foodGroups.containsKey(identifier);
	}

	public static Set<FoodGroup> getFoodGroupsForFood(@Nonnull ItemStack food)
	{
		Set<FoodGroup> wildCardFoodGroups = foodToIncludedFoodGroups.get(OreDictionaryHelper.getWildCardItemStackHash(food));
		Set<FoodGroup> exactFoodGroups = foodToIncludedFoodGroups.get(OreDictionaryHelper.getItemStackHash(food));
		Set<FoodGroup> allFoodGroups = new HashSet<FoodGroup>();

		if (wildCardFoodGroups != null)
		{
			for (FoodGroup foodGroup : wildCardFoodGroups)
			{
				if (!foodGroup.isFoodExcluded(food))
					allFoodGroups.add(foodGroup);
			}
		}
		if (exactFoodGroups != null)
		{
			for (FoodGroup foodGroup : exactFoodGroups)
			{
				if (!foodGroup.isFoodExcluded(food))
					allFoodGroups.add(foodGroup);
			}
		}

		return allFoodGroups;
	}

	private static boolean isAnyFoodGroupBlacklist(Collection<FoodGroup> foodGroups)
	{
		for (FoodGroup foodGroup : foodGroups)
		{
			if (foodGroup.blacklist)
				return true;
		}
		return false;
	}

	public static boolean isFoodBlacklisted(@Nonnull ItemStack food)
	{
		if (!hasBlacklist && !ModConfig.USE_FOOD_GROUPS_AS_WHITELISTS)
			return false;

		Set<FoodGroup> foodGroups = getFoodGroupsForFood(food);
		boolean isInAnyFoodGroups = !foodGroups.isEmpty();
		boolean isInBlacklistFoodGroup = isInAnyFoodGroups && isAnyFoodGroupBlacklist(foodGroups);
		return (ModConfig.USE_FOOD_GROUPS_AS_WHITELISTS && !isInAnyFoodGroups) || isInBlacklistFoodGroup;
	}

	public static void sync(EntityPlayerMP player)
	{
		for (FoodGroup foodGroup : foodGroups.values())
		{
			PacketDispatcher.get().sendTo(new PacketFoodGroup(foodGroup), player);
		}
	}

	public static void setInStone()
	{
		foodToIncludedFoodGroups.clear();

		for (FoodGroup foodGroup : getFoodGroups())
		{
			foodGroup.init();
			for (Integer itemHash : foodGroup.getMatchingItemStackHashes())
			{
				if (foodToIncludedFoodGroups.get(itemHash) == null)
				{
					foodToIncludedFoodGroups.put(itemHash, new HashSet<FoodGroup>());
				}
				foodToIncludedFoodGroups.get(itemHash).add(foodGroup);
			}
		}
	}

	public static void clear()
	{
		foodGroups.clear();
		foodToIncludedFoodGroups.clear();
		hasBlacklist = false;
		PacketFoodGroup.resetCount();
	}
}
java/squeek/spiceoflife/compat/PacketDispatcher.java
public class PacketDispatcher
{
	public static IPacketDispatcher get()
	{
		return new PacketDispatcherNetty();
	}

	// based on FML's NetworkRegistry.TargetPoint
	public static class PacketTarget
	{
		public PacketTarget(int dimension, double x, double y, double z, double range)
		{
			this.x = x;
			this.y = y;
			this.z = z;
			this.range = range;
			this.dimension = dimension;
		}

		public final double x;
		public final double y;
		public final double z;
		public final double range;
		public final int dimension;
	}
}
java/squeek/spiceoflife/foodtracker/FoodModifier.java
public class FoodModifier
{
	public static final FoodModifier GLOBAL = new FoodModifier();

	@SubscribeEvent(priority = EventPriority.LOWEST)
	public void getFoodValues(FoodEvent.GetPlayerFoodValues event)
	{
		if (ModConfig.FOOD_MODIFIER_ENABLED)
		{
			ItemStack actualFood = event.food;
			if (FoodHelper.isFoodContainer(event.food))
			{
				actualFood = ((ItemFoodContainer) event.food.getItem()).getBestFoodForPlayerToEat(event.food, event.player);
			}

			float modifier = FoodModifier.getFoodModifier(event.player, actualFood);

			event.foodValues = FoodModifier.getModifiedFoodValues(event.foodValues, modifier);
		}
	}

	@SubscribeEvent(priority = EventPriority.LOWEST)
	public void getFoodEatingSpeed(LivingEntityUseItemEvent.Start event)
	{
		if (!(event.getEntity() instanceof EntityPlayer))
			return;

		if (ModConfig.FOOD_EATING_SPEED_MODIFIER > 0 && AppleCoreAPI.accessor.isFood(event.getItem()))
		{
			EntityPlayer player = (EntityPlayer) event.getEntityLiving();
			ItemStack actualFood = event.getItem();
			if (FoodHelper.isFoodContainer(event.getItem()))
			{
				actualFood = ((ItemFoodContainer) event.getItem().getItem()).getBestFoodForPlayerToEat(event.getItem(), player);
			}

			float nutritionalValue = FoodModifier.getFoodModifier(player, actualFood);
			float denominator = (float) Math.pow(nutritionalValue, ModConfig.FOOD_EATING_SPEED_MODIFIER);

			if (denominator > 0)
				event.setDuration((int) (event.getDuration() / denominator));
			else
				event.setDuration(Short.MAX_VALUE);

			if (ModConfig.FOOD_EATING_DURATION_MAX > 0)
				event.setDuration(Math.min(event.getDuration(), ModConfig.FOOD_EATING_DURATION_MAX));
		}
	}

	public Expression expression;

	public FoodModifier()
	{
		this(ModConfig.FOOD_MODIFIER_FORMULA);
	}

	public FoodModifier(String formula)
	{
		setFormula(formula);
	}

	public void setFormula(String formula)
	{
		expression = new Expression(formula);
	}

	public static void onGlobalFormulaChanged()
	{
		FoodModifier.GLOBAL.setFormula(ModConfig.FOOD_MODIFIER_FORMULA);
	}

	public static float getFoodGroupModifier(FoodHistory foodHistory, @Nonnull ItemStack food, FoodGroup foodGroup)
	{
		FoodModifier effectiveFoodModifier = foodGroup != null ? foodGroup.getFoodModifier() : FoodModifier.GLOBAL;
		int count = foodHistory.getFoodCountForFoodGroup(food, foodGroup);
		int historySize = foodHistory.getHistoryLengthInRelevantUnits();
		FoodValues totalFoodValues = foodHistory.getTotalFoodValuesForFoodGroup(food, foodGroup);
		FoodValues foodValues = FoodValues.get(food);

		BigDecimal result = effectiveFoodModifier.expression.with("count", new BigDecimal(count))
			.and("cur_history_length", new BigDecimal(historySize))
			.and("food_hunger_value", new BigDecimal(foodValues.hunger))
			.and("food_saturation_mod", new BigDecimal(foodValues.saturationModifier))
			.and("cur_hunger", new BigDecimal(foodHistory.player.getFoodStats().getFoodLevel()))
			.and("cur_saturation", new BigDecimal(foodHistory.player.getFoodStats().getSaturationLevel()))
			.and("total_food_eaten", new BigDecimal(foodHistory.totalFoodsEatenAllTime))
			.and("max_history_length", new BigDecimal(ModConfig.FOOD_HISTORY_LENGTH))
			.and("hunger_count", new BigDecimal(totalFoodValues.hunger))
			.and("saturation_count", new BigDecimal(totalFoodValues.saturationModifier))
			.and("food_group_count", new BigDecimal(FoodGroupRegistry.getFoodGroupsForFood(food).size()))
			.and("distinct_food_groups_eaten", new BigDecimal(foodHistory.getDistinctFoodGroups().size()))
			.and("total_food_groups", new BigDecimal(FoodGroupRegistry.numFoodGroups()))
			.and("exact_count", new BigDecimal(foodHistory.getFoodCountIgnoringFoodGroups(food)))
			.eval();

		return result.floatValue();
	}

	public static float getFoodModifier(EntityPlayer player, @Nonnull ItemStack food)
	{
		return getFoodModifier(FoodHistory.get(player), food);
	}

	public static float getFoodModifier(FoodHistory foodHistory, @Nonnull ItemStack food)
	{
		if (!ModConfig.FOOD_MODIFIER_ENABLED)
			return 1f;

		if (!FoodHelper.canFoodDiminish(food))
			return 1f;

		if (ModConfig.FOOD_EATEN_THRESHOLD > 0 && foodHistory.totalFoodsEatenAllTime < ModConfig.FOOD_EATEN_THRESHOLD)
			return 1f;

		// if this food has multiple food groups, calculate the modifier for each individually
		// and then take the average
		// for foods with <= 1 food group, this just means dividing the modifier by 1
		Set<FoodGroup> foodGroupsSet = FoodGroupRegistry.getFoodGroupsForFood(food);
		FoodGroup[] foodGroups = foodGroupsSet.toArray(new FoodGroup[foodGroupsSet.size()]);
		int numIterations = Math.max(1, foodGroups.length);
		float modifierSum = 0f;

		for (int i = 0; i < numIterations; i++)
		{
			FoodGroup foodGroup = i < foodGroups.length ? foodGroups[i] : null;
			modifierSum += getFoodGroupModifier(foodHistory, food, foodGroup);
		}

		return modifierSum / numIterations;
	}

	public static FoodValues getModifiedFoodValues(FoodValues foodValues, float modifier)
	{
		int hunger = foodValues.hunger;
		float saturationModifier = foodValues.saturationModifier;

		if (ModConfig.AFFECT_FOOD_HUNGER_VALUES)
		{
			if (hunger < 0 && ModConfig.AFFECT_NEGATIVE_FOOD_HUNGER_VALUES)
				hunger = (int) ModConfig.FOOD_HUNGER_ROUNDING_MODE.round(hunger * (2 - modifier));
			else if (hunger > 0)
				hunger = (int) ModConfig.FOOD_HUNGER_ROUNDING_MODE.round(hunger * modifier);
		}

		if (ModConfig.AFFECT_FOOD_SATURATION_MODIFIERS)
		{
			if (saturationModifier < 0 && ModConfig.AFFECT_NEGATIVE_FOOD_SATURATION_MODIFIERS)
				saturationModifier *= 2 - modifier;
			else if (saturationModifier > 0)
				saturationModifier *= modifier;
		}

		return new FoodValues(hunger, saturationModifier);
	}
}
java/squeek/spiceoflife/interfaces/IPacketProcessor.java
public interface IPacketProcessor
{
	PacketBase processAndReply(Side side, EntityPlayer player);
	void processInWorldThread(Side side, EntityPlayer sidedPlayer);
}
java/squeek/spiceoflife/foodtracker/FoodHistory.java
public class FoodHistory implements IFoodHistory, ICapabilitySerializable<NBTTagCompound>
{
	public static final ResourceLocation CAPABILITY_ID = new ResourceLocation(ModInfo.MODID, "History");
	public static final String TAG_KEY = ModInfo.MODID + "History";

	@CapabilityInject(IFoodHistory.class)
	public static final Capability<IFoodHistory> CAPABILITY = null;

	public final EntityPlayer player;
	protected FoodQueue history = FoodHistory.getNewFoodQueue();
	public int totalFoodsEatenAllTime = 0;
	public boolean wasGivenFoodJournal = false;
	public long ticksActive = 0;

	public FoodHistory()
	{
		this(null);
	}

	public FoodHistory(EntityPlayer player)
	{
		this.player = player;
	}

	@Override
	public void onHistoryTypeChanged()
	{
		FoodQueue oldHistory = history;
		history = FoodHistory.getNewFoodQueue();
		history.addAll(oldHistory);
	}

	public static FoodQueue getNewFoodQueue()
	{
		if (ModConfig.USE_HUNGER_QUEUE)
			return new FixedHungerQueue(ModConfig.FOOD_HISTORY_LENGTH);
		else if (ModConfig.USE_TIME_QUEUE)
			return new FixedTimeQueue((long) ModConfig.FOOD_HISTORY_LENGTH * MiscHelper.TICKS_PER_DAY);
		else
			return new FixedSizeQueue(ModConfig.FOOD_HISTORY_LENGTH);
	}

	@Override
	public void deltaTicksActive(long delta)
	{
		this.ticksActive += delta;
	}

	@Override
	public boolean addFood(FoodEaten foodEaten)
	{
		return addFood(foodEaten, true);
	}

	@Override
	public boolean addFood(FoodEaten foodEaten, boolean countsTowardsAllTime)
	{
		if (countsTowardsAllTime)
			totalFoodsEatenAllTime++;

		boolean isAtThreshold = countsTowardsAllTime && totalFoodsEatenAllTime == ModConfig.FOOD_EATEN_THRESHOLD;
		if (player != null && !player.world.isRemote)
		{
			if (ModConfig.GIVE_FOOD_JOURNAL_ON_DIMINISHING_RETURNS && !wasGivenFoodJournal && isAtThreshold)
			{
				ItemFoodJournal.giveToPlayer(player);
				wasGivenFoodJournal = true;
			}
			if (ModConfig.CLEAR_HISTORY_ON_FOOD_EATEN_THRESHOLD && isAtThreshold)
			{
				history.clear();
				return true;
			}
		}
		return history.add(foodEaten);
	}

	@Override
	public int getFoodCountIgnoringFoodGroups(@Nonnull ItemStack food)
	{
		return getFoodCountForFoodGroup(food, null);
	}

	@Override
	public int getFoodCountForFoodGroup(@Nonnull ItemStack food, FoodGroup foodGroup)
	{
		int count = 0;

		for (FoodEaten foodEaten : history)
		{
			if (foodEaten.itemStack == ItemStack.EMPTY)
				continue;

			if (food.isItemEqual(foodEaten.itemStack) || foodEaten.getFoodGroups().contains(foodGroup))
			{
				count += 1;
			}
		}
		return count;
	}

	@Override
	public boolean containsFoodOrItsFoodGroups(@Nonnull ItemStack food)
	{
		Set<FoodGroup> foodGroups = FoodGroupRegistry.getFoodGroupsForFood(food);
		for (FoodEaten foodEaten : history)
		{
			if (foodEaten.itemStack == ItemStack.EMPTY)
				continue;

			if (food.isItemEqual(foodEaten.itemStack) || MiscHelper.collectionsOverlap(foodGroups, foodEaten.getFoodGroups()))
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * See {@link #getTotalFoodValuesForFoodGroup}
	 */
	@Override
	public FoodValues getTotalFoodValuesIgnoringFoodGroups(@Nonnull ItemStack food)
	{
		return getTotalFoodValuesForFoodGroup(food, null);
	}

	/**
	 * Note: the returned FoodValues is not a standard FoodValues.
	 * The saturationModifier is set to the total, not to a modifier
	 */
	@Override
	public FoodValues getTotalFoodValuesForFoodGroup(@Nonnull ItemStack food, FoodGroup foodGroup)
	{
		int totalHunger = 0;
		float totalSaturation = 0f;

		for (FoodEaten foodEaten : history)
		{
			if (foodEaten.itemStack == ItemStack.EMPTY)
				continue;

			if (food.isItemEqual(foodEaten.itemStack) || foodEaten.getFoodGroups().contains(foodGroup))
			{
				totalHunger += foodEaten.foodValues.hunger;
				totalSaturation += foodEaten.foodValues.getUnboundedSaturationIncrement();
			}
		}

		if (totalHunger == 0)
			return new FoodValues(0, 0f);
		else
			return new FoodValues(totalHunger, totalSaturation);
	}

	@Override
	public FoodQueue getHistory()
	{
		return history;
	}

	@Override
	public int getHistoryLengthInRelevantUnits()
	{
		return ModConfig.USE_HUNGER_QUEUE ? ((FixedHungerQueue) history).hunger() : history.size();
	}

	@Override
	public FoodEaten getLastEatenFood()
	{
		return history.peekLast();
	}

	@Override
	public Set<FoodGroup> getDistinctFoodGroups()
	{
		Set<FoodGroup> distinctFoodGroups = new HashSet<FoodGroup>();
		for (FoodEaten foodEaten : history)
		{
			if (foodEaten.itemStack == ItemStack.EMPTY)
				continue;

			distinctFoodGroups.addAll(foodEaten.getFoodGroups());
		}
		return distinctFoodGroups;
	}

	@Override
	public void reset()
	{
		history.clear();
		totalFoodsEatenAllTime = 0;
		wasGivenFoodJournal = false;
		ticksActive = 0;
	}

	@Override
	public void validate()
	{
		List<FoodEaten> invalidFoods = new ArrayList<FoodEaten>();
		for (FoodEaten foodEaten : history)
		{
			if (!FoodHelper.isValidFood(foodEaten.itemStack))
			{
				invalidFoods.add(foodEaten);
			}
		}
		history.removeAll(invalidFoods);
		totalFoodsEatenAllTime -= invalidFoods.size();
	}

	@Nonnull
	public static FoodHistory get(EntityPlayer player)
	{
		if (player.hasCapability(CAPABILITY, null))
		{
			FoodHistory history = (FoodHistory) player.getCapability(CAPABILITY, null);
			if (history != null)
				return history;
		}
		throw new RuntimeException("[The Spice Of Life] Food history capability not found for " + player.toString());
	}

	@Override
	public void pack(IByteIO data)
	{
		data.writeLong(ticksActive);
		data.writeShort(getHistory().size());

		for (FoodEaten foodEaten : getHistory())
		{
			foodEaten.pack(data);
		}
	}

	@Override
	public void unpack(IByteIO data)
	{
		ticksActive = data.readLong();
		short historySize = data.readShort();

		for (int i = 0; i < historySize; i++)
		{
			FoodEaten foodEaten = new FoodEaten();
			foodEaten.unpack(data);
			addFood(foodEaten);
		}
	}

	@Override
	// null compound parameter means save persistent data only
	public void writeToNBTData(NBTTagCompound data)
	{
		NBTTagCompound rootPersistentCompound = player.getEntityData().getCompoundTag(EntityPlayer.PERSISTED_NBT_TAG);
		NBTTagCompound nonPersistentCompound = new NBTTagCompound();
		NBTTagCompound persistentCompound = new NBTTagCompound();

		if (!history.isEmpty())
		{
			if (data != null || ModConfig.FOOD_HISTORY_PERSISTS_THROUGH_DEATH)
			{
				NBTTagCompound nbtHistory = new NBTTagCompound();

				history.writeToNBTData(nbtHistory);

				if (ModConfig.FOOD_HISTORY_PERSISTS_THROUGH_DEATH)
					persistentCompound.setTag("History", nbtHistory);
				else
					nonPersistentCompound.setTag("History", nbtHistory);
			}
		}
		if (totalFoodsEatenAllTime > 0)
		{
			persistentCompound.setInteger("Total", totalFoodsEatenAllTime);
		}
		if (wasGivenFoodJournal)
		{
			persistentCompound.setBoolean("FoodJournal", wasGivenFoodJournal);
		}
		if (ticksActive > 0)
		{
			persistentCompound.setLong("Ticks", ticksActive);
		}

		if (data != null && !nonPersistentCompound.hasNoTags())
			data.setTag(TAG_KEY, nonPersistentCompound);

		if (!persistentCompound.hasNoTags())
			rootPersistentCompound.setTag(TAG_KEY, persistentCompound);

		if (!player.getEntityData().hasKey(EntityPlayer.PERSISTED_NBT_TAG))
			player.getEntityData().setTag(EntityPlayer.PERSISTED_NBT_TAG, rootPersistentCompound);
	}

	@Override
	// null compound parameter means load persistent data only
	public void readFromNBTData(NBTTagCompound data)
	{
		NBTTagCompound rootPersistentCompound = player.getEntityData().getCompoundTag(EntityPlayer.PERSISTED_NBT_TAG);

		if ((data != null && data.hasKey(TAG_KEY)) || rootPersistentCompound.hasKey(TAG_KEY))
		{
			NBTTagCompound nonPersistentCompound = data != null ? data.getCompoundTag(TAG_KEY) : new NBTTagCompound();
			NBTTagCompound persistentCompound = rootPersistentCompound.getCompoundTag(TAG_KEY);

			NBTTagCompound nbtHistory = ModConfig.FOOD_HISTORY_PERSISTS_THROUGH_DEATH ? persistentCompound.getCompoundTag("History") : nonPersistentCompound.getCompoundTag("History");

			history.readFromNBTData(nbtHistory);

			totalFoodsEatenAllTime = persistentCompound.getInteger("Total");
			wasGivenFoodJournal = persistentCompound.getBoolean("FoodJournal");
			ticksActive = persistentCompound.getLong("Ticks");
		}
	}

	@Override public NBTTagCompound serializeNBT()
	{
		NBTTagCompound compound = new NBTTagCompound();
		writeToNBTData(compound);
		return compound;
	}

	@Override public void deserializeNBT(NBTTagCompound nbt)
	{
		readFromNBTData(nbt);
	}

	@Override public boolean hasCapability(@Nonnull Capability<?> capability, EnumFacing facing)
	{
		return capability == CAPABILITY;
	}

	@Override public <T> T getCapability(@Nonnull Capability<T> capability, EnumFacing facing)
	{
		return capability == CAPABILITY ? (T) this : null;
	}
}
java/squeek/spiceoflife/network/PacketConfigSync.java
public class PacketConfigSync extends PacketBase
{
	public PacketConfigSync()
	{
	}

	@Override
	public void pack(IByteIO data)
	{
		ModConfig.instance.pack(data);
	}

	@Override
	public void unpack(IByteIO data)
	{
		ModConfig.instance.unpack(data);
	}

	@Override
	public void processInWorldThread(Side side, EntityPlayer player)
	{
		ModConfig.instance.processInWorldThread(side, player);
	}

	@Override
	public PacketBase processAndReply(Side side, EntityPlayer player)
	{
		return ModConfig.instance.processAndReply(side, player);
	}
}
java/squeek/spiceoflife/interfaces/IPackable.java
public interface IPackable
{
	void pack(IByteIO data);

	void unpack(IByteIO data);
}
java/squeek/spiceoflife/network/PacketBase.java
public abstract class PacketBase implements IMessage, IPackable, IPacketProcessor
{
	public PacketBase()
	{
	}

	public Packet getPacket()
	{
		return PacketHandler.channel.getPacketFrom(this);
	}

	@Override
	public void fromBytes(ByteBuf buf)
	{
		unpack(ByteIO.get(buf));
	}

	@Override
	public void toBytes(ByteBuf buf)
	{
		pack(ByteIO.get(buf));
	}
}
java/squeek/spiceoflife/compat/IByteIO.java
public interface IByteIO
{

	boolean readBoolean();

	byte readByte();

	short readUnsignedByte();

	short readShort();

	int readUnsignedShort();

	int readMedium();

	int readUnsignedMedium();

	int readInt();

	long readUnsignedInt();

	long readLong();

	char readChar();

	float readFloat();

	double readDouble();

	@Nonnull ItemStack readItemStack();

	NBTTagCompound readTag();

	String readUTF();

	IByteIO writeBoolean(boolean value);

	IByteIO writeByte(int value);

	IByteIO writeShort(int value);

	IByteIO writeMedium(int value);

	IByteIO writeInt(int value);

	IByteIO writeLong(long value);

	IByteIO writeChar(int value);

	IByteIO writeFloat(float value);

	IByteIO writeDouble(double value);

	IByteIO writeBytes(byte[] src);

	IByteIO writeZero(int length);

	IByteIO writeItemStack(@Nonnull ItemStack itemStack);

	IByteIO writeTag(NBTTagCompound tag);

	IByteIO writeUTF(String str);

	byte[] bytes();

	int readableSize();

	int writableSize();

	IByteIO skipBytes(int length);

	IByteIO clear();

	@Override int hashCode();

	@Override boolean equals(Object obj);

	@Override String toString();

}
java/squeek/spiceoflife/foodtracker/foodgroups/FoodGroupConfig.java
public class FoodGroupConfig
{
	private static final Gson gson = new GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create();
	private static File[] configFiles;

	public static void setup(File configDirectory)
	{
		File modConfigDirectory = new File(configDirectory, ModInfo.MODID);
		if (modConfigDirectory.exists() || modConfigDirectory.mkdirs())
		{
			writeExampleFoodGroup(modConfigDirectory);
			configFiles = modConfigDirectory.listFiles();
		}
	}

	public static void writeExampleFoodGroup(File configDirectory)
	{
		String exampleFoodGroupFileName = "example-food-group.json";
		String exampleFoodGroupRelativePath = "example/" + exampleFoodGroupFileName;
		File exampleFoodGroupDest = new File(configDirectory, exampleFoodGroupFileName);

		try
		{
			boolean shouldOverwrite = shouldOverwriteExampleFoodGroup(exampleFoodGroupDest);
			if (ModSpiceOfLife.instance.sourceFile != null && ModSpiceOfLife.instance.sourceFile.isDirectory())
			{
				File sourceFile = new File(ModSpiceOfLife.instance.sourceFile, exampleFoodGroupRelativePath);
				FileHelper.copyFile(sourceFile, exampleFoodGroupDest, shouldOverwrite);
			}
			else
			{
				InputStream exampleFoodGroupInputStream = FoodGroupConfig.class.getClassLoader().getResourceAsStream(exampleFoodGroupRelativePath);
				FileHelper.copyFile(exampleFoodGroupInputStream, exampleFoodGroupDest, shouldOverwrite);
				exampleFoodGroupInputStream.close();
			}
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	public static boolean shouldOverwriteExampleFoodGroup(File exampleFoodGroup) throws IOException
	{
		FileInputStream exampleFoodGroupStream;
		try
		{
			exampleFoodGroupStream = new FileInputStream(exampleFoodGroup);
		}
		catch (FileNotFoundException e)
		{
			return true;
		}
		BufferedReader exampleFoodGroupReader = null;
		try
		{
			exampleFoodGroupReader = new BufferedReader(new InputStreamReader(exampleFoodGroupStream, "UTF8"));
			String firstLine = exampleFoodGroupReader.readLine();
			return !("// Mod Version: " + ModInfo.VERSION).equals(firstLine);
		}
		finally
		{
			MiscHelper.tryCloseStream(exampleFoodGroupReader);
		}
	}

	public static void load()
	{
		for (File configFile : configFiles)
		{
			boolean isJson = FilenameUtils.getExtension(configFile.getName()).equalsIgnoreCase("json");
			if (!isJson)
				continue;

			InputStreamReader reader = null;
			try
			{
				reader = new InputStreamReader(new FileInputStream(configFile), "UTF8");
				FoodGroup foodGroup = gson.fromJson(reader, FoodGroup.class);
				if (foodGroup != null && foodGroup.enabled)
				{
					foodGroup.identifier = FilenameUtils.removeExtension(configFile.getName());
					foodGroup.initFromConfig();
					FoodGroupRegistry.addFoodGroup(foodGroup);
				}
			}
			catch (FileNotFoundException e)
			{
				e.printStackTrace();
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
			finally
			{
				MiscHelper.tryCloseStream(reader);
			}
		}
	}

}


Question: import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import squeek.spiceoflife.compat.IByteIO;
import squeek.spiceoflife.compat.PacketDispatcher;
import squeek.spiceoflife.foodtracker.FoodHistory;
import squeek.spiceoflife.foodtracker.FoodModifier;
import squeek.spiceoflife.foodtracker.foodgroups.FoodGroupConfig;
import squeek.spiceoflife.foodtracker.foodgroups.FoodGroupRegistry;
import squeek.spiceoflife.interfaces.IPackable;
import squeek.spiceoflife.interfaces.IPacketProcessor;
import squeek.spiceoflife.network.PacketBase;
import squeek.spiceoflife.network.PacketConfigSync;
import java.io.File;
import java.util.Locale;
		 * CLIENT
		 */
		config.getCategory(CATEGORY_CLIENT).setComment(CATEGORY_CLIENT_COMMENT);

		LEFT_CLICK_OPENS_FOOD_CONTAINERS = config.get(CATEGORY_CLIENT, LEFT_CLICK_OPENS_FOOD_CONTAINERS_NAME, LEFT_CLICK_OPENS_FOOD_CONTAINERS_DEFAULT, LEFT_CLICK_OPENS_FOOD_CONTAINERS_COMMENT).getBoolean(LEFT_CLICK_OPENS_FOOD_CONTAINERS_DEFAULT);

		/*
		 * FOOD GROUPS
		 */
		config.getCategory(CATEGORY_FOODGROUPS).setComment(CATEGORY_FOODGROUPS_COMMENT);
		FoodGroupConfig.setup(file.getParentFile());

		// remove obsolete config options
		config.getCategory(CATEGORY_SERVER).remove("use.food.groups");
		config.getCategory(CATEGORY_FOODGROUPS).clear();

		// temporarily disable chance to drop food, needs a better implementation
		FOOD_CONTAINERS_CHANCE_TO_DROP_FOOD = 0;

		save();
	}

	public static void setRoundingMode()
	{
		for (RoundingMode roundingMode : RoundingMode.values())
		{
			if (roundingMode.id.equals(FOOD_HUNGER_ROUNDING_MODE_STRING.toLowerCase(Locale.ROOT)))
			{
				FOOD_HUNGER_ROUNDING_MODE = roundingMode;
				break;
			}
		}
		if (FOOD_HUNGER_ROUNDING_MODE == null)
		{
			ModSpiceOfLife.Log.warn("Rounding mode '" + FOOD_HUNGER_ROUNDING_MODE_STRING + "' not recognized; defaulting to 'round'");
			FOOD_HUNGER_ROUNDING_MODE_STRING = "round";
			FOOD_HUNGER_ROUNDING_MODE = RoundingMode.ROUND;
		}
	}

	public static void save()
	{
		config.save();
	}

	public static void load()
	{
		config.load();
	}

	@Override
	public void pack(IByteIO data)
	{
		data.writeBoolean(FOOD_MODIFIER_ENABLED_CONFIG_VAL);
		if (FOOD_MODIFIER_ENABLED_CONFIG_VAL)
		{
			data.writeUTF(FOOD_MODIFIER_FORMULA);
			data.writeShort(FOOD_HISTORY_LENGTH);
			data.writeBoolean(FOOD_HISTORY_PERSISTS_THROUGH_DEATH);
			data.writeInt(FOOD_EATEN_THRESHOLD);
			data.writeBoolean(CLEAR_HISTORY_ON_FOOD_EATEN_THRESHOLD);
			data.writeBoolean(USE_FOOD_GROUPS_AS_WHITELISTS);
			data.writeBoolean(AFFECT_FOOD_SATURATION_MODIFIERS);
			data.writeBoolean(AFFECT_NEGATIVE_FOOD_SATURATION_MODIFIERS);
			data.writeFloat(FOOD_EATING_SPEED_MODIFIER);
			data.writeInt(FOOD_EATING_DURATION_MAX);
			data.writeBoolean(USE_HUNGER_QUEUE);
			data.writeBoolean(USE_TIME_QUEUE);
			data.writeBoolean(PROGRESS_TIME_WHILE_LOGGED_OFF);
			data.writeUTF(FOOD_HUNGER_ROUNDING_MODE_STRING);
		}
		data.writeInt(FOOD_CONTAINERS_MAX_STACKSIZE);
		data.writeFloat(FOOD_CONTAINERS_CHANCE_TO_DROP_FOOD);
	}

	@Override
	public void unpack(IByteIO data)
	{
		FOOD_MODIFIER_ENABLED = data.readBoolean();
		if (FOOD_MODIFIER_ENABLED)
		{
			FOOD_MODIFIER_FORMULA = data.readUTF();
			FOOD_HISTORY_LENGTH = data.readShort();
			FOOD_HISTORY_PERSISTS_THROUGH_DEATH = data.readBoolean();
			FOOD_EATEN_THRESHOLD = data.readInt();
			CLEAR_HISTORY_ON_FOOD_EATEN_THRESHOLD = data.readBoolean();
			USE_FOOD_GROUPS_AS_WHITELISTS = data.readBoolean();
			AFFECT_FOOD_SATURATION_MODIFIERS = data.readBoolean();
			AFFECT_NEGATIVE_FOOD_SATURATION_MODIFIERS = data.readBoolean();
			FOOD_EATING_SPEED_MODIFIER = data.readFloat();
			FOOD_EATING_DURATION_MAX = data.readInt();
			USE_HUNGER_QUEUE = data.readBoolean();
			USE_TIME_QUEUE = data.readBoolean();
			PROGRESS_TIME_WHILE_LOGGED_OFF = data.readBoolean();
			FOOD_HUNGER_ROUNDING_MODE_STRING = data.readUTF();
		}
		FOOD_CONTAINERS_MAX_STACKSIZE = data.readInt();
		FOOD_CONTAINERS_CHANCE_TO_DROP_FOOD = data.readFloat();
	}

	@Override
	public void processInWorldThread(Side side, EntityPlayer player)
	{
		if (FOOD_MODIFIER_ENABLED)
		{
			setRoundingMode();
			FoodModifier.onGlobalFormulaChanged();
			FoodHistory.get(player).onHistoryTypeChanged();
			FoodGroupRegistry.clear();
		}
	}

	@Override
	public PacketBase processAndReply(Side side, EntityPlayer player)
	{
		return null;
	}

	public static void sync(EntityPlayerMP player)
	{
Answer: 		PacketDispatcher.get().sendTo(new PacketConfigSync(), player);