package kubatech.tileentity.gregtech.multiblock.eigbuckets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import net.minecraft.block.Block; import net.minecraft.block.BlockLiquid; import net.minecraft.init.Blocks; import net.minecraft.item.Item; import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import net.minecraftforge.oredict.OreDictionary; import gregtech.api.GregTechAPI; import gregtech.api.enums.ItemList; import gregtech.common.blocks.BlockOresAbstract; import gregtech.common.blocks.ItemOres; import gregtech.common.blocks.TileEntityOres; import ic2.api.crops.CropCard; import ic2.api.crops.Crops; import ic2.core.Ic2Items; import ic2.core.crop.CropStickreed; import ic2.core.crop.IC2Crops; import ic2.core.crop.TileEntityCrop; import kubatech.api.eig.EIGBucket; import kubatech.api.eig.EIGDropTable; import kubatech.api.eig.IEIGBucketFactory; import kubatech.tileentity.gregtech.multiblock.MTEExtremeIndustrialGreenhouse; public class EIGIC2Bucket extends EIGBucket { public final static IEIGBucketFactory factory = new EIGIC2Bucket.Factory(); private static final String NBT_IDENTIFIER = "IC2"; private static final int REVISION_NUMBER = 0; // region crop simulation variables private final static int NUMBER_OF_DROPS_TO_SIMULATE = 1000; // nutrient factors /** * Set to true if you want to assume the crop is on wet farmland for a +2 bonus to nutrients */ private static final boolean IS_ON_WET_FARMLAND = true; /** * The amount of water stored in the crop stick when hydration is turned on. * bounds of 0 to 200 inclusive */ private static final int WATER_STORAGE_VALUE = 200; // nutrient factors /** * The number of blocks of dirt we assume are under. Subtract 1 if we have a block under our crop. * bounds of 0 to 3, inclusive */ private static final int NUMBER_OF_DIRT_BLOCKS_UNDER = 0; /** * The amount of fertilizer stored in the crop stick * bounds of 0 to 200, inclusive */ private static final int FERTILIZER_STORAGE_VALUE = 0; // air quality factors /** * How many blocks in a 3x3 area centered on the crop do not contain solid blocks or other crops. * Max value is 8 because the crop always counts itself. * bound of 0-8 inclusive */ private static final int CROP_OBSTRUCTION_VALUE = 5; /** * Being able to see the sky gives a +2 bonus to the air quality */ private static final boolean CROP_CAN_SEE_SKY = false; // endregion crop simulation variables public static class Factory implements IEIGBucketFactory { @Override public String getNBTIdentifier() { return NBT_IDENTIFIER; } @Override public EIGBucket tryCreateBucket(MTEExtremeIndustrialGreenhouse greenhouse, ItemStack input) { // Check if input is a seed. if (!ItemList.IC2_Crop_Seeds.isStackEqual(input, true, true)) return null; if (!input.hasTagCompound()) return null; // Validate that stat nbt data exists. NBTTagCompound nbt = input.getTagCompound(); if (!(nbt.hasKey("growth") && nbt.hasKey("gain") && nbt.hasKey("resistance"))) return null; CropCard cc = IC2Crops.instance.getCropCard(input); if (cc == null) return null; return new EIGIC2Bucket(greenhouse, input); } @Override public EIGBucket restore(NBTTagCompound nbt) { return new EIGIC2Bucket(nbt); } } public final boolean useNoHumidity; /** * The average amount of growth cycles needed to reach maturity. */ private double growthTime = 0; private EIGDropTable drops = new EIGDropTable(); private boolean isValid = false; /** * Used to migrate old EIG greenhouse slots to the new bucket system, needs custom handling as to not void the * support blocks. * * @implNote DOES NOT VALIDATE THE CONTENTS OF THE BUCKET, YOU'LL HAVE TO REVALIDATE WHEN THE WORLD IS LOADED. * * @param seed The item stack for the item that served as the seed before * @param count The number of seed in the bucket * @param supportBlock The block that goes under the bucket * @param useNoHumidity Whether to use no humidity in growth speed calculations. */ public EIGIC2Bucket(ItemStack seed, int count, ItemStack supportBlock, boolean useNoHumidity) { super(seed, count, supportBlock == null ? null : new ItemStack[] { supportBlock }); this.useNoHumidity = useNoHumidity; // revalidate me this.isValid = false; } private EIGIC2Bucket(MTEExtremeIndustrialGreenhouse greenhouse, ItemStack seed) { super(seed, 1, null); this.useNoHumidity = greenhouse.isInNoHumidityMode(); this.recalculateDrops(greenhouse); } private EIGIC2Bucket(NBTTagCompound nbt) { super(nbt); this.useNoHumidity = nbt.getBoolean("useNoHumidity"); // If the invalid key exists then drops and growth time haven't been saved if (!nbt.hasKey("invalid")) { this.drops = new EIGDropTable(nbt, "drops"); this.growthTime = nbt.getDouble("growthTime"); this.isValid = nbt.getInteger("version") == REVISION_NUMBER && this.growthTime > 0 && !this.drops.isEmpty(); } } @Override public NBTTagCompound save() { NBTTagCompound nbt = super.save(); nbt.setBoolean("useNoHumidity", this.useNoHumidity); if (this.isValid) { nbt.setTag("drops", this.drops.save()); nbt.setDouble("growthTime", this.growthTime); } else { nbt.setBoolean("invalid", true); } nbt.setInteger("version", REVISION_NUMBER); return nbt; } @Override protected String getNBTIdentifier() { return NBT_IDENTIFIER; } @Override public void addProgress(double multiplier, EIGDropTable tracker) { // abort early if the bucket is invalid if (!this.isValid()) return; // else apply drops to tracker double growthPercent = multiplier / (this.growthTime * TileEntityCrop.tickRate); if (this.drops != null) { this.drops.addTo(tracker, this.seedCount * growthPercent); } } @Override protected void getAdditionalInfoData(StringBuilder sb) { sb.append(" | Humidity: "); sb.append(this.useNoHumidity ? "Off" : "On"); } @Override public boolean revalidate(MTEExtremeIndustrialGreenhouse greenhouse) { this.recalculateDrops(greenhouse); return this.isValid(); } @Override public boolean isValid() { return super.isValid() && this.isValid; } /** * (Re-)calculates the pre-generated drop table for this bucket. * * @param greenhouse The {@link MTEExtremeIndustrialGreenhouse} that contains this bucket. */ public void recalculateDrops(MTEExtremeIndustrialGreenhouse greenhouse) { this.isValid = false; World world = greenhouse.getBaseMetaTileEntity() .getWorld(); int[] abc = new int[] { 0, -2, 3 }; int[] xyz = new int[] { 0, 0, 0 }; greenhouse.getExtendedFacing() .getWorldOffset(abc, xyz); xyz[0] += greenhouse.getBaseMetaTileEntity() .getXCoord(); xyz[1] += greenhouse.getBaseMetaTileEntity() .getYCoord(); xyz[2] += greenhouse.getBaseMetaTileEntity() .getZCoord(); boolean cheating = false; FakeTileEntityCrop crop; try { if (world.getBlock(xyz[0], xyz[1] - 2, xyz[2]) != GregTechAPI.sBlockCasings4 || world.getBlockMetadata(xyz[0], xyz[1] - 2, xyz[2]) != 1) { // no cheating = true; return; } // instantiate the TE in which we grow the seed. crop = new FakeTileEntityCrop(this, greenhouse, xyz); if (!crop.isValid) return; CropCard cc = crop.getCrop(); // region can grow checks // Check if we can put the current block under the soil. if (this.supportItems != null && this.supportItems.length == 1 && this.supportItems[0] != null) { if (!setBlock(this.supportItems[0], xyz[0], xyz[1] - 2, xyz[2], world)) { return; } // update nutrients if we need a block under. crop.updateNutrientsForBlockUnder(); } // Check if the crop has a chance to die in the current environment if (calcAvgGrowthRate(crop, cc, 0) < 0) return; // Check if the crop has a chance to grow in the current environment. if (calcAvgGrowthRate(crop, cc, 6) <= 0) return; ItemStack blockInputStackToConsume = null; if (!crop.canMature()) { // If the block we have in storage no longer functions, we are no longer valid, the seed and block // should be ejected if possible. if (this.supportItems != null) return; // assume we need a block under the farmland/fertilized dirt and update nutrients accordingly crop.updateNutrientsForBlockUnder(); // Try to find the needed block in the inputs boolean canGrow = false; ArrayList inputs = greenhouse.getStoredInputs(); for (ItemStack potentialBlock : inputs) { // if the input can't be placed in the world skip to the next input if (potentialBlock == null || potentialBlock.stackSize <= 0) continue; if (!setBlock(potentialBlock, xyz[0], xyz[1] - 2, xyz[2], world)) continue; // check if the crop can grow with the block under it. if (!crop.canMature()) continue; // If we don't have enough blocks to consume, abort. if (this.seedCount > potentialBlock.stackSize) return; canGrow = true; blockInputStackToConsume = potentialBlock; // Don't consume the block just yet, we do that once everything is valid. ItemStack newSupport = potentialBlock.copy(); newSupport.stackSize = 1; this.supportItems = new ItemStack[] { newSupport }; break; } if (!canGrow) return; } // check if the crop does a block under check and try to put a requested block if possible if (this.supportItems == null) { // some crops get increased outputs if a specific block is under them. cc.getGain(crop); if (crop.hasRequestedBlockUnder()) { ArrayList inputs = greenhouse.getStoredInputs(); boolean keepLooking = !inputs.isEmpty(); if (keepLooking && !crop.reqBlockOreDict.isEmpty()) { oreDictLoop: for (String reqOreDictName : crop.reqBlockOreDict) { if (reqOreDictName == null || OreDictionary.doesOreNameExist(reqOreDictName)) continue; int oreId = OreDictionary.getOreID(reqOreDictName); for (ItemStack potentialBlock : inputs) { if (potentialBlock == null || potentialBlock.stackSize <= 0) continue; for (int inputOreId : OreDictionary.getOreIDs(potentialBlock)) { if (inputOreId != oreId) continue; blockInputStackToConsume = potentialBlock; // Don't consume the block just yet, we do that once everything is valid. ItemStack newSupport = potentialBlock.copy(); newSupport.stackSize = 1; this.supportItems = new ItemStack[] { newSupport }; keepLooking = false; crop.updateNutrientsForBlockUnder(); break oreDictLoop; } } } } if (keepLooking && !crop.reqBlockSet.isEmpty()) { blockLoop: for (Block reqBlock : crop.reqBlockSet) { if (reqBlock == null || reqBlock instanceof BlockLiquid) continue; for (ItemStack potentialBlockStack : inputs) { // TODO: figure out a way to handle liquid block requirements // water lilly looks for water and players don't really have access to those. if (potentialBlockStack == null || potentialBlockStack.stackSize <= 0) continue; // check if it places a block that is equal to the the one we are looking for Block inputBlock = Block.getBlockFromItem(potentialBlockStack.getItem()); if (inputBlock != reqBlock) continue; blockInputStackToConsume = potentialBlockStack; // Don't consume the block just yet, we do that once everything is valid. ItemStack newSupport = potentialBlockStack.copy(); newSupport.stackSize = 1; this.supportItems = new ItemStack[] { newSupport }; keepLooking = false; crop.updateNutrientsForBlockUnder(); break blockLoop; } } } } } // check if the crop can be harvested at its max size // Eg: the Eating plant cannot be harvested at its max size of 6, only 4 or 5 can crop.setSize((byte) cc.maxSize()); if (!cc.canBeHarvested(crop)) return; // endregion can grow checks // region drop rate calculations // PRE CALCULATE DROP RATES // TODO: Add better loot table handling for crops like red wheat // berries, etc. EIGDropTable drops = new EIGDropTable(); // Multiply drop sizes by the average number drop rounds per harvest. double avgDropRounds = getRealAverageDropRounds(crop, cc); double avgStackIncrease = getRealAverageDropIncrease(crop, cc); HashMap sizeAfterHarvestFrequencies = new HashMap<>(); for (int i = 0; i < NUMBER_OF_DROPS_TO_SIMULATE; i++) { // try generating some loot drop ItemStack drop = cc.getGain(crop); if (drop == null || drop.stackSize <= 0) continue; sizeAfterHarvestFrequencies.merge((int) cc.getSizeAfterHarvest(crop), 1, Integer::sum); // Merge the new drop with the current loot table. double avgAmount = (drop.stackSize + avgStackIncrease) * avgDropRounds; drops.addDrop(drop, avgAmount / NUMBER_OF_DROPS_TO_SIMULATE); } if (drops.isEmpty()) return; // endregion drop rate calculations // region growth time calculation // Just doing average(ceil(stageGrowth/growthSpeed)) isn't good enough it's off by as much as 20% double avgGrowthCyclesToHarvest = calcRealAvgGrowthRate(crop, cc, sizeAfterHarvestFrequencies); if (avgGrowthCyclesToHarvest <= 0) { return; } // endregion growth time calculation // Consume new under block if necessary if (blockInputStackToConsume != null) blockInputStackToConsume.stackSize -= this.seedCount; // We are good return success this.growthTime = avgGrowthCyclesToHarvest; this.drops = drops; this.isValid = true; } catch (Exception e) { e.printStackTrace(System.err); } finally { // always reset the world to it's original state if (!cheating) world.setBlock(xyz[0], xyz[1] - 2, xyz[2], GregTechAPI.sBlockCasings4, 1, 0); // world.setBlockToAir(xyz[0], xyz[1], xyz[2]); } } /** * Attempts to place a block in the world, used for testing crop viability and drops. * * @param stack The {@link ItemStack} to place. * @param x The x coordinate at which to place the block. * @param y The y coordinate at which to place the block. * @param z The z coordinate at which to place the block. * @param world The world in which to place the block. * @return true of a block was placed. */ private static boolean setBlock(ItemStack stack, int x, int y, int z, World world) { Item item = stack.getItem(); Block b = Block.getBlockFromItem(item); if (b == Blocks.air || !(item instanceof ItemBlock)) return false; short tDamage = (short) item.getDamage(stack); if (item instanceof ItemOres && tDamage > 0) { if (!world.setBlock( x, y, z, b, TileEntityOres .getHarvestData(tDamage, ((BlockOresAbstract) b).getBaseBlockHarvestLevel(tDamage % 16000 / 1000)), 0)) { return false; } TileEntityOres tTileEntity = (TileEntityOres) world.getTileEntity(x, y, z); tTileEntity.mMetaData = tDamage; tTileEntity.mNatural = false; } else world.setBlock(x, y, z, b, tDamage, 0); return true; } // region drop rate calculations /** * Calculates the average number of separate item drops to be rolled per harvest using information obtained by * decompiling IC2. * * @see TileEntityCrop#harvest_automated(boolean) * @param te The {@link TileEntityCrop} holding the crop * @param cc The {@link CropCard} of the seed * @return The average number of drops to computer per harvest */ private static double getRealAverageDropRounds(TileEntityCrop te, CropCard cc) { // this should be ~99.995% accurate double chance = (double) cc.dropGainChance() * Math.pow(1.03, te.getGain()); // this is essentially just performing an integration using the composite trapezoidal rule. double min = -10, max = 10; int steps = 10000; double stepSize = (max - min) / steps; double sum = 0; for (int k = 1; k <= steps - 1; k++) { sum += getWeightedDropChance(min + k * stepSize, chance); } double minVal = getWeightedDropChance(min, chance); double maxVal = getWeightedDropChance(max, chance); return stepSize * ((minVal + maxVal) / 2 + sum); } /** * Evaluates the value of y for a standard normal distribution * * @param x The value of x to evaluate * @return The value of y */ private static double stdNormDistr(double x) { return Math.exp(-0.5 * (x * x)) / SQRT2PI; } private static final double SQRT2PI = Math.sqrt(2.0d * Math.PI); /** * Calculates the weighted drop chance using * * @param x The value rolled by nextGaussian * @param chance the base drop chance * @return the weighted drop chance */ private static double getWeightedDropChance(double x, double chance) { return Math.max(0L, Math.round(x * chance * 0.6827d + chance)) * stdNormDistr(x); } /** * Calculates the average drop of the stack size caused by seed's gain using information obtained by * decompiling IC2. * * @see TileEntityCrop#harvest_automated(boolean) * @param te The {@link TileEntityCrop} holding the crop * @param cc The {@link CropCard} of the seed * @return The average number of drops to computer per harvest */ private static double getRealAverageDropIncrease(TileEntityCrop te, CropCard cc) { // yes gain has the amazing ability to sometimes add 1 to your stack size! return (te.getGain() + 1) / 100.0d; } // endregion drop rate calculations // region growth time approximation /** * Calculates the average number growth cycles needed for a crop to grow to maturity. * * @see EIGIC2Bucket#calcAvgGrowthRate(TileEntityCrop, CropCard, int) * @param te The {@link TileEntityCrop} holding the crop * @param cc The {@link CropCard} of the seed * @return The average growth rate as a floating point number */ private static double calcRealAvgGrowthRate(TileEntityCrop te, CropCard cc, HashMap sizeAfterHarvestFrequencies) { // Compute growth speeds. int[] growthSpeeds = new int[7]; for (int i = 0; i < 7; i++) growthSpeeds[i] = calcAvgGrowthRate(te, cc, i); // if it's stick reed, we know what the distribution should look like if (cc.getClass() == CropStickreed.class) { sizeAfterHarvestFrequencies.clear(); sizeAfterHarvestFrequencies.put(1, 1); sizeAfterHarvestFrequencies.put(2, 1); sizeAfterHarvestFrequencies.put(3, 1); } // Get the duration of all growth stages int[] growthDurations = new int[cc.maxSize()]; // , index 0 is assumed to be 0 since stage 0 is usually impossible. // The frequency table should prevent stage 0 from having an effect on the result. growthDurations[0] = 0; // stage 0 doesn't usually exist. for (byte i = 1; i < growthDurations.length; i++) { te.setSize(i); growthDurations[i] = cc.growthDuration(te); } return calcRealAvgGrowthRate(growthSpeeds, growthDurations, sizeAfterHarvestFrequencies); } /** * Calculates the average number growth cycles needed for a crop to grow to maturity. * * @implNote This method is entirely self-contained and can therefore be unit tested. * * @param growthSpeeds The speeds at which the crop can grow. * @param stageGoals The total to reach for each stage * @param startStageFrequency How often the growth starts from a given stage * @return The average growth rate as a floating point number */ public static double calcRealAvgGrowthRate(int[] growthSpeeds, int[] stageGoals, HashMap startStageFrequency) { // taking out the zero rolls out of the calculation tends to make the math more accurate for lower speeds. int[] nonZeroSpeeds = Arrays.stream(growthSpeeds) .filter(x -> x > 0) .toArray(); int zeroRolls = growthSpeeds.length - nonZeroSpeeds.length; if (zeroRolls >= growthSpeeds.length) return -1; // compute stage lengths and stage frequencies double[] avgCyclePerStage = new double[stageGoals.length]; double[] normalizedStageFrequencies = new double[stageGoals.length]; long frequenciesSum = startStageFrequency.values() .parallelStream() .mapToInt(x -> x) .sum(); for (int i = 0; i < stageGoals.length; i++) { avgCyclePerStage[i] = calcAvgCyclesToGoal(nonZeroSpeeds, stageGoals[i]); normalizedStageFrequencies[i] = startStageFrequency.getOrDefault(i, 0) * stageGoals.length / (double) frequenciesSum; } // Compute multipliers based on how often the growth starts at a given rate. double[] frequencyMultipliers = new double[avgCyclePerStage.length]; Arrays.fill(frequencyMultipliers, 1.0d); conv1DAndCopyToSignal( frequencyMultipliers, normalizedStageFrequencies, new double[avgCyclePerStage.length], 0, frequencyMultipliers.length, 0); // apply multipliers to length for (int i = 0; i < avgCyclePerStage.length; i++) avgCyclePerStage[i] *= frequencyMultipliers[i]; // lengthen average based on number of 0 rolls. double average = Arrays.stream(avgCyclePerStage) .average() .orElse(-1); if (average <= 0) return -1; if (zeroRolls > 0) { average = average / nonZeroSpeeds.length * growthSpeeds.length; } // profit return average; } /** * Computes the average number of rolls of an N sided fair dice with irregular number progressions needed to surpass * a given total. * * @param speeds The speeds at which the crop grows. * @param goal The total to match or surpass. * @return The average number of rolls of speeds to meet or surpass the goal. */ private static double calcAvgCyclesToGoal(int[] speeds, int goal) { // even if the goal is 0, it will always take at least 1 cycle. if (goal <= 0) return 1; double mult = 1.0d; int goalCap = speeds[speeds.length - 1] * 1000; if (goal > goalCap) { mult = (double) goal / goalCap; goal = goalCap; } // condition start signal double[] signal = new double[goal]; Arrays.fill(signal, 0); signal[0] = 1; // Create kernel out of our growth speeds double[] kernel = tabulate(speeds, 1.0d / speeds.length); double[] convolutionTarget = new double[signal.length]; LinkedList P = new LinkedList(); // Perform convolutions on the signal until it's too weak to be recognised. double p, avgRolls = 1; int iterNo = 0; // 1e-1 is a threshold, you can increase it for to increase the accuracy of the output. // 1e-1 is already accurate enough that any value beyond that is unwarranted. int min = speeds[0]; int max = speeds[speeds.length - 1]; do { avgRolls += p = conv1DAndCopyToSignal(signal, kernel, convolutionTarget, min, max, iterNo); iterNo += 1; } while (p >= 1e-1 / goal); return avgRolls * mult; } /** * Creates an array that corresponds to the amount of times a number appears in a list. * * Ex: {1,2,3,4} -> {0,1,1,1,1}, {0,2,2,4} -> {1,0,2,0,1} * * @param bin The number list to tabulate * @param multiplier A multiplier to apply the output list * @return The number to tabulate */ private static double[] tabulate(int[] bin, double multiplier) { double[] ret = new double[bin[bin.length - 1] + 1]; Arrays.fill(ret, 0); for (int i : bin) ret[i] += multiplier; return ret; } /** * Computes a 1D convolution of a signal and stores the results in the signal array. * Essentially performs `X <- convolve(X,rev(Y))[1:length(X)]` in R * * @param signal The signal to apply the convolution to. * @param kernel The kernel to compute with. * @param fixedLengthTarget A memory optimisation so we don't just create a ton of arrays since we overwrite it. * Should be the same length as the signal. */ private static double conv1DAndCopyToSignal(double[] signal, double[] kernel, double[] fixedLengthTarget, int minValue, int maxValue, int iterNo) { // for a 1d convolution we would usually use kMax = signal.length + kernel.length - 1 // but since we are directly applying our result to our signal, there is no reason to compute // values where k > signal.length. // we could probably run this loop in parallel. double sum = 0; int maxK = Math.min(signal.length, (iterNo + 1) * maxValue + 1); int startAt = Math.min(signal.length, minValue * (iterNo + 1)); int k = Math.max(0, startAt - kernel.length); for (; k < startAt; k++) fixedLengthTarget[k] = 0; for (; k < maxK; k++) { // I needs to be a valid index of the kernel. fixedLengthTarget[k] = 0; for (int i = Math.max(0, k - kernel.length + 1); i <= k; i++) { double v = signal[i] * kernel[k - i]; sum += v; fixedLengthTarget[k] += v; } } System.arraycopy(fixedLengthTarget, 0, signal, 0, signal.length); return sum; } /** * Calculates the average growth rate of an ic2 crop using information obtained though decompiling IC2. * Calls to random functions have been either replaced with customisable values or boundary tests. * * @see TileEntityCrop#calcGrowthRate() * @param te The {@link TileEntityCrop} holding the crop * @param cc The {@link CropCard} of the seed * @param rngRoll The role for the base rng * @return The amounts of growth point added to the growth progress in average every growth tick */ private static int calcAvgGrowthRate(TileEntityCrop te, CropCard cc, int rngRoll) { // the original logic uses IC2.random.nextInt(7) int base = 3 + rngRoll + te.getGrowth(); int need = Math.max(0, (cc.tier() - 1) * 4 + te.getGrowth() + te.getGain() + te.getResistance()); int have = cc.weightInfluences(te, te.getHumidity(), te.getNutrients(), te.getAirQuality()) * 5; if (have >= need) { // The crop has a good enough environment to grow normally return base * (100 + (have - need)) / 100; } else { // this only happens if we don't have enough // resources to grow properly. int neg = (need - have) * 4; if (neg > 100) { // a crop with a resistance 31 will never die since the original // checks for `IC2.random.nextInt(32) > this.statResistance` // so assume that the crop will eventually die if it doesn't // have maxed out resistance stats. 0 means no growth this tick // -1 means the crop dies. return te.getResistance() >= 31 ? 0 : -1; } // else apply neg to base return Math.max(0, base * (100 - neg) / 100); } } // endregion growth time approximation // region deterministic environmental calculations /** * Calculates the humidity at the location of the controller using information obtained by decompiling IC2. * Returns 0 if the greenhouse is in no humidity mode. * * @see EIGIC2Bucket#IS_ON_WET_FARMLAND * @see EIGIC2Bucket#WATER_STORAGE_VALUE * @see TileEntityCrop#updateHumidity() * @param greenhouse The {@link MTEExtremeIndustrialGreenhouse} that holds the seed. * @return The humidity environmental value at the controller's location. */ public static byte getHumidity(MTEExtremeIndustrialGreenhouse greenhouse, boolean useNoHumidity) { if (useNoHumidity) return 0; int value = Crops.instance.getHumidityBiomeBonus( greenhouse.getBaseMetaTileEntity() .getBiome()); if (IS_ON_WET_FARMLAND) value += 2; // we add 2 if we have more than 5 water in storage if (WATER_STORAGE_VALUE >= 5) value += 2; // add 1 for every 25 water stored (max of 200 value += (WATER_STORAGE_VALUE + 24) / 25; return (byte) value; } /** * Calculates the nutrient value at the location of the controller using information obtained by decompiling IC2 * * @see EIGIC2Bucket#NUMBER_OF_DIRT_BLOCKS_UNDER * @see EIGIC2Bucket#FERTILIZER_STORAGE_VALUE * @see TileEntityCrop#updateNutrients() * @param greenhouse The {@link MTEExtremeIndustrialGreenhouse} that holds the seed. * @return The nutrient environmental value at the controller's location. */ public static byte getNutrients(MTEExtremeIndustrialGreenhouse greenhouse) { int value = Crops.instance.getNutrientBiomeBonus( greenhouse.getBaseMetaTileEntity() .getBiome()); value += NUMBER_OF_DIRT_BLOCKS_UNDER; value += (FERTILIZER_STORAGE_VALUE + 19) / 20; return (byte) value; } /** * Calculates the air quality at the location of the controller bucket using information obtained by decompiling IC2 * * @see EIGIC2Bucket#CROP_OBSTRUCTION_VALUE * @see EIGIC2Bucket#CROP_CAN_SEE_SKY * @see TileEntityCrop#updateAirQuality() * @param greenhouse The {@link MTEExtremeIndustrialGreenhouse} that holds the seed. * @return The air quality environmental value at the controller's location. */ public static byte getAirQuality(MTEExtremeIndustrialGreenhouse greenhouse) { // clamp height bonus to 0-4, use the height of the crop itself // TODO: check if we want to add the extra +2 for the actual height of the crop stick in the EIG. int value = Math.max( 0, Math.min( 4, (greenhouse.getBaseMetaTileEntity() .getYCoord() - 64) / 15)); // min value of fresh is technically 8 since the crop itself will count as an obstruction at xOff = 0, zOff = 0 value += CROP_OBSTRUCTION_VALUE / 2; // you get a +2 bonus for being able to see the sky if (CROP_CAN_SEE_SKY) value += 2; return (byte) value; } // endregion deterministic environmental calculations private static class FakeTileEntityCrop extends TileEntityCrop { private boolean isValid; public Set reqBlockSet = new HashSet<>(); public Set reqBlockOreDict = new HashSet<>(); private int lightLevel = 15; public FakeTileEntityCrop(EIGIC2Bucket bucket, MTEExtremeIndustrialGreenhouse greenhouse, int[] xyz) { super(); this.isValid = false; this.ticker = 1; // put seed in crop stick CropCard cc = Crops.instance.getCropCard(bucket.seed); this.setCrop(cc); NBTTagCompound nbt = bucket.seed.getTagCompound(); this.setGrowth(nbt.getByte("growth")); this.setGain(nbt.getByte("gain")); this.setResistance(nbt.getByte("resistance")); this.setWorldObj( greenhouse.getBaseMetaTileEntity() .getWorld()); this.xCoord = xyz[0]; this.yCoord = xyz[1]; this.zCoord = xyz[2]; this.blockType = Block.getBlockFromItem(Ic2Items.crop.getItem()); this.blockMetadata = 0; this.waterStorage = bucket.useNoHumidity ? 0 : WATER_STORAGE_VALUE; this.humidity = EIGIC2Bucket.getHumidity(greenhouse, bucket.useNoHumidity); this.nutrientStorage = FERTILIZER_STORAGE_VALUE; this.nutrients = EIGIC2Bucket.getNutrients(greenhouse); this.airQuality = EIGIC2Bucket.getAirQuality(greenhouse); this.isValid = true; } public boolean canMature() { CropCard cc = this.getCrop(); this.size = cc.maxSize() - 1; // try with a high light level this.lightLevel = 15; if (cc.canGrow(this)) return true; // and then with a low light level. this.lightLevel = 9; return cc.canGrow(this); } @Override public boolean isBlockBelow(Block reqBlock) { this.reqBlockSet.add(reqBlock); return super.isBlockBelow(reqBlock); } @Override public boolean isBlockBelow(String oreDictionaryName) { this.reqBlockOreDict.add(oreDictionaryName); return super.isBlockBelow(oreDictionaryName); } // region environment simulation @Override public int getLightLevel() { // 9 should allow most light dependent crops to grow // the only exception I know of the eating plant which checks return this.lightLevel; } @Override public byte getHumidity() { return this.humidity; } @Override public byte updateHumidity() { return this.humidity; } @Override public byte getNutrients() { return this.nutrients; } @Override public byte updateNutrients() { return this.nutrients; } @Override public byte getAirQuality() { return this.airQuality; } @Override public byte updateAirQuality() { return this.nutrients; } // endregion environment simulation /** * Updates the nutrient value based on the fact tha the crop needs a block under it. */ public void updateNutrientsForBlockUnder() { // -1 because the farm land is included in the root check. if ((this.getCrop() .getrootslength(this) - 1 - NUMBER_OF_DIRT_BLOCKS_UNDER) <= 0 && this.nutrients > 0) { this.nutrients--; } } /** * Checks if the crop stick has requested a block to be under it yet. * * @return true if a block under check was made. */ public boolean hasRequestedBlockUnder() { return !this.reqBlockSet.isEmpty() || !this.reqBlockOreDict.isEmpty(); } } }