diff options
author | Raven Szewczyk <git@eigenraven.me> | 2024-05-30 18:26:10 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-30 19:26:10 +0200 |
commit | 337594e83a74c432c140b3df3287575b81bce467 (patch) | |
tree | abe57b3390d3dd037ea1442f83c4519ebcb9de07 /src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets | |
parent | 752f262ccd545bdb785ef0e9ce922bf1117d23d6 (diff) | |
download | GT5-Unofficial-337594e83a74c432c140b3df3287575b81bce467.tar.gz GT5-Unofficial-337594e83a74c432c140b3df3287575b81bce467.tar.bz2 GT5-Unofficial-337594e83a74c432c140b3df3287575b81bce467.zip |
Complete backend rework of the EIG (#2616)
* Complete backend rework of the EIG
* Mergening Related Updates
Also some loader references refactoring
* fix
(cherry picked from commit 7fd5d7417bddfb6e49ede3986d9a547f15b21289)
* More Mergening fixes
Updates the declaration of the stem mixin to match the new format.
* Inline EIG IC2 bucket constants
addresses: https://github.com/GTNewHorizons/GT5-Unofficial/pull/2616#discussion_r1620596497
* Fix Seed Removal in regular seed simulations
Should address https://github.com/GTNewHorizons/GT5-Unofficial/pull/2616#discussion_r1620583338
---------
Co-authored-by: Guillaume Mercier <10gui-gui10@live.ca>
Co-authored-by: Martin Robertz <dream-master@gmx.net>
Diffstat (limited to 'src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets')
5 files changed, 1499 insertions, 0 deletions
diff --git a/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGFlowerBucket.java b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGFlowerBucket.java new file mode 100644 index 0000000000..1c5588c335 --- /dev/null +++ b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGFlowerBucket.java @@ -0,0 +1,73 @@ +package kubatech.tileentity.gregtech.multiblock.eigbuckets; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockFlower; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import kubatech.api.eig.EIGBucket; +import kubatech.api.eig.EIGDropTable; +import kubatech.api.eig.IEIGBucketFactory; +import kubatech.tileentity.gregtech.multiblock.GT_MetaTileEntity_ExtremeIndustrialGreenhouse; + +public class EIGFlowerBucket extends EIGBucket { + + public final static IEIGBucketFactory factory = new EIGFlowerBucket.Factory(); + private static final String NBT_IDENTIFIER = "FLOWER"; + private static final int REVISION_NUMBER = 0; + + public static class Factory implements IEIGBucketFactory { + + @Override + public String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public EIGBucket tryCreateBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack input) { + // Check if input is a flower, reed or cacti. They all drop their source item multiplied by their seed count + Item item = input.getItem(); + Block block = Block.getBlockFromItem(item); + if (item != Items.reeds && block != Blocks.cactus && !(block instanceof BlockFlower)) return null; + return new EIGFlowerBucket(input); + } + + @Override + public EIGBucket restore(NBTTagCompound nbt) { + return new EIGFlowerBucket(nbt); + } + } + + private EIGFlowerBucket(ItemStack input) { + super(input, 1, null); + } + + private EIGFlowerBucket(NBTTagCompound nbt) { + super(nbt); + } + + @Override + public NBTTagCompound save() { + NBTTagCompound nbt = super.save(); + nbt.setInteger("version", REVISION_NUMBER); + return nbt; + } + + @Override + protected String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public void addProgress(double multiplier, EIGDropTable tracker) { + tracker.addDrop(this.seed, this.seedCount * multiplier); + } + + @Override + public boolean revalidate(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + return this.isValid(); + } +} diff --git a/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGIC2Bucket.java b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGIC2Bucket.java new file mode 100644 index 0000000000..7daa524d5d --- /dev/null +++ b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGIC2Bucket.java @@ -0,0 +1,905 @@ +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.GregTech_API; +import gregtech.api.enums.ItemList; +import gregtech.common.blocks.GT_Block_Ores_Abstract; +import gregtech.common.blocks.GT_Item_Ores; +import gregtech.common.blocks.GT_TileEntity_Ores; +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.GT_MetaTileEntity_ExtremeIndustrialGreenhouse; + +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(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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 GT_MetaTileEntity_ExtremeIndustrialGreenhouse} that contains this bucket. + */ + public void recalculateDrops(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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]) != GregTech_API.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<ItemStack> 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<ItemStack> 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<Integer, Integer> 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], GregTech_API.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 GT_Item_Ores && tDamage > 0) { + if (!world.setBlock( + x, + y, + z, + b, + GT_TileEntity_Ores.getHarvestData( + tDamage, + ((GT_Block_Ores_Abstract) b).getBaseBlockHarvestLevel(tDamage % 16000 / 1000)), + 0)) { + return false; + } + GT_TileEntity_Ores tTileEntity = (GT_TileEntity_Ores) 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<Integer, Integer> 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<Integer, Integer> 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<Double> P = new LinkedList<Double>(); + + // 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 GT_MetaTileEntity_ExtremeIndustrialGreenhouse} that holds the seed. + * @return The humidity environmental value at the controller's location. + */ + public static byte getHumidity(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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 GT_MetaTileEntity_ExtremeIndustrialGreenhouse} that holds the seed. + * @return The nutrient environmental value at the controller's location. + */ + public static byte getNutrients(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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 GT_MetaTileEntity_ExtremeIndustrialGreenhouse} that holds the seed. + * @return The air quality environmental value at the controller's location. + */ + public static byte getAirQuality(GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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<Block> reqBlockSet = new HashSet<>(); + public Set<String> reqBlockOreDict = new HashSet<>(); + private int lightLevel = 15; + + public FakeTileEntityCrop(EIGIC2Bucket bucket, GT_MetaTileEntity_ExtremeIndustrialGreenhouse 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(); + } + } + +} diff --git a/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGRainbowCactusBucket.java b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGRainbowCactusBucket.java new file mode 100644 index 0000000000..6342080722 --- /dev/null +++ b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGRainbowCactusBucket.java @@ -0,0 +1,77 @@ +package kubatech.tileentity.gregtech.multiblock.eigbuckets; + +import java.util.ArrayList; +import java.util.Random; + +import net.minecraft.block.Block; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import kubatech.api.eig.EIGBucket; +import kubatech.api.eig.EIGDropTable; +import kubatech.api.eig.IEIGBucketFactory; +import kubatech.tileentity.gregtech.multiblock.GT_MetaTileEntity_ExtremeIndustrialGreenhouse; +import tb.common.block.BlockRainbowCactus; +import tb.init.TBBlocks; + +// This is more of a proof of concept to show how to implement a custom handler. + +public class EIGRainbowCactusBucket extends EIGBucket { + + public final static IEIGBucketFactory factory = new EIGRainbowCactusBucket.Factory(); + private static final String NBT_IDENTIFIER = "TB:RAINCACTI"; + private static final int REVISION_NUMBER = 0; + + public static class Factory implements IEIGBucketFactory { + + @Override + public String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public EIGBucket tryCreateBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack input) { + // check if input is rainbow cacti; + if (!(Block.getBlockFromItem(input.getItem()) instanceof BlockRainbowCactus)) return null; + return new EIGRainbowCactusBucket(input, 1); + } + + @Override + public EIGBucket restore(NBTTagCompound nbt) { + return new EIGRainbowCactusBucket(nbt); + } + + } + + private final Random random = new Random(); + + public EIGRainbowCactusBucket(ItemStack seed, int seedCount) { + super(seed, seedCount, null); + } + + public EIGRainbowCactusBucket(NBTTagCompound nbt) { + super(nbt); + } + + @Override + public boolean revalidate(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + return this.isValid(); + } + + @Override + protected String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public void addProgress(double multiplier, EIGDropTable tracker) { + if (!this.isValid()) return; + // TODO: make the addDyeDropsToOutput static in TB. + ArrayList<ItemStack> drops = new ArrayList<>(); + ((BlockRainbowCactus) TBBlocks.rainbowCactus).addDyeDropsToOutput(this.random, drops); + for (ItemStack drop : drops) { + tracker.addDrop(drop, drop.stackSize * multiplier * this.seedCount); + } + } + +} diff --git a/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGSeedBucket.java b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGSeedBucket.java new file mode 100644 index 0000000000..51b4a7162a --- /dev/null +++ b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGSeedBucket.java @@ -0,0 +1,286 @@ +package kubatech.tileentity.gregtech.multiblock.eigbuckets; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Random; + +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.inventory.InventoryCrafting; +import net.minecraft.item.Item; +import net.minecraft.item.ItemSeedFood; +import net.minecraft.item.ItemSeeds; +import net.minecraft.item.ItemStack; +import net.minecraft.item.crafting.CraftingManager; +import net.minecraft.item.crafting.IRecipe; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; +import net.minecraftforge.common.IPlantable; + +import cpw.mods.fml.common.registry.GameRegistry; +import gregtech.api.util.GT_Utility; +import gregtech.common.GT_DummyWorld; +import kubatech.api.eig.EIGBucket; +import kubatech.api.eig.EIGDropTable; +import kubatech.api.eig.IEIGBucketFactory; +import kubatech.tileentity.gregtech.multiblock.GT_MetaTileEntity_ExtremeIndustrialGreenhouse; + +public class EIGSeedBucket extends EIGBucket { + + public static final IEIGBucketFactory factory = new EIGSeedBucket.Factory(); + private static final String NBT_IDENTIFIER = "SEED"; + private static final int REVISION_NUMBER = 0; + private static final int NUMBER_OF_DROPS_TO_SIMULATE = 1000; + private static final int FORTUNE_LEVEL = 0; + private static final EIGSeedBucket.GreenHouseWorld fakeWorld = new EIGSeedBucket.GreenHouseWorld(5, 5, 5); + + public static class Factory implements IEIGBucketFactory { + + @Override + public String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public EIGBucket tryCreateBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack input) { + return new EIGSeedBucket(greenhouse, input); + } + + @Override + public EIGBucket restore(NBTTagCompound nbt) { + return new EIGSeedBucket(nbt); + } + + } + + private boolean isValid = false; + private EIGDropTable drops = new EIGDropTable(); + + private EIGSeedBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack seed) { + super(seed, 1, null); + this.recalculateDrops(greenhouse); + } + + private EIGSeedBucket(NBTTagCompound nbt) { + super(nbt); + this.drops = new EIGDropTable(nbt, "drops"); + this.isValid = nbt.getInteger("version") == REVISION_NUMBER && !this.drops.isEmpty(); + } + + @Override + public NBTTagCompound save() { + NBTTagCompound nbt = super.save(); + nbt.setTag("drops", this.drops.save()); + nbt.setInteger("version", REVISION_NUMBER); + return nbt; + } + + @Override + protected String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public void addProgress(double multiplier, EIGDropTable tracker) { + if (!this.isValid()) return; + this.drops.addTo(tracker, multiplier * this.seedCount); + } + + @Override + public boolean isValid() { + return super.isValid() && this.isValid; + } + + @Override + public boolean revalidate(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + this.recalculateDrops(greenhouse); + return this.isValid(); + } + + public void recalculateDrops(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + this.isValid = false; + int optimalGrowthMetadata = 7; + // Get the relevant item and block for this item. + Item item = this.seed.getItem(); + Block block; + if (!(item instanceof IPlantable)) return; + if (item instanceof ItemSeeds) { + block = ((ItemSeeds) item).getPlant(fakeWorld, 0, 0, 0); + } else if (item instanceof ItemSeedFood) { + block = ((ItemSeedFood) item).getPlant(fakeWorld, 0, 0, 0); + } else { + // We can't plant it, we can't handle it, get out. + return; + } + + // Natura crops have an optimal harvest stage of 8. + GameRegistry.UniqueIdentifier u = GameRegistry.findUniqueIdentifierFor(item); + if (u != null && Objects.equals(u.modId, "Natura")) optimalGrowthMetadata = 8; + + // Pre-Generate drops. + EIGDropTable drops = new EIGDropTable(); + World world = greenhouse.getBaseMetaTileEntity() + .getWorld(); + for (int i = 0; i < NUMBER_OF_DROPS_TO_SIMULATE; i++) { + ArrayList<ItemStack> blockDrops = block.getDrops(world, 0, 0, 0, optimalGrowthMetadata, FORTUNE_LEVEL); + for (ItemStack drop : blockDrops) { + drops.addDrop(drop, drop.stackSize); + } + } + + // reduce the number of drops to account for the seeds + if (!removeSeedFromDrops(world, drops, this.seed, NUMBER_OF_DROPS_TO_SIMULATE)) return; + + // reduce drop count to account for the number of simulations + drops.entrySet() + .forEach(x -> x.setValue(x.getValue() / NUMBER_OF_DROPS_TO_SIMULATE)); + + // make sure we actually got a drop + if (drops.isEmpty()) return; + + // and we are good, see ya. + this.drops = drops; + this.isValid = true; + } + + private boolean removeSeedFromDrops(World world, EIGDropTable drops, ItemStack seed, int seedsToConsume) { + // make a safe copy of the seed just in case + ItemStack seedSafe = seed.copy(); + seedSafe.stackSize = 1; + // first check if we dropped an item identical to our seed item. + int inputSeedDropCountAfterRemoval = (int) Math.round(drops.getItemAmount(seedSafe)) - seedsToConsume; + if (inputSeedDropCountAfterRemoval > 0) { + drops.setItemAmount(seedSafe, inputSeedDropCountAfterRemoval); + } else { + drops.removeItem(seedSafe); + } + // return true if we were able to find enough seeds in the drops. + if (inputSeedDropCountAfterRemoval >= 0) return true; + + // else try to find items that can be crafted into the seed + int seedsToCraft = -inputSeedDropCountAfterRemoval; + IRecipe[] validRecipes = CraftingManager.getInstance() + .getRecipeList() + .parallelStream() + .filter(recipe -> GT_Utility.areStacksEqual(recipe.getRecipeOutput(), seed)) + .toArray(IRecipe[]::new); + + // if no recipes outputs the input seed, abort. + if (validRecipes.length == 0) return false; + + // check the recipes we found for one that can consume our seed + for (Iterator<Map.Entry<ItemStack, Double>> dropIterator = drops.entrySet() + .iterator(); dropIterator.hasNext();) { + Map.Entry<ItemStack, Double> entry = dropIterator.next(); + int inputCount = (int) Math.round(entry.getValue()); + ItemStack input = entry.getKey() + .copy(); + input.stackSize = 1; + EIGCraftingSeedFinder seedFinder = new EIGCraftingSeedFinder(input); + for (IRecipe recipe : validRecipes) { + if (recipe.matches(seedFinder, world)) { + // account for recipes that potentially drop more than 1 seed per input. + int outputsPerCraft = recipe.getCraftingResult(seedFinder).stackSize; + int craftableSeeds = outputsPerCraft * inputCount; + if (seedsToCraft >= craftableSeeds) { + // if the entire drop is consumed, just remove it from the list. + dropIterator.remove(); + seedsToCraft -= craftableSeeds; + if (seedsToCraft <= 0) { + return true; + } + } else { + // else remove the right amount from the drop, and get out. + entry.setValue(entry.getValue() - (double) seedsToCraft / outputsPerCraft); + return true; + } + } + } + } + + return false; + } + + class EIGCraftingSeedFinder extends InventoryCrafting { + + public ItemStack recipeInput; + + public EIGCraftingSeedFinder(ItemStack recipeInput) { + super(null, 3, 3); + this.recipeInput = recipeInput; + } + + @Override + public ItemStack getStackInSlot(int p_70301_1_) { + if (p_70301_1_ == 0) return this.recipeInput.copy(); + return null; + } + + @Override + public ItemStack getStackInSlotOnClosing(int par1) { + return null; + } + + @Override + public ItemStack decrStackSize(int par1, int par2) { + return null; + } + + @SuppressWarnings("EmptyMethod") + @Override + public void setInventorySlotContents(int par1, ItemStack par2ItemStack) {} + } + + private static class GreenHouseWorld extends GT_DummyWorld { + + public int x, y, z, meta = 0; + public Block block; + + GreenHouseWorld(int x, int y, int z) { + super(); + this.x = x; + this.y = y; + this.z = z; + this.rand = new EIGSeedBucket.GreenHouseRandom(); + } + + @Override + public int getBlockMetadata(int aX, int aY, int aZ) { + if (aX == x && aY == y && aZ == z) return 7; + return 0; + } + + @Override + public Block getBlock(int aX, int aY, int aZ) { + if (aY == y - 1) return Blocks.farmland; + return Blocks.air; + } + + @Override + public int getBlockLightValue(int p_72957_1_, int p_72957_2_, int p_72957_3_) { + return 10; + } + + @Override + public boolean setBlock(int aX, int aY, int aZ, Block aBlock, int aMeta, int aFlags) { + if (aBlock == Blocks.air) return false; + if (aX == x && aY == y && aZ == z) return false; + block = aBlock; + meta = aMeta; + return true; + } + } + + private static class GreenHouseRandom extends Random { + + private static final long serialVersionUID = -387271808935248890L; + + @Override + public int nextInt(int bound) { + return 0; + } + } + +} diff --git a/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGStemBucket.java b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGStemBucket.java new file mode 100644 index 0000000000..e0ebcf8652 --- /dev/null +++ b/src/main/java/kubatech/tileentity/gregtech/multiblock/eigbuckets/EIGStemBucket.java @@ -0,0 +1,158 @@ +package kubatech.tileentity.gregtech.multiblock.eigbuckets; + +import java.util.ArrayList; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockStem; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.common.IPlantable; + +import kubatech.api.IBlockStemAccesor; +import kubatech.api.eig.EIGBucket; +import kubatech.api.eig.EIGDropTable; +import kubatech.api.eig.IEIGBucketFactory; +import kubatech.tileentity.gregtech.multiblock.GT_MetaTileEntity_ExtremeIndustrialGreenhouse; + +public class EIGStemBucket extends EIGBucket { + + public final static IEIGBucketFactory factory = new EIGStemBucket.Factory(); + private static final String NBT_IDENTIFIER = "STEM"; + private static final int REVISION_NUMBER = 0; + private final static int NUMBER_OF_DROPS_TO_SIMULATE = 100; + + public static class Factory implements IEIGBucketFactory { + + @Override + public String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public EIGBucket tryCreateBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack input) { + // Check if input is a flower, reed or cacti. They all drop their source item multiplied by their seed count + Item item = input.getItem(); + if (!(item instanceof IPlantable)) return null; + Block block = ((IPlantable) item).getPlant( + greenhouse.getBaseMetaTileEntity() + .getWorld(), + 0, + 0, + 0); + if (!(block instanceof BlockStem)) return null; + return new EIGStemBucket(greenhouse, input); + } + + @Override + public EIGBucket restore(NBTTagCompound nbt) { + return new EIGStemBucket(nbt); + } + } + + private boolean isValid = false; + private EIGDropTable drops = new EIGDropTable(); + + private EIGStemBucket(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse, ItemStack input) { + super(input, 1, null); + recalculateDrops(greenhouse); + } + + private EIGStemBucket(NBTTagCompound nbt) { + super(nbt); + this.drops = new EIGDropTable(nbt, "drops"); + this.isValid = nbt.getInteger("version") == REVISION_NUMBER && !this.drops.isEmpty(); + } + + @Override + public NBTTagCompound save() { + NBTTagCompound nbt = super.save(); + if (this.drops != null) { + nbt.setTag("drops", this.drops.save()); + } + nbt.setInteger("version", REVISION_NUMBER); + return nbt; + } + + @Override + protected String getNBTIdentifier() { + return NBT_IDENTIFIER; + } + + @Override + public void addProgress(double multiplier, EIGDropTable tracker) { + if (!this.isValid()) return; + this.drops.addTo(tracker, multiplier * this.seedCount); + } + + @Override + public boolean isValid() { + return super.isValid() && this.isValid; + } + + @Override + public boolean revalidate(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + recalculateDrops(greenhouse); + return this.isValid(); + } + + /** + * Attempts to predetermine what item the stem crop will drop. + * + * @param greenhouse The greenhouse that houses this bucket. + */ + public void recalculateDrops(GT_MetaTileEntity_ExtremeIndustrialGreenhouse greenhouse) { + this.isValid = false; + Item item = this.seed.getItem(); + if (!(item instanceof IPlantable)) return; + Block stemBlock = ((IPlantable) item).getPlant( + greenhouse.getBaseMetaTileEntity() + .getWorld(), + 0, + 0, + 0); + if (!(stemBlock instanceof BlockStem)) return; + Block cropBlock = ((IBlockStemAccesor) stemBlock).getCropBlock(); + if (cropBlock == null || cropBlock == Blocks.air) return; + // if we know some crops needs a specific metadata, remap here + int metadata = 0; + + EIGDropTable drops = new EIGDropTable(); + + for (int i = 0; i < NUMBER_OF_DROPS_TO_SIMULATE; i++) { + // simulate 1 round of drops + ArrayList<ItemStack> blockDrops = cropBlock.getDrops( + greenhouse.getBaseMetaTileEntity() + .getWorld(), + greenhouse.getBaseMetaTileEntity() + .getXCoord(), + greenhouse.getBaseMetaTileEntity() + .getYCoord(), + greenhouse.getBaseMetaTileEntity() + .getZCoord(), + metadata, + 0); + if (blockDrops == null || blockDrops.isEmpty()) continue; + // if the droped item is a block that places itself, assume this is the only possible drop + // eg: pumpkin, redlon + if (i == 0 && blockDrops.size() == 1) { + ItemStack drop = blockDrops.get(0); + if (drop != null && drop.stackSize >= 1 && drop.getItem() == Item.getItemFromBlock(cropBlock)) { + drops.addDrop(drop, drop.stackSize); + break; + } + } + // else append all the drops + for (ItemStack drop : blockDrops) { + drops.addDrop(drop, drop.stackSize / (double) NUMBER_OF_DROPS_TO_SIMULATE); + } + } + // check that we did in fact drop something.s + if (drops.isEmpty()) return; + + // all checks passed we are good to go + this.drops = drops; + this.isValid = true; + } +} |