aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/common
diff options
context:
space:
mode:
authorNotAPenguin <michiel.vandeginste@gmail.com>2024-08-03 23:21:44 +0200
committerGitHub <noreply@github.com>2024-08-03 23:21:44 +0200
commitb65ace37de4f585b8089ad413ee877b792da11ca (patch)
tree2e0445785c5008df15151f729da148fb70dbc21d /src/main/java/gregtech/common
parente180e49fc6305f71b1f1b18141b946f794a7012b (diff)
downloadGT5-Unofficial-b65ace37de4f585b8089ad413ee877b792da11ca.tar.gz
GT5-Unofficial-b65ace37de4f585b8089ad413ee877b792da11ca.tar.bz2
GT5-Unofficial-b65ace37de4f585b8089ad413ee877b792da11ca.zip
Waterline rework (#2577)
* Add barebones PurificationPlant class * Make simple 3x3 structure to form purification plant * Add base purification unit class and dummy sifter unit MTE * Make sifter unit form * Fix accidental wildcard import * Implement basic linking of units to controller using data stick * Make linking more robust, save bidirectional links, add scanner output * add linking range, error message and unregister old controller when re-linking * Add link status of purification units to waila body * Disable maintenance issues on purification plant units * spotless * Check structure of linked purification units in main controller * Remove all star imports * Small refactor to avoid updating status from main controller * spotless * Attempt to document current code * Convert some comments to javadoc * Implement basic processing cycle, sync it with linked purification units * Make water purification plant drain power * Calculate power drain from active units and remove controller power drain * spotless * Add very barebones recipemap * Fix recipemap name in lang file * spotless * Fix purification unit recipemap name * spotless * more sane amount of max fluid outputs * add some item outputs to sifter unit * Very simple recipe processing, may be buggy * spotless * Implement recipe failure * Implement void protection for purification units * spotless * buff item output odds slightly * Add WIP grade 1 structure * spotless * Store base success chance in recipe metadata and display it in NEI * Fill sifter plant with water * Add comment * Allow construction sifter unit in survival * Implement water boost * Fix water boost allowing output chance to go over 100% * Implement failed recipes outputting lower tier water * Fix typo * Fix deformed purification unit still drawing power * Slightly refactor recipe check so base class can read result * Create empty ModularUI container for purification plant * The great gui struggle part 1 * More gui struggles, we have a button now * Adjust button text and size * gui wars: the rise of the sync * gui wars: a new hope * fix the sync * is pengu old enough to know exceeder? * Fix being able to link the same unit multiple times * Sync status string to client * Sign sifter with my name * Show status somewhat properly * Adjust sifter base chance and structure * Fully implement sifter unit * More tooltip refactoring * Add structure info to sifter tooltip. * nitpicking tooltips * Adding sound to Purification Plant Main Unit. * fix star imports * Add basic coagulator unit, add recipemap for it * Write coagulator tooltip * comma nitpicking * more tooltip work * small refactor to purification plant controller * start work on custom recipemap frontend * Fully implement coagulator * Update structure requirements in tooltips * Move controller text in structure tooltips to be consistent * fix NPE on world load * Add base ph adjustment unit MTE * Add info to main controller and energy hatch check * Fixing tooltip of Main Controller & Energy/Exotic Hatch check. * Create full pH adjustment structure * disallow any voiding on purification unit * Small custom RecipeMap frontend for ph adjustment * Generate random initial pH value * Implement inserting NaOH and HCl to adjust pH * Add easter egg * Implement pH sensor hatch * Properly consume HCl and round pH value to 2 digits * Write ph adjustment unit tooltip * Tooltip nitpicking * Try to fix some structurelib hints * More trying to fix hints * Add industrial strength concrete casing block * Add water loop sound to the game * Document random initial pH in tooltip * Add glass material base * Fix spotless formatting in Textures docs because I cannot take it anymore * Add glass texture * Try adding transparent glass * Transparent glass working * Create pH resistant glass and update pH structure to use it * Create full structure for main purification plant * Create custom water purification casing block * Properly balance ferrous wastewater reprocessing and reduce input by a factor 10 * Add pH factor to NEI tooltip and fix coagulator structure * Structure tooltip for Purification Plant base * Add GT_Block_Glass2 and properly set maxMeta * Add Tinted Industrial Glass blocks * Fix BlockCasing9 not showing custom tooltip * Register tinted glass as EV glass * Add sterile water plant casing and revert tooltip change * Mention required water in sifter tooltip * Add more textures and casings * Add more textures, sounds and add structure info for pH adjustment * Rename sifter unit to clarifier * Rename coagulation unit to flocculation unit * Add activated carbon line * Fix unintended activated carbon recipe * Add activated carbon filter * Add polyaluminium chloride + solution * Add new custom textures by @BlueHero233 * Wip recipe page with new background for flocculation * Fix flocculation background image mostly * Finally aligned the slots * angery mumbles * Finish flocculation recipe page * All the recipe pages! * Add new reworked textures * Fix ph adjustment being t3 instead of t4 * Fix invisible casing * apply chembalance to polyaluminium chloride properly * Fix ferrous wastewater -> flocculation waste liquid * Move flocculation to grade 3 * create ozonation unit with placeholder blocks * add new blocks for ozonation with placeholder textures * Add water to ozonation structure * Create ozone gas material * Add ozone recipe * Add textures for ozone unit * Add sound loop for ozonation * fix * implement ozonation mechanics * Finalize ozonation tooltip * Create dummy plasma heater multi * Update textures for plasma heater * Add grade 5 recipemap * Add hatches to plasma heater multi * Add basic plasma heating unit variables * Implement plasma heating unit mechanics * Add plasma heater tooltip * Add structure info to plasma heater tooltip * fix ozonation tooltip, add frontend * Fix positioning on ozonation tooltip and fix plasma heater crash * Add UV treatment MTE and structure without textures * Revert accidental addition of debug dependencies * Add initial version of uv unit textures * update naquadria casing, add water color gradient * Some minor cleanup and added docs * Create lens housing bus * Add lens bus to UV treatment unit * Add lens indicator hatch * Merge GT_MetaGeneratedItem_03.java * Add lens indicator hatch * Add the lens cycle, uv treatment recipe map and fix eut of flocculation recipe * Implement lens swapping mechanic * Clean up first lens swap * Fix uv recipemap lang and move lens cycle to recipe * Write uv treatment tooltip * Add sounds for uv and plasma steps * Create empty degasifier class * Create temporary debug structure for degasifier * set temp casing index for degasifier * create degasifier control hatch * create slightly less temporary but still temporary structure for degasifier * Start impl of degasifier * fix fluid consumption and nbt loading of degasifier * Degasifier implementation should work now * Rename and reformat some things and start work on degasser tooltiop * give last bit much lower chance of being on to avoid cheesing * Finish degasifier tooltip * Integrate some deleno lore * hopefully fix all moved casing meta ids after merge * Create finalized degasser structure * Integrate more deleno lore * Add even more lore * Create placeholder particle catalysts and fetch particle items from gt++ * Fix wrong casing and recipemap localization * Create parallel config menu * refactor purification recipecheck slightly * implement parallel amount on water i/o and power * add tooltip info about parallel config * fix text * update block names in structure tooltips * create structure tooltip for degasser * create textureless quark catalyst items * add the purple glass * fix lore typos * fix some casing indices * remove concrete floor from water plant and reword tooltip * fix main plant structure and add placeholder structure for t8 step * fix structurecheck for main plant and add random catalyst generation for t8 * implement basic mechanics for particle extractor (wip) * Create plasma heater frontend * implement final mechanics and bugfixes for particle extractor * add recipes for re-aligning quark catalysts * add simple recipes for catalyst alignment * initial replacement of purified water in engraver recipes * add purified water to all wafer cutting recipes * fix purified water amounts * buff quark cyclotron recipe again * extract t8 unit casings into their own icons * Write initial tooltip for t8 module * add purified water to mask recipes * Add recipe comparator to show low tier purified water recipes first * add min casing check to waterline multis * buff ozone production * update t8 structure * make purified water optional again for naq wafers * Fix blockrenderer for purification plant * fix nei previews * fix nei * really fix nei this time * add t8 lore * fix hatch recipe locking blocking automation on some steps * try to solve weirdness with grade 3 recipe * fix issues with recipecheck * fix missing null check * make ph sensor use a strict inequality check * fix min casings on t5 * significantly nerf purified water usage for beamline masks * disable void protection for waterline * small adjustments to t6 unit * more small adjustments to t6 unit to prevent easy automation cheese * fix degasser redstone output and missing return statement * remove water QFT catalyst recipes --------- Co-authored-by: Tianyou Mei <meitianyou94@gmail.com> Co-authored-by: OlliedeLeeuw <ollie.riemersma@xs4all.nl> Co-authored-by: Ollie_de_Leeuw <154506304+OlliedeLeeuw@users.noreply.github.com> Co-authored-by: Martin Robertz <dream-master@gmx.net>
Diffstat (limited to 'src/main/java/gregtech/common')
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Block_Casings10.java4
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Block_Casings9.java51
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Block_Glass1.java106
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Block_TintedIndustrialGlass.java98
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Item_Casings9.java14
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Item_Glass1.java15
-rw-r--r--src/main/java/gregtech/common/blocks/GT_Item_TintedIndustrialGlass.java10
-rw-r--r--src/main/java/gregtech/common/gui/modularui/widget/TextButtonWidget.java57
-rw-r--r--src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java65
-rw-r--r--src/main/java/gregtech/common/items/ID_MetaItem_03.java9
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_Hatch_DegasifierControlHatch.java122
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensHousing.java52
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensIndicator.java117
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationPlant.java740
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitBase.java793
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitClarifier.java333
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitDegasifier.java838
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitFlocculation.java496
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitOzonation.java299
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitParticleExtractor.java484
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPhAdjustment.java609
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPlasmaHeater.java570
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitUVTreatment.java524
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_pHSensor.java196
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/LinkedPurificationUnit.java125
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationPlantStructureString.java15
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationUnitStatus.java14
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/PurifiedWaterHelpers.java35
-rw-r--r--src/main/java/gregtech/common/tileentities/machines/multi/purification/UVTreatmentLensCycle.java39
29 files changed, 6829 insertions, 1 deletions
diff --git a/src/main/java/gregtech/common/blocks/GT_Block_Casings10.java b/src/main/java/gregtech/common/blocks/GT_Block_Casings10.java
index 660c709886..83219f45ee 100644
--- a/src/main/java/gregtech/common/blocks/GT_Block_Casings10.java
+++ b/src/main/java/gregtech/common/blocks/GT_Block_Casings10.java
@@ -20,7 +20,10 @@ public class GT_Block_Casings10 extends GT_Block_Casings_Abstract {
GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".0.name", "MagTech Casing");
GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".1.name", "Laser Containment Casing");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".2.name", "Quark Exclusion Casing");
+
ItemList.Casing_Electromagnetic_Separator.set(new ItemStack(this, 1, 0));
+ ItemList.BlockQuarkContainmentCasing.set(new ItemStack(this, 1, 2));
ItemList.Casing_Laser.set(new ItemStack(this, 1, 1));
}
@@ -35,6 +38,7 @@ public class GT_Block_Casings10 extends GT_Block_Casings_Abstract {
return switch (aMeta) {
case 0 -> Textures.BlockIcons.MACHINE_CASING_EMS.getIcon();
case 1 -> Textures.BlockIcons.MACHINE_CASING_LASER.getIcon();
+ case 2 -> Textures.BlockIcons.BLOCK_QUARK_CONTAINMENT_CASING.getIcon();
default -> Textures.BlockIcons.MACHINE_CASING_ROBUST_TUNGSTENSTEEL.getIcon();
};
}
diff --git a/src/main/java/gregtech/common/blocks/GT_Block_Casings9.java b/src/main/java/gregtech/common/blocks/GT_Block_Casings9.java
index 4441885f30..6f33cda013 100644
--- a/src/main/java/gregtech/common/blocks/GT_Block_Casings9.java
+++ b/src/main/java/gregtech/common/blocks/GT_Block_Casings9.java
@@ -22,15 +22,51 @@ public class GT_Block_Casings9 extends GT_Block_Casings_Abstract {
GT_LanguageManager
.addStringLocalization(getUnlocalizedName() + ".1.tooltip", "Less than five 0.1μm particles per m^3");
GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".2.name", "Primitive Wooden Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".3.name", "Superplasticizer-Treated High Strength Concrete");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".4.name", "Sterile Water Plant Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".5.name", "Reinforced Sterile Water Plant Casing");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".6.name", "Slick Sterile Flocculation Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".7.name", "Stabilized Naquadah Water Plant Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".8.name", "Inert Neutralization Water Plant Casing");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".9.name", "Reactive Gas Containment Casing");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".10.name", "Inert Filtration Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".11.name", "Heat-Resistant Trinium Plated Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".12.name", "Naquadria-Reinforced Water Plant Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".13.name", "High Energy Ultraviolet Emitter Casing");
+ // placeholder name
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".14.name", "Particle Beam Guidance Pipe Casing");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".15.name", "Femtometer-Calibrated Particle Beam Casing");
ItemList.Casing_Pipe_Polybenzimidazole.set(new ItemStack(this, 1, 0));
ItemList.Casing_Vent_T2.set(new ItemStack(this, 1, 1));
ItemList.WoodenCasing.set(new ItemStack(this, 1, 2));
+ ItemList.BlockIndustrialStrengthConcrete.set(new ItemStack(this, 1, 3));
+ ItemList.BlockIndustrialWaterPlantCasing.set(new ItemStack(this, 1, 4));
+ ItemList.BlockSterileWaterPlantCasing.set(new ItemStack(this, 1, 5));
+ ItemList.BlockFlocculationCasing.set(new ItemStack(this, 1, 6));
+ ItemList.BlockNaquadahReinforcedWaterPlantCasing.set(new ItemStack(this, 1, 7));
+ ItemList.BlockExtremeCorrosionResistantCasing.set(new ItemStack(this, 1, 8));
+ ItemList.BlockHighPressureResistantCasing.set(new ItemStack(this, 1, 9));
+ ItemList.BlockOzoneCasing.set(new ItemStack(this, 1, 10));
+ ItemList.BlockPlasmaHeatingCasing.set(new ItemStack(this, 1, 11));
+ ItemList.BlockNaquadriaReinforcedWaterPlantCasing.set(new ItemStack(this, 1, 12));
+ ItemList.BlockUltraVioletLaserEmitter.set(new ItemStack(this, 1, 13));
+ ItemList.BlockQuarkPipe.set(new ItemStack(this, 1, 14));
+ ItemList.BlockQuarkReleaseChamber.set(new ItemStack(this, 1, 15));
}
@Override
public int getTextureIndex(int aMeta) {
- return (1 << 7) | (aMeta + 64);
+ return (16 << 7) | (aMeta + 16);
}
@Override
@@ -41,6 +77,19 @@ public class GT_Block_Casings9 extends GT_Block_Casings_Abstract {
case 1 -> Textures.BlockIcons.MACHINE_CASING_VENT_T2.getIcon();
case 2 -> ordinalSide >= 2 ? Textures.BlockIcons.TEXTURE_METAL_PANEL_E.getIcon()
: Textures.BlockIcons.TEXTURE_METAL_PANEL_E_A.getIcon();
+ case 3 -> Textures.BlockIcons.INDUSTRIAL_STRENGTH_CONCRETE.getIcon();
+ case 4 -> Textures.BlockIcons.MACHINE_CASING_INDUSTRIAL_WATER_PLANT.getIcon();
+ case 5 -> Textures.BlockIcons.WATER_PLANT_CONCRETE_CASING.getIcon();
+ case 6 -> Textures.BlockIcons.MACHINE_CASING_FLOCCULATION.getIcon();
+ case 7 -> Textures.BlockIcons.MACHINE_CASING_NAQUADAH_REINFORCED_WATER_PLANT.getIcon();
+ case 8 -> Textures.BlockIcons.MACHINE_CASING_EXTREME_CORROSION_RESISTANT.getIcon();
+ case 9 -> Textures.BlockIcons.MACHINE_CASING_HIGH_PRESSURE_RESISTANT.getIcon();
+ case 10 -> Textures.BlockIcons.MACHINE_CASING_OZONE.getIcon();
+ case 11 -> Textures.BlockIcons.MACHINE_CASING_PLASMA_HEATER.getIcon();
+ case 12 -> Textures.BlockIcons.NAQUADRIA_REINFORCED_WATER_PLANT_CASING.getIcon();
+ case 13 -> Textures.BlockIcons.UV_BACKLIGHT_STERILIZER_CASING.getIcon();
+ case 14 -> Textures.BlockIcons.BLOCK_QUARK_PIPE.getIcon();
+ case 15 -> Textures.BlockIcons.BLOCK_QUARK_RELEASE_CHAMBER.getIcon();
default -> Textures.BlockIcons.MACHINE_CASING_ROBUST_TUNGSTENSTEEL.getIcon();
};
}
diff --git a/src/main/java/gregtech/common/blocks/GT_Block_Glass1.java b/src/main/java/gregtech/common/blocks/GT_Block_Glass1.java
new file mode 100644
index 0000000000..0665411259
--- /dev/null
+++ b/src/main/java/gregtech/common/blocks/GT_Block_Glass1.java
@@ -0,0 +1,106 @@
+package gregtech.common.blocks;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.material.Material;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Facing;
+import net.minecraft.util.IIcon;
+import net.minecraft.world.IBlockAccess;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Textures;
+import gregtech.api.util.GT_LanguageManager;
+
+/**
+ * The glass is split into separate files because they are registered as regular blocks, and a regular block can have
+ * 16 subtypes at most.
+ *
+ * This class hosts various special types of tiered glass with not many tiers.
+ */
+public class GT_Block_Glass1 extends GT_Block_Casings_Abstract {
+
+ public GT_Block_Glass1() {
+ super(GT_Item_Glass1.class, "gt.blockglass1", Material.glass, 4);
+ this.opaque = false;
+
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".0.name", "Chemical Grade Glass");
+ GT_LanguageManager.addStringLocalization(
+ getUnlocalizedName() + ".0.tooltip",
+ "Able to resist the most extreme chemical conditions.");
+
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".1.name", "Electron-Permeable Neutronium Coated Glass");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".2.name", "Omni-Purpose Infinity Fused Glass");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".3.name", "Non-Photonic Matter Exclusion Glass");
+
+ ItemList.GlassPHResistant.set(new ItemStack(this, 1, 0));
+ ItemList.GlassUVResistant.set(new ItemStack(this, 1, 1));
+ ItemList.GlassOmniPurposeInfinityFused.set(new ItemStack(this, 1, 2));
+ ItemList.GlassQuarkContainment.set(new ItemStack(this, 1, 3));
+ }
+
+ @Override
+ public int getTextureIndex(int aMeta) {
+ // Page 16, 0-16
+ return (16 << 7) | (aMeta);
+ }
+
+ @Override
+ public boolean isNormalCube(IBlockAccess aWorld, int aX, int aY, int aZ) {
+ return false;
+ }
+
+ @Override
+ public boolean isOpaqueCube() {
+ return false;
+ }
+
+ @Override
+ @SideOnly(Side.CLIENT)
+ public int getRenderBlockPass() {
+ return 1;
+ }
+
+ @Override
+ public boolean renderAsNormalBlock() {
+ return false;
+ }
+
+ @Override
+ @SideOnly(Side.CLIENT)
+ public IIcon getIcon(int ordinalSide, int aMeta) {
+ return switch (aMeta) {
+ case 0 -> Textures.BlockIcons.GLASS_PH_RESISTANT.getIcon();
+ case 1 -> Textures.BlockIcons.NEUTRONIUM_COATED_UV_RESISTANT_GLASS.getIcon();
+ case 2 -> Textures.BlockIcons.OMNI_PURPOSE_INFINITY_FUSED_GLASS.getIcon();
+ case 3 -> Textures.BlockIcons.GLASS_QUARK_CONTAINMENT.getIcon();
+ default -> Textures.BlockIcons.MACHINE_CASING_ROBUST_TUNGSTENSTEEL.getIcon();
+ };
+ }
+
+ /**
+ * Returns true if the given side of this block type should be rendered, if the adjacent block is at the given
+ * coordinates. Args: blockAccess, x, y, z, side
+ */
+ @SideOnly(Side.CLIENT)
+ @Override
+ public boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) {
+ Block block = worldIn.getBlock(x, y, z);
+
+ if (worldIn.getBlockMetadata(x, y, z) != worldIn.getBlockMetadata(
+ x - Facing.offsetsXForSide[side],
+ y - Facing.offsetsYForSide[side],
+ z - Facing.offsetsZForSide[side])) {
+ return true;
+ }
+
+ if (block == this) {
+ return false;
+ }
+
+ return super.shouldSideBeRendered(worldIn, x, y, z, side);
+ }
+}
diff --git a/src/main/java/gregtech/common/blocks/GT_Block_TintedIndustrialGlass.java b/src/main/java/gregtech/common/blocks/GT_Block_TintedIndustrialGlass.java
new file mode 100644
index 0000000000..93c6ebb298
--- /dev/null
+++ b/src/main/java/gregtech/common/blocks/GT_Block_TintedIndustrialGlass.java
@@ -0,0 +1,98 @@
+package gregtech.common.blocks;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.material.Material;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Facing;
+import net.minecraft.util.IIcon;
+import net.minecraft.world.IBlockAccess;
+import net.minecraftforge.oredict.OreDictionary;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Textures;
+import gregtech.api.util.GT_LanguageManager;
+
+public class GT_Block_TintedIndustrialGlass extends GT_Block_Casings_Abstract {
+
+ public GT_Block_TintedIndustrialGlass() {
+ super(GT_Item_Glass1.class, "gt.blocktintedglass", Material.glass, 4);
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".0.name", "Tinted Industrial Glass (White)");
+ GT_LanguageManager
+ .addStringLocalization(getUnlocalizedName() + ".1.name", "Tinted Industrial Glass (Light Gray)");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".2.name", "Tinted Industrial Glass (Gray)");
+ GT_LanguageManager.addStringLocalization(getUnlocalizedName() + ".3.name", "Tinted Industrial Glass (Black)");
+ ItemList.GlassTintedIndustrialWhite.set(new ItemStack(this, 1, 0));
+ ItemList.GlassTintedIndustrialLightGray.set(new ItemStack(this, 1, 1));
+ ItemList.GlassTintedIndustrialGray.set(new ItemStack(this, 1, 2));
+ ItemList.GlassTintedIndustrialBlack.set(new ItemStack(this, 1, 3));
+
+ // Register tinted industrial glass as EV-Tier glass
+ OreDictionary.registerOre("blockGlassEV", this);
+
+ this.opaque = false;
+ }
+
+ @Override
+ public int getTextureIndex(int aMeta) {
+ // Page 16, 32-47
+ return (16 << 7) | (aMeta + 32);
+ }
+
+ @Override
+ public boolean isNormalCube(IBlockAccess aWorld, int aX, int aY, int aZ) {
+ return false;
+ }
+
+ @Override
+ public boolean isOpaqueCube() {
+ return false;
+ }
+
+ @Override
+ @SideOnly(Side.CLIENT)
+ public int getRenderBlockPass() {
+ return 1;
+ }
+
+ @Override
+ public boolean renderAsNormalBlock() {
+ return false;
+ }
+
+ @Override
+ @SideOnly(Side.CLIENT)
+ public IIcon getIcon(int ordinalSide, int aMeta) {
+ return switch (aMeta) {
+ case 0 -> Textures.BlockIcons.GLASS_TINTED_INDUSTRIAL_WHITE.getIcon();
+ case 1 -> Textures.BlockIcons.GLASS_TINTED_INDUSTRIAL_LIGHT_GRAY.getIcon();
+ case 2 -> Textures.BlockIcons.GLASS_TINTED_INDUSTRIAL_GRAY.getIcon();
+ case 3 -> Textures.BlockIcons.GLASS_TINTED_INDUSTRIAL_BLACK.getIcon();
+ default -> Textures.BlockIcons.MACHINE_CASING_ROBUST_TUNGSTENSTEEL.getIcon();
+ };
+ }
+
+ /**
+ * Returns true if the given side of this block type should be rendered, if the adjacent block is at the given
+ * coordinates. Args: blockAccess, x, y, z, side
+ */
+ @SideOnly(Side.CLIENT)
+ @Override
+ public boolean shouldSideBeRendered(IBlockAccess worldIn, int x, int y, int z, int side) {
+ Block block = worldIn.getBlock(x, y, z);
+
+ if (worldIn.getBlockMetadata(x, y, z) != worldIn.getBlockMetadata(
+ x - Facing.offsetsXForSide[side],
+ y - Facing.offsetsYForSide[side],
+ z - Facing.offsetsZForSide[side])) {
+ return true;
+ }
+
+ if (block == this) {
+ return false;
+ }
+
+ return super.shouldSideBeRendered(worldIn, x, y, z, side);
+ }
+}
diff --git a/src/main/java/gregtech/common/blocks/GT_Item_Casings9.java b/src/main/java/gregtech/common/blocks/GT_Item_Casings9.java
index 678a2424be..e9b39ee2cd 100644
--- a/src/main/java/gregtech/common/blocks/GT_Item_Casings9.java
+++ b/src/main/java/gregtech/common/blocks/GT_Item_Casings9.java
@@ -11,4 +11,18 @@ public class GT_Item_Casings9 extends GT_Item_Casings_Abstract {
public GT_Item_Casings9(Block block) {
super(block);
}
+
+ /*
+ * @Override
+ * public void addInformation(ItemStack aStack, EntityPlayer aPlayer, List<String> aList, boolean aF3_H) {
+ * // Add tooltip info if it was given
+ * String localizedTooltip = GT_LanguageManager.getTranslation(aStack.getUnlocalizedName() + ".tooltip");
+ * // getTranslation returns the key if no translation was found, but this just means
+ * // no tooltip was set.
+ * if (localizedTooltip.startsWith("gt.")) {
+ * aList.add(localizedTooltip);
+ * }
+ * super.addInformation(aStack, aPlayer, aList, aF3_H);
+ * }
+ */
}
diff --git a/src/main/java/gregtech/common/blocks/GT_Item_Glass1.java b/src/main/java/gregtech/common/blocks/GT_Item_Glass1.java
new file mode 100644
index 0000000000..7fbb33ff6e
--- /dev/null
+++ b/src/main/java/gregtech/common/blocks/GT_Item_Glass1.java
@@ -0,0 +1,15 @@
+package gregtech.common.blocks;
+
+import net.minecraft.block.Block;
+
+/**
+ * The glass types are split into separate files because they are registered as regular blocks, and a regular block can
+ * have
+ * 16 subtypes at most.
+ */
+public class GT_Item_Glass1 extends GT_Item_Casings_Abstract {
+
+ public GT_Item_Glass1(Block block) {
+ super(block);
+ }
+}
diff --git a/src/main/java/gregtech/common/blocks/GT_Item_TintedIndustrialGlass.java b/src/main/java/gregtech/common/blocks/GT_Item_TintedIndustrialGlass.java
new file mode 100644
index 0000000000..9f633922ac
--- /dev/null
+++ b/src/main/java/gregtech/common/blocks/GT_Item_TintedIndustrialGlass.java
@@ -0,0 +1,10 @@
+package gregtech.common.blocks;
+
+import net.minecraft.block.Block;
+
+public class GT_Item_TintedIndustrialGlass extends GT_Item_Casings_Abstract {
+
+ public GT_Item_TintedIndustrialGlass(Block block) {
+ super(block);
+ }
+}
diff --git a/src/main/java/gregtech/common/gui/modularui/widget/TextButtonWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/TextButtonWidget.java
new file mode 100644
index 0000000000..492eef0a0a
--- /dev/null
+++ b/src/main/java/gregtech/common/gui/modularui/widget/TextButtonWidget.java
@@ -0,0 +1,57 @@
+package gregtech.common.gui.modularui.widget;
+
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.api.widget.Widget;
+import com.gtnewhorizons.modularui.common.widget.ButtonWidget;
+import com.gtnewhorizons.modularui.common.widget.MultiChildWidget;
+import com.gtnewhorizons.modularui.common.widget.TextWidget;
+
+public class TextButtonWidget extends MultiChildWidget {
+
+ private ButtonWidget mButton;
+ private TextWidget mText;
+
+ private int leftMargin;
+
+ public TextButtonWidget(String text) {
+ mButton = new ButtonWidget();
+ mText = new TextWidget(text);
+ mText.setPos(0, 0);
+ mButton.setPos(0, 0);
+
+ this.addChild(mButton);
+ this.addChild(mText);
+ }
+
+ public ButtonWidget button() {
+ return mButton;
+ }
+
+ public TextWidget text() {
+ return mText;
+ }
+
+ @Override
+ public Widget setPos(int x, int y) {
+ return super.setPos(x, y);
+ }
+
+ @Override
+ public Widget setSize(int width, int height) {
+ this.mButton.setSize(width, height);
+ this.mText.setSize(width, height);
+ return super.setSize(width, height);
+ }
+
+ @Override
+ public Widget setSize(Size size) {
+ this.mButton.setSize(size);
+ return super.setSize(size);
+ }
+
+ public TextButtonWidget setLeftMargin(int margin) {
+ this.leftMargin = margin;
+ this.mText.setPos(this.leftMargin, this.mText.getPos().y);
+ return this;
+ }
+}
diff --git a/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java b/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java
index 49419aa47f..52a268baf9 100644
--- a/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java
+++ b/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java
@@ -22,6 +22,7 @@ import static gregtech.client.GT_TooltipHandler.Tier.UV;
import static gregtech.client.GT_TooltipHandler.Tier.UXV;
import static gregtech.client.GT_TooltipHandler.Tier.ZPM;
import static gregtech.client.GT_TooltipHandler.registerTieredTooltip;
+import static gregtech.common.items.ID_MetaItem_03.Activated_Carbon_Filter_Mesh;
import static gregtech.common.items.ID_MetaItem_03.Circuit_Biomainframe;
import static gregtech.common.items.ID_MetaItem_03.Circuit_Bioprocessor;
import static gregtech.common.items.ID_MetaItem_03.Circuit_Biowarecomputer;
@@ -203,6 +204,14 @@ import static gregtech.common.items.ID_MetaItem_03.NuclearStar;
import static gregtech.common.items.ID_MetaItem_03.Optical_Cpu_Containment_Housing;
import static gregtech.common.items.ID_MetaItem_03.Optically_Compatible_Memory;
import static gregtech.common.items.ID_MetaItem_03.Optically_Perfected_CPU;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Catalyst_Housing;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Bottom;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Charm;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Down;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Strange;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Top;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Unaligned;
+import static gregtech.common.items.ID_MetaItem_03.Quark_Creation_Catalyst_Up;
import static gregtech.common.items.ID_MetaItem_03.Spinneret;
import static gregtech.common.items.ID_MetaItem_03.Timepiece;
import static gregtech.common.items.ID_MetaItem_03.Transdimensional_Alignment_Matrix;
@@ -1103,6 +1112,62 @@ public class GT_MetaGenerated_Item_03 extends GT_MetaGenerated_Item_X32 {
"Removing this cover will destroy the linked card",
GT_Values.AuthorQuerns)));
+ ItemList.ActivatedCarbonFilterMesh.set(
+ addItem(
+ Activated_Carbon_Filter_Mesh.ID,
+ "Activated Carbon Filter Mesh",
+ "The most granular filter you could possibly make.",
+ SubTag.NO_UNIFICATION));
+
+ ItemList.Quark_Catalyst_Housing.set(
+ addItem(
+ Quark_Catalyst_Housing.ID,
+ "Empty Quark Release Catalyst Housing",
+ "Capable of holding Quark Release Catalysts",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Up.set(
+ addItem(
+ Quark_Creation_Catalyst_Up.ID,
+ "Up-Quark Releasing Catalyst",
+ "Can release up-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Down.set(
+ addItem(
+ Quark_Creation_Catalyst_Down.ID,
+ "Down-Quark Releasing Catalyst",
+ "Can release down-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Strange.set(
+ addItem(
+ Quark_Creation_Catalyst_Strange.ID,
+ "Strange-Quark Releasing Catalyst",
+ "Can release strange-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Charm.set(
+ addItem(
+ Quark_Creation_Catalyst_Charm.ID,
+ "Charm-Quark Releasing Catalyst",
+ "Can release charm-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Bottom.set(
+ addItem(
+ Quark_Creation_Catalyst_Bottom.ID,
+ "Top-Quark Releasing Catalyst",
+ "Can release top-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Top.set(
+ addItem(
+ Quark_Creation_Catalyst_Top.ID,
+ "Bottom-Quark Releasing Catalyst",
+ "Can release bottom-quarks into environment to reshape matter",
+ SubTag.NO_UNIFICATION));
+ ItemList.Quark_Creation_Catalyst_Unaligned.set(
+ addItem(
+ Quark_Creation_Catalyst_Unaligned.ID,
+ "Unaligned Quark Releasing Catalyst",
+ "Needs to be realigned before use",
+ SubTag.NO_UNIFICATION));
+
ItemList.Optical_Cpu_Containment_Housing
.set(addItem(Optical_Cpu_Containment_Housing.ID, "Optical CPU Containment Housing", "CPU Housing", o));
ItemList.Optically_Perfected_CPU
diff --git a/src/main/java/gregtech/common/items/ID_MetaItem_03.java b/src/main/java/gregtech/common/items/ID_MetaItem_03.java
index fac8fd3841..fea5a687df 100644
--- a/src/main/java/gregtech/common/items/ID_MetaItem_03.java
+++ b/src/main/java/gregtech/common/items/ID_MetaItem_03.java
@@ -183,6 +183,15 @@ public enum ID_MetaItem_03 {
NuclearStar(230),
IndustrialApiary_Upgrade_UNLIGHT(231),
Cover_Metrics_Transmitter(232),
+ Activated_Carbon_Filter_Mesh(233),
+ Quark_Catalyst_Housing(234),
+ Quark_Creation_Catalyst_Up(235),
+ Quark_Creation_Catalyst_Down(236),
+ Quark_Creation_Catalyst_Strange(237),
+ Quark_Creation_Catalyst_Charm(238),
+ Quark_Creation_Catalyst_Bottom(239),
+ Quark_Creation_Catalyst_Top(240),
+ Quark_Creation_Catalyst_Unaligned(241),
Circuit_Silicon_Ingot6(721),
Circuit_Silicon_Wafer6(722),
Circuit_Silicon_Wafer7(723),
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_Hatch_DegasifierControlHatch.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_Hatch_DegasifierControlHatch.java
new file mode 100644
index 0000000000..3d49f05dca
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_Hatch_DegasifierControlHatch.java
@@ -0,0 +1,122 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import gregtech.api.enums.Textures;
+import gregtech.api.interfaces.IIconContainer;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.render.TextureFactory;
+
+public class GT_MetaTileEntity_Hatch_DegasifierControlHatch extends GT_MetaTileEntity_Hatch {
+
+ private byte outputStrength = 0;
+
+ private static final IIconContainer textureFont = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR;
+ private static final IIconContainer textureFont_Glow = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR_GLOW;
+
+ public GT_MetaTileEntity_Hatch_DegasifierControlHatch(int aID, String aName, String aNameRegional, int aTier) {
+ super(aID, aName, aNameRegional, aTier, 0, "Outputs a control signal for the Degasser Purification Unit");
+ }
+
+ public GT_MetaTileEntity_Hatch_DegasifierControlHatch(String aName, int aTier, String[] aDescription,
+ ITexture[][][] aTextures) {
+ super(aName, aTier, 0, aDescription, aTextures);
+ }
+
+ @Override
+ public boolean isValidSlot(int aIndex) {
+ return false;
+ }
+
+ @Override
+ public boolean isSimpleMachine() {
+ return true;
+ }
+
+ @Override
+ public boolean allowGeneralRedstoneOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean allowPullStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection Side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public boolean allowPutStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public boolean isFacingValid(ForgeDirection facing) {
+ return true;
+ }
+
+ @Override
+ public void initDefaultModes(NBTTagCompound aNBT) {
+ getBaseMetaTileEntity().setActive(true);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_Hatch_DegasifierControlHatch(mName, mTier, mDescriptionArray, mTextures);
+ }
+
+ @Override
+ public String[] getDescription() {
+ return new String[] { "Can be installed in the Degasser Purification Unit.",
+ "Outputs Redstone Signal Strength based on the current control signal." };
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ outputStrength = aNBT.getByte("mOutputStrength");
+ super.loadNBTData(aNBT);
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ aNBT.setByte("mOutputStrength", outputStrength);
+ super.saveNBTData(aNBT);
+ }
+
+ // Pass zero signal to disable output
+ public void updateOutputSignal(byte signal) {
+ outputStrength = signal;
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ if (outputStrength > 0) {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setStrongOutputRedstoneSignal(side, outputStrength);
+ }
+ } else {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setStrongOutputRedstoneSignal(side, (byte) 0);
+ }
+ }
+ super.onPostTick(aBaseMetaTileEntity, aTick);
+ }
+
+ @Override
+ public ITexture[] getTexturesActive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont), TextureFactory.builder()
+ .addIcon(textureFont_Glow)
+ .glow()
+ .build() };
+ }
+
+ @Override
+ public ITexture[] getTexturesInactive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont) };
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensHousing.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensHousing.java
new file mode 100644
index 0000000000..bd025e0e69
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensHousing.java
@@ -0,0 +1,52 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.MetaTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_InputBus;
+import gregtech.client.GT_TooltipHandler;
+
+public class GT_MetaTileEntity_LensHousing extends GT_MetaTileEntity_Hatch_InputBus {
+
+ public GT_MetaTileEntity_LensHousing(int id, String name, String nameRegional) {
+ super(
+ id,
+ name,
+ nameRegional,
+ GT_TooltipHandler.Tier.UV.ordinal(),
+ 1,
+ new String[] { "Holds a lens for UV laser focusing." });
+ }
+
+ public GT_MetaTileEntity_LensHousing(String aName, int aTier, String[] aDescription, ITexture[][][] aTextures) {
+ super(aName, aTier, aDescription, aTextures);
+ }
+
+ @Override
+ public MetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_LensHousing(this.mName, this.mTier, this.mDescriptionArray, this.mTextures);
+ }
+
+ @Override
+ public int getSizeInventory() {
+ return 1;
+ }
+
+ @Override
+ public int getCircuitSlot() {
+ return -1;
+ }
+
+ @Override
+ public boolean allowSelectCircuit() {
+ return false;
+ }
+
+ @Override
+ public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) {
+ getBaseMetaTileEntity().add1by1Slot(builder);
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensIndicator.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensIndicator.java
new file mode 100644
index 0000000000..c017025d42
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_LensIndicator.java
@@ -0,0 +1,117 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import gregtech.api.enums.Textures;
+import gregtech.api.interfaces.IIconContainer;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.render.TextureFactory;
+
+public class GT_MetaTileEntity_LensIndicator extends GT_MetaTileEntity_Hatch {
+
+ private boolean isOn = false;
+
+ private static final IIconContainer textureFont = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR;
+ private static final IIconContainer textureFont_Glow = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR_GLOW;
+
+ public GT_MetaTileEntity_LensIndicator(int aID, String aName, String aNameRegional, int aTier) {
+ super(aID, aName, aNameRegional, aTier, 0, "Indicates required lens swaps.");
+ }
+
+ public GT_MetaTileEntity_LensIndicator(String aName, int aTier, String[] aDescription, ITexture[][][] aTextures) {
+ super(aName, aTier, 0, aDescription, aTextures);
+ }
+
+ @Override
+ public boolean isValidSlot(int aIndex) {
+ return false;
+ }
+
+ @Override
+ public boolean isSimpleMachine() {
+ return true;
+ }
+
+ @Override
+ public boolean isFacingValid(ForgeDirection facing) {
+ return true;
+ }
+
+ @Override
+ public boolean isAccessAllowed(EntityPlayer aPlayer) {
+ return true;
+ }
+
+ @Override
+ public boolean allowGeneralRedstoneOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean allowPullStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection Side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public boolean allowPutStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public void initDefaultModes(NBTTagCompound aNBT) {
+ getBaseMetaTileEntity().setActive(true);
+ }
+
+ @Override
+ public String[] getDescription() {
+ return new String[] { "Can be installed in the UV Treatment Purification Unit.",
+ "Outputs Redstone Signal when a lens swap is requested." };
+ }
+
+ /**
+ * Updates redstone output strength based on the pH of the multiblock.
+ */
+ public void updateRedstoneOutput(boolean enabled) {
+ isOn = enabled;
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ if (isOn) {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setInternalOutputRedstoneSignal(side, (byte) 15);
+ }
+ } else {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setInternalOutputRedstoneSignal(side, (byte) 0);
+ }
+ }
+ super.onPostTick(aBaseMetaTileEntity, aTick);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_LensIndicator(mName, mTier, mDescriptionArray, mTextures);
+ }
+
+ @Override
+ public ITexture[] getTexturesActive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont), TextureFactory.builder()
+ .addIcon(textureFont_Glow)
+ .glow()
+ .build() };
+ }
+
+ @Override
+ public ITexture[] getTexturesInactive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont) };
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationPlant.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationPlant.java
new file mode 100644
index 0000000000..42bfd06d7a
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationPlant.java
@@ -0,0 +1,740 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlockAnyMeta;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.GT_HatchElement.Energy;
+import static gregtech.api.enums.GT_HatchElement.ExoticEnergy;
+import static gregtech.api.enums.GT_HatchElement.Maintenance;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_GLOW;
+import static gregtech.api.util.GT_RecipeBuilder.SECONDS;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+import static gregtech.common.tileentities.machines.multi.purification.GT_MetaTileEntity_PurificationUnitBase.WATER_BOOST_BONUS_CHANCE;
+import static gregtech.common.tileentities.machines.multi.purification.GT_MetaTileEntity_PurificationUnitBase.WATER_BOOST_NEEDED_FLUID;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+import com.gtnewhorizons.modularui.api.drawable.shapes.Rectangle;
+import com.gtnewhorizons.modularui.api.forge.ItemStackHandler;
+import com.gtnewhorizons.modularui.api.math.Alignment;
+import com.gtnewhorizons.modularui.api.math.Color;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+import com.gtnewhorizons.modularui.api.widget.Widget;
+import com.gtnewhorizons.modularui.common.widget.ButtonWidget;
+import com.gtnewhorizons.modularui.common.widget.DynamicPositionedColumn;
+import com.gtnewhorizons.modularui.common.widget.DynamicPositionedRow;
+import com.gtnewhorizons.modularui.common.widget.FakeSyncWidget;
+import com.gtnewhorizons.modularui.common.widget.Scrollable;
+import com.gtnewhorizons.modularui.common.widget.SlotWidget;
+import com.gtnewhorizons.modularui.common.widget.TextWidget;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.Textures;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_ExtendedPowerMultiBlockBase;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Log;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.shutdown.ShutDownReasonRegistry;
+import gregtech.common.gui.modularui.widget.ShutDownReasonSyncer;
+import gregtech.common.gui.modularui.widget.TextButtonWidget;
+
+public class GT_MetaTileEntity_PurificationPlant
+ extends GT_MetaTileEntity_ExtendedPowerMultiBlockBase<GT_MetaTileEntity_PurificationPlant>
+ implements ISurvivalConstructable {
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final String STRUCTURE_PIECE_MAIN_SURVIVAL = "main_survival";
+
+ /**
+ * Maximum distance in each axis between the purification plant main controller and the controller blocks of the
+ * purification plant units.
+ */
+ public static final int MAX_UNIT_DISTANCE = 32;
+
+ /**
+ * Time in ticks for a full processing cycle to complete.
+ */
+ public static final int CYCLE_TIME_TICKS = 120 * SECONDS;
+
+ /**
+ * Stores all purification units linked to this controller.
+ * Normally all units in this list should be valid and unique, if not then there is a bug where they are not being
+ * unlinked properly on block destruction/relinking.
+ */
+ private final List<LinkedPurificationUnit> mLinkedUnits = new ArrayList<>();
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationPlant> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationPlant>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, PurificationPlantStructureString.STRUCTURE_STRING)
+ // Create an identical structure for survival autobuild, with water replaced with air
+ .addShape(
+ STRUCTURE_PIECE_MAIN_SURVIVAL,
+ Arrays.stream(PurificationPlantStructureString.STRUCTURE_STRING)
+ .map(
+ sa -> Arrays.stream(sa)
+ .map(s -> s.replaceAll("F", " "))
+ .toArray(String[]::new))
+ .toArray(String[][]::new))
+ // Superplasticizer-treated high strength concrete
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings9, 3))
+ // Sterile Water Plant Casing
+ .addElement('B', ofBlock(GregTech_API.sBlockCasings9, 4))
+ // Reinforced Sterile Water Plant Casing
+ .addElement('C', ofBlock(GregTech_API.sBlockCasings9, 5))
+ // Tinted Industrial Glass
+ .addElement('D', ofBlockAnyMeta(GregTech_API.sBlockTintedGlass, 0))
+ .addElement('F', ofBlock(Blocks.water, 0))
+ .addElement('G', ofFrame(Materials.Tungsten))
+ // Hatch space
+ .addElement(
+ 'H',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationPlant>buildHatchAdder()
+ .atLeastList(t.getAllowedHatches())
+ .dot(1)
+ .casingIndex(GT_Utility.getCasingTextureIndex(GregTech_API.sBlockCasings9, 4))
+ .build()),
+ ofBlock(GregTech_API.sBlockCasings9, 4)))
+ .build();
+
+ public GT_MetaTileEntity_PurificationPlant(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationPlant(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(STRUCTURE_PIECE_MAIN, stackSize, hintsOnly, 3, 6, 0);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ int built = survivialBuildPiece(STRUCTURE_PIECE_MAIN_SURVIVAL, stackSize, 3, 6, 0, elementBudget, env, true);
+ if (built == -1) {
+ GT_Utility.sendChatToPlayer(
+ env.getActor(),
+ EnumChatFormatting.GREEN + "Auto placing done ! Now go place the water yourself !");
+ return 0;
+ }
+ return built;
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationPlant> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ final GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Plant")
+ .addInfo("Main controller block for the Water Purification Plant.")
+ .addInfo(
+ "Freely place " + EnumChatFormatting.YELLOW
+ + "Purification Units "
+ + EnumChatFormatting.GRAY
+ + "within "
+ + EnumChatFormatting.RED
+ + MAX_UNIT_DISTANCE
+ + EnumChatFormatting.GRAY
+ + " blocks along each axis.")
+ .addInfo("Left click this controller with a data stick, then right click a purification unit to link.")
+ .addInfo("Supplies power to linked purification units. This multiblock accepts TecTech energy hatches.")
+ .addSeparator()
+ .addInfo(
+ "Works in fixed time processing cycles of " + EnumChatFormatting.RED
+ + CYCLE_TIME_TICKS / SECONDS
+ + EnumChatFormatting.GRAY
+ + " seconds.")
+ .addInfo("All linked units follow this cycle.")
+ .addSeparator()
+ .addInfo("Every recipe has a base chance of success. Success rate can be boosted")
+ .addInfo("by using a portion of the target output as a secondary input.")
+ .addInfo(
+ EnumChatFormatting.RED + GT_Utility.formatNumbers(WATER_BOOST_NEEDED_FLUID * 100)
+ + "%"
+ + EnumChatFormatting.GRAY
+ + " of output yield will be consumed in exchange for an")
+ .addInfo(
+ "additive " + EnumChatFormatting.RED
+ + GT_Utility.formatNumbers(WATER_BOOST_BONUS_CHANCE * 100)
+ + "%"
+ + EnumChatFormatting.GRAY
+ + " increase to success.")
+ .addInfo(
+ "On recipe failure, each purification unit has a " + EnumChatFormatting.RED
+ + "50%"
+ + EnumChatFormatting.GRAY
+ + " chance")
+ .addInfo("to return water of the same quality as the input or lower.")
+ .addSeparator()
+ .addInfo("Every purification unit has a configuration window to configure maximum parallel amount.")
+ .addInfo(
+ "This will only scale purified water I/O and power usage. Other catalysts and outputs are unchanged.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "Contaminants and ionized particles in water can cause significant imperfections in delicate")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "processes related to the cutting and engraving of silicon wafers and chips. It is crucial that")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "the water is systematically purified through a series of increasingly precise and complex")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "purification processes, and this multiblock is the heart of the operation.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(7, 9, 8, false)
+ .addCasingInfoExactlyColored(
+ "Superplasticizer-Treated High Strength Concrete",
+ EnumChatFormatting.GRAY,
+ 56,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 77,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoRangeColored(
+ "Reinforced Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 71,
+ 72,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Tungsten Frame Box",
+ EnumChatFormatting.GRAY,
+ 30,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Tinted Industrial Glass",
+ EnumChatFormatting.GRAY,
+ 6,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored("Reinforced Door", EnumChatFormatting.GRAY, 1, EnumChatFormatting.GOLD, false)
+ .addController("Front center")
+ .addEnergyHatch(EnumChatFormatting.GOLD + "1", 1)
+ .addMaintenanceHatch(EnumChatFormatting.GOLD + "1", 1)
+ .addStructureInfo("Requires water to be placed in the tank.")
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationPlant(this.mName);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection side, ForgeDirection facing,
+ int colorIndex, boolean active, boolean redstoneLevel) {
+ if (side == facing) {
+ if (active) return new ITexture[] {
+ Textures.BlockIcons
+ .getCasingTextureForId(GT_Utility.getCasingTextureIndex(GregTech_API.sBlockCasings9, 4)),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] {
+ Textures.BlockIcons
+ .getCasingTextureForId(GT_Utility.getCasingTextureIndex(GregTech_API.sBlockCasings9, 4)),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons
+ .getCasingTextureForId(GT_Utility.getCasingTextureIndex(GregTech_API.sBlockCasings9, 4)) };
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ private List<IHatchElement<? super GT_MetaTileEntity_PurificationPlant>> getAllowedHatches() {
+ return ImmutableList.of(Maintenance, Energy, ExoticEnergy);
+ }
+
+ @Override
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ // Check self
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, 3, 6, 0)) {
+ return false;
+ }
+
+ // Check hatches
+ if (!checkHatches()) {
+ return false;
+ }
+
+ // using nano forge method of detecting hatches.
+ if (!checkExoticAndNormalEnergyHatches()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean checkHatches() {
+ // Exactly one maintenance hatch is required
+ return mMaintenanceHatches.size() == 1;
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.onPostTick(aBaseMetaTileEntity, aTick);
+
+ if (aBaseMetaTileEntity.isServerSide()) {
+ // Trigger structure check of linked units, but never all in the same tick, and at most once per cycle.
+ for (int i = 0; i < mLinkedUnits.size(); ++i) {
+ if (aTick % CYCLE_TIME_TICKS == i) {
+ LinkedPurificationUnit unit = mLinkedUnits.get(i);
+ boolean structure = unit.metaTileEntity()
+ .checkStructure(true);
+ // If unit was active but deformed, set as inactive
+ if (unit.isActive() && !structure) {
+ unit.setActive(false);
+ // Also remember to recalculate power usage, since otherwise the deformed unit will
+ // keep drawing power
+ this.lEUt = -calculateEffectivePowerUsage();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ updateCycleProgress();
+ // Calculate efficiency based on maintenance issues
+ if (mMaxProgresstime > 0) {
+ mEfficiency = Math.max(
+ 0,
+ Math.min(
+ mEfficiency + mEfficiencyIncrease,
+ getMaxEfficiency(mInventory[1]) - ((getIdealStatus() - getRepairStatus()) * 1000)));
+ }
+ }
+
+ private void updateCycleProgress() {
+ // Since the plant does not run recipes directly, we just continuously loop the base cycle
+ if (mMachine) {
+ // cycle is running, so simply advance it
+ if (mMaxProgresstime > 0) {
+ // onRunningTick is responsible for draining power
+ if (onRunningTick(mInventory[1])) {
+ markDirty();
+ mProgresstime += 1;
+ // Update progress time for active units
+ for (LinkedPurificationUnit unit : this.mLinkedUnits) {
+ if (unit.isActive()) {
+ GT_MetaTileEntity_PurificationUnitBase<?> metaTileEntity = unit.metaTileEntity();
+ metaTileEntity.mProgresstime = mProgresstime;
+ }
+ }
+ // Cycle finished
+ if (mProgresstime >= mMaxProgresstime) {
+ this.endCycle();
+ }
+ } else {
+ // Power drain failed, shut down all other units due to power loss.
+ // Note that we do not need to shut down self, as this is done in
+ // onRunningTick already
+ for (LinkedPurificationUnit unit : mLinkedUnits) {
+ if (unit.isActive()) {
+ unit.metaTileEntity()
+ .stopMachine(ShutDownReasonRegistry.POWER_LOSS);
+ }
+ }
+ }
+ }
+
+ // No cycle running, start a new cycle if the machine is turned on
+ if (mMaxProgresstime == 0 && isAllowedToWork()) {
+ this.startCycle();
+ }
+ }
+ }
+
+ private void startCycle() {
+ mProgresstime = 0;
+ mMaxProgresstime = CYCLE_TIME_TICKS;
+ mEfficiency = (10000 - (getIdealStatus() - getRepairStatus()) * 1000);
+
+ // Find active units and notify them that the cycle started
+ for (LinkedPurificationUnit unit : this.mLinkedUnits) {
+ GT_MetaTileEntity_PurificationUnitBase<?> metaTileEntity = unit.metaTileEntity();
+ PurificationUnitStatus status = metaTileEntity.status();
+ // Unit needs to be online to be considered active.
+ if (status == PurificationUnitStatus.ONLINE) {
+ // Perform recipe check for unit and start it if successful
+ if (metaTileEntity.doPurificationRecipeCheck()) {
+ unit.setActive(true);
+ metaTileEntity.startCycle(mMaxProgresstime, mProgresstime);
+ }
+ }
+ }
+
+ // After activating all units, calculate power usage
+ lEUt = -calculateEffectivePowerUsage();
+ }
+
+ private void endCycle() {
+ mMaxProgresstime = 0;
+
+ // Mark all units as inactive and reset their progress time
+ for (LinkedPurificationUnit unit : this.mLinkedUnits) {
+ GT_MetaTileEntity_PurificationUnitBase<?> metaTileEntity = unit.metaTileEntity();
+ // If this unit was active, end the cycle
+ if (unit.isActive()) {
+ metaTileEntity.endCycle();
+ }
+ unit.setActive(false);
+ }
+ }
+
+ /**
+ * Calculate power usage of all units
+ */
+ private long calculateEffectivePowerUsage() {
+ long euT = 0;
+ for (LinkedPurificationUnit unit : mLinkedUnits) {
+ if (unit.isActive()) {
+ euT += unit.metaTileEntity()
+ .getActualPowerUsage();
+ }
+ }
+ return euT;
+ }
+
+ @Override
+ public int getMaxEfficiency(ItemStack aStack) {
+ return 10000;
+ }
+
+ @Override
+ public int getDamageToComponent(ItemStack aStack) {
+ return 0;
+ }
+
+ @Override
+ public boolean explodesOnComponentBreak(ItemStack aStack) {
+ return false;
+ }
+
+ public void registerLinkedUnit(GT_MetaTileEntity_PurificationUnitBase<?> unit) {
+ this.mLinkedUnits.add(new LinkedPurificationUnit(unit));
+ }
+
+ public void unregisterLinkedUnit(GT_MetaTileEntity_PurificationUnitBase<?> unit) {
+ this.mLinkedUnits.removeIf(link -> link.metaTileEntity() == unit);
+ }
+
+ @Override
+ public void onLeftclick(IGregTechTileEntity aBaseMetaTileEntity, EntityPlayer aPlayer) {
+ if (!(aPlayer instanceof EntityPlayerMP)) return;
+
+ // Save link data to data stick, very similar to Crafting Input Buffer.
+
+ ItemStack dataStick = aPlayer.inventory.getCurrentItem();
+ if (!ItemList.Tool_DataStick.isStackEqual(dataStick, false, true)) return;
+
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setString("type", "PurificationPlant");
+ tag.setInteger("x", aBaseMetaTileEntity.getXCoord());
+ tag.setInteger("y", aBaseMetaTileEntity.getYCoord());
+ tag.setInteger("z", aBaseMetaTileEntity.getZCoord());
+
+ dataStick.stackTagCompound = tag;
+ dataStick.setStackDisplayName(
+ "Purification Plant Link Data Stick (" + aBaseMetaTileEntity
+ .getXCoord() + ", " + aBaseMetaTileEntity.getYCoord() + ", " + aBaseMetaTileEntity.getZCoord() + ")");
+ aPlayer.addChatMessage(new ChatComponentText("Saved Link Data to Data Stick"));
+ }
+
+ @Override
+ public String[] getInfoData() {
+ var ret = new ArrayList<String>();
+ // Show linked purification units and their status
+ ret.add("Linked Purification Units: ");
+ for (LinkedPurificationUnit unit : this.mLinkedUnits) {
+ String text = EnumChatFormatting.AQUA + unit.metaTileEntity()
+ .getLocalName() + ": ";
+ PurificationUnitStatus status = unit.metaTileEntity()
+ .status();
+ switch (status) {
+ case ONLINE -> {
+ text = text + EnumChatFormatting.GREEN + "Online";
+ }
+ case DISABLED -> {
+ text = text + EnumChatFormatting.YELLOW + "Disabled";
+ }
+ case INCOMPLETE_STRUCTURE -> {
+ text = text + EnumChatFormatting.RED + "Incomplete Structure";
+ }
+ }
+ ret.add(text);
+ }
+ return ret.toArray(new String[0]);
+ }
+
+ @Override
+ public void onBlockDestroyed() {
+ // When the controller is destroyed we want to notify all currently linked units
+ for (LinkedPurificationUnit unit : this.mLinkedUnits) {
+ unit.metaTileEntity()
+ .unlinkController();
+ }
+ super.onBlockDestroyed();
+ }
+
+ private void drawTopText(DynamicPositionedColumn screenElements) {
+ screenElements.setSynced(false)
+ .setSpace(0)
+ .setPos(10, 8);
+
+ screenElements
+ .widget(
+ new TextWidget(GT_Utility.trans("138", "Incomplete Structure.")).setDefaultColor(EnumChatFormatting.RED)
+ .setEnabled(widget -> !mMachine))
+ .widget(new FakeSyncWidget.BooleanSyncer(() -> mMachine, val -> mMachine = val));
+
+ screenElements.widget(
+ new TextWidget("Hit with Soft Mallet to start.").setDefaultColor(EnumChatFormatting.BLACK)
+ .setEnabled(
+ widget -> getBaseMetaTileEntity().getErrorDisplayID() == 0 && !getBaseMetaTileEntity().isActive()))
+ .widget(
+ new FakeSyncWidget.IntegerSyncer(
+ () -> getBaseMetaTileEntity().getErrorDisplayID(),
+ val -> getBaseMetaTileEntity().setErrorDisplayID(val)))
+ .widget(
+ new FakeSyncWidget.BooleanSyncer(
+ () -> getBaseMetaTileEntity().isActive(),
+ val -> getBaseMetaTileEntity().setActive(val)));
+ screenElements.widget(
+ new TextWidget(GT_Utility.trans("142", "Running perfectly.")).setDefaultColor(EnumChatFormatting.GREEN)
+ .setEnabled(
+ widget -> getBaseMetaTileEntity().getErrorDisplayID() == 0 && getBaseMetaTileEntity().isActive()));
+ screenElements.widget(
+ TextWidget.dynamicString(
+ () -> getBaseMetaTileEntity().getLastShutDownReason()
+ .getDisplayString())
+ .setSynced(false)
+ .setTextAlignment(Alignment.CenterLeft)
+ .setEnabled(
+ widget -> shouldDisplayShutDownReason() && !getBaseMetaTileEntity().isActive()
+ && GT_Utility.isStringValid(
+ getBaseMetaTileEntity().getLastShutDownReason()
+ .getDisplayString())
+ && getBaseMetaTileEntity().wasShutdown()))
+ .widget(
+ new ShutDownReasonSyncer(
+ () -> getBaseMetaTileEntity().getLastShutDownReason(),
+ reason -> getBaseMetaTileEntity().setShutDownReason(reason)))
+ .widget(
+ new FakeSyncWidget.BooleanSyncer(
+ () -> getBaseMetaTileEntity().wasShutdown(),
+ wasShutDown -> getBaseMetaTileEntity().setShutdownStatus(wasShutDown)));
+ screenElements.widget(
+ TextWidget.dynamicString(this::generateCurrentRecipeInfoString)
+ .setSynced(false)
+ .setTextAlignment(Alignment.CenterLeft)
+ .setEnabled(widget -> (mMaxProgresstime > 0)))
+ .widget(new FakeSyncWidget.IntegerSyncer(() -> mProgresstime, val -> mProgresstime = val))
+ .widget(new FakeSyncWidget.IntegerSyncer(() -> mMaxProgresstime, val -> mMaxProgresstime = val));
+ }
+
+ private final int STATUS_WINDOW_ID = 10;
+
+ private ModularWindow createStatusWindow(final EntityPlayer player) {
+ final int windowWidth = 260;
+ final int windowHeight = 200;
+ ModularWindow.Builder builder = ModularWindow.builder(windowWidth, windowHeight);
+ builder.setBackground(GT_UITextures.BACKGROUND_SINGLEBLOCK_DEFAULT);
+ builder.widget(
+ ButtonWidget.closeWindowButton(true)
+ .setPos(windowWidth - 15, 3));
+
+ // Title widget
+ builder.widget(
+ new TextWidget(EnumChatFormatting.BOLD + "Purification Unit Status").setTextAlignment(Alignment.Center)
+ .setPos(5, 10)
+ .setSize(windowWidth, 8));
+
+ int currentYPosition = 20;
+ Scrollable mainDisp = new Scrollable().setVerticalScroll();
+
+ int rowHeight = 20;
+ for (int i = 0; i < this.mLinkedUnits.size(); i++) {
+ mainDisp.widget(makeUnitStatusWidget(mLinkedUnits.get(i)).setPos(0, rowHeight * (i + 1)));
+ }
+
+ builder.widget(
+ mainDisp.setPos(5, currentYPosition)
+ .setSize(windowWidth - 10, windowHeight - currentYPosition - 5));
+ return builder.build();
+ }
+
+ private Widget makeStatusWindowButton() {
+ TextButtonWidget widget = (TextButtonWidget) new TextButtonWidget("Status").setLeftMargin(4)
+ .setSize(40, 16)
+ .setPos(10, 40);
+ widget.button()
+ .setOnClick(
+ (clickData, w) -> {
+ if (!w.isClient()) w.getContext()
+ .openSyncedWindow(STATUS_WINDOW_ID);
+ })
+ .setBackground(GT_UITextures.BUTTON_STANDARD);
+ widget.text()
+ .setTextAlignment(Alignment.CenterLeft)
+ .setDefaultColor(EnumChatFormatting.BLACK);
+ return widget;
+ }
+
+ private Widget makeUnitStatusWidget(LinkedPurificationUnit unit) {
+ // Draw small machine controller icon
+ DynamicPositionedRow row = new DynamicPositionedRow();
+ ItemStackHandler machineIcon = new ItemStackHandler(1);
+ machineIcon.setStackInSlot(
+ 0,
+ unit.metaTileEntity()
+ .getStackForm(1));
+
+ row.widget(
+ SlotWidget.phantom(machineIcon, 0)
+ .disableInteraction()
+ .setPos(0, 0))
+ .setSize(20, 20);
+
+ // Display machine name and status
+ String name = unit.metaTileEntity()
+ .getLocalName();
+
+ row.widget(
+ TextWidget.dynamicString(() -> name + " " + unit.getStatusString())
+ .setSynced(false)
+ .setTextAlignment(Alignment.CenterLeft)
+ .setPos(25, 0)
+ .setSize(0, 20))
+ .widget(new FakeSyncWidget.StringSyncer(() -> name, _name -> {}))
+ .widget(
+ unit.metaTileEntity()
+ .makeSyncerWidgets())
+ .widget(new FakeSyncWidget.BooleanSyncer(unit::isActive, unit::setActive));;
+
+ return row;
+ }
+
+ @Override
+ public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) {
+
+ buildContext.addSyncedWindow(STATUS_WINDOW_ID, this::createStatusWindow);
+
+ // Draw basic recipe info
+ final DynamicPositionedColumn controlTextArea = new DynamicPositionedColumn();
+ drawTopText(controlTextArea);
+ builder.widget(controlTextArea);
+
+ // Draw line separator
+ builder.widget(
+ new Rectangle().setColor(Color.rgb(114, 120, 139))
+ .asWidget()
+ .setSizeProvider((screenSize, window, parent) -> new Size(window.getSize().width - 8, 2))
+ .setPos(3, 32));
+
+ // Add status window button
+ builder.widget(makeStatusWindowButton());
+
+ // Add parallel count number input
+
+ builder.widget(createPowerSwitchButton(builder));
+
+ // Add value syncers, note that we do this here so
+ // everything is updated once the status gui opens
+ addSyncers(builder);
+ }
+
+ private void addSyncers(ModularWindow.Builder builder) {
+ // Sync connection list to client
+ builder.widget(new FakeSyncWidget.ListSyncer<>(() -> mLinkedUnits, links -> {
+ mLinkedUnits.clear();
+ mLinkedUnits.addAll(links);
+ }, (buffer, link) -> {
+ // Try to save link data to NBT, so we can reconstruct it on client
+ try {
+ buffer.writeNBTTagCompoundToBuffer(link.writeLinkDataToNBT());
+ } catch (IOException e) {
+ GT_Log.err.println(e.getCause());
+ }
+ }, buffer -> {
+ // Try to load link data from NBT compound as constructed above.
+ try {
+ return new LinkedPurificationUnit(buffer.readNBTTagCompoundFromBuffer());
+ } catch (IOException e) {
+ GT_Log.err.println(e.getCause());
+ }
+ return null;
+ }));
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitBase.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitBase.java
new file mode 100644
index 0000000000..0abf9d525f
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitBase.java
@@ -0,0 +1,793 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static gregtech.api.metatileentity.BaseTileEntity.TOOLTIP_DELAY;
+import static net.minecraft.util.StatCollector.translateToLocal;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Stream;
+
+import net.minecraft.block.Block;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.world.World;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.gtnewhorizons.modularui.api.drawable.IDrawable;
+import com.gtnewhorizons.modularui.api.drawable.UITexture;
+import com.gtnewhorizons.modularui.api.math.Alignment;
+import com.gtnewhorizons.modularui.api.math.Color;
+import com.gtnewhorizons.modularui.api.math.Size;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+import com.gtnewhorizons.modularui.api.widget.Widget;
+import com.gtnewhorizons.modularui.common.widget.ButtonWidget;
+import com.gtnewhorizons.modularui.common.widget.FakeSyncWidget;
+import com.gtnewhorizons.modularui.common.widget.MultiChildWidget;
+import com.gtnewhorizons.modularui.common.widget.TextWidget;
+import com.gtnewhorizons.modularui.common.widget.textfield.NumericWidget;
+
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.VoidingMode;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_ExtendedPowerMultiBlockBase;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.check.CheckRecipeResult;
+import gregtech.api.recipe.check.CheckRecipeResultRegistry;
+import gregtech.api.recipe.metadata.PurificationPlantBaseChanceKey;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Recipe;
+import gregtech.api.util.GT_Utility;
+import gregtech.common.blocks.GT_Block_Casings_Abstract;
+import mcp.mobius.waila.api.IWailaConfigHandler;
+import mcp.mobius.waila.api.IWailaDataAccessor;
+
+/**
+ * Base class for purification units. This class handles all shared behaviour between units.
+ * When inheriting from this, make sure to call super.loadNBTData() and super.saveNBTData()
+ * if you override these methods, or linking will break.
+ */
+public abstract class GT_MetaTileEntity_PurificationUnitBase<T extends GT_MetaTileEntity_ExtendedPowerMultiBlockBase<T>>
+ extends GT_MetaTileEntity_ExtendedPowerMultiBlockBase<T> {
+
+ /**
+ * Ratio of output fluid that needs to be inserted back as input to trigger a "water boost".
+ * Must be in [0, 1].
+ */
+ public static final float WATER_BOOST_NEEDED_FLUID = 0.1f;
+ /**
+ * Additive bonus to success chance when water boost is active.
+ * Must be in [0, 1]
+ */
+ public static final float WATER_BOOST_BONUS_CHANCE = 0.15f;
+
+ /**
+ * Small internal enum to report back the various error cases when linking purification units to the
+ * purification plant.
+ */
+ private enum LinkResult {
+ /**
+ * Link target was out of range of the main controller
+ */
+ TOO_FAR,
+ /**
+ * No valid GT_MetaTileEntity_PurificationPlant was found at the link target position.
+ */
+ NO_VALID_PLANT,
+ /**
+ * Link successful
+ */
+ SUCCESS,
+ }
+
+ /**
+ * Coordinates of the main purification plant controller. These can be used to find the controller again
+ * on world load.
+ */
+ private int controllerX, controllerY, controllerZ;
+
+ /**
+ * Whether a controller was previously set.
+ */
+ private boolean controllerSet = false;
+
+ /**
+ * Pointer to the main purification plant controller.
+ */
+ private GT_MetaTileEntity_PurificationPlant controller = null;
+
+ /**
+ * The current recipe being run in the purification unit. Note that purification unit recipes are a bit special,
+ * so input and output in the recipe might not exactly match the required inputs and produced outputs.
+ * For more information, always look at the purification unit tooltip and implementation.
+ */
+ protected GT_Recipe currentRecipe = null;
+
+ /**
+ * Current chance of the recipe succeeding, always in [0, 100]. A chance above 100 will be interpreted as 100.
+ */
+ protected float currentRecipeChance = 0.0f;
+
+ /**
+ * Configured parallel amount. Only water I/O and power scale.
+ */
+ protected int maxParallel = 1;
+
+ protected int effectiveParallel = 1;
+
+ protected ArrayList<FluidStack> storedFluids = null;
+
+ protected GT_MetaTileEntity_PurificationUnitBase(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ protected GT_MetaTileEntity_PurificationUnitBase(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public int getMaxEfficiency(ItemStack aStack) {
+ return 10000;
+ }
+
+ @Override
+ public int getDamageToComponent(ItemStack aStack) {
+ return 0;
+ }
+
+ @Override
+ public boolean explodesOnComponentBreak(ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public boolean doRandomMaintenanceDamage() {
+ // The individual purification unit structures cannot have maintenance issues, so do nothing.
+ return true;
+ }
+
+ /**
+ * Used to more easily grab a correct texture index from a block + meta.
+ *
+ * @param block Block to use as base. Must implement GT_Block_Casings_Abstract
+ * @param meta Metadata of the block to pick the actual block
+ * @return The correct index into the global texture atlas.
+ */
+ protected static int getTextureIndex(Block block, int meta) {
+ return ((GT_Block_Casings_Abstract) block).getTextureIndex(meta);
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTimer) {
+ super.onPostTick(aBaseMetaTileEntity, aTimer);
+ // Try to re-link to controller periodically, for example on game load.
+ if (aTimer % 100 == 5 && controllerSet && getController() == null) {
+ trySetControllerFromCoord(controllerX, controllerY, controllerZ);
+ }
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ // Main controller updates progress time. We can do I/O logic here.
+ // The logic for operating purification units is typically implemented by overriding this behaviour.
+ if (mMaxProgresstime > 0) {
+ this.markDirty();
+ // Do not take maintenance into consideration, because purification units do not get
+ // maintenance issues.
+ // Technically, this entire efficiency stat is a bit useless for purification units, since
+ // their power draw does not actually depend on it, but it's nice to keep around for consistency with other
+ // multiblocks. This way, you still gradually see the efficiency go down when it powers down.
+ mEfficiency = Math.max(0, Math.min(mEfficiency + mEfficiencyIncrease, getMaxEfficiency(mInventory[1])));
+ }
+ }
+
+ protected CheckRecipeResult findRecipeForInputs(FluidStack[] fluidInputs, ItemStack... itemInputs) {
+ RecipeMap<?> recipeMap = this.getRecipeMap();
+
+ // Grab a stream of recipes and find the one with the highest success chance
+ Stream<GT_Recipe> recipes = recipeMap.findRecipeQuery()
+ .fluids(fluidInputs)
+ .items(itemInputs)
+ .findAll();
+ GT_Recipe recipe = recipes
+ .max(Comparator.comparing(r -> r.getMetadataOrDefault(PurificationPlantBaseChanceKey.INSTANCE, 0.0f)))
+ .orElse(null);
+
+ if (recipe == null) {
+ return CheckRecipeResultRegistry.NO_RECIPE;
+ }
+
+ if (this.protectsExcessFluid() && !this.canOutputAll(recipe.mFluidOutputs)) {
+ return CheckRecipeResultRegistry.FLUID_OUTPUT_FULL;
+ }
+
+ if (this.protectsExcessItem() && !this.canOutputAll(recipe.mOutputs)) {
+ return CheckRecipeResultRegistry.ITEM_OUTPUT_FULL;
+ }
+
+ this.currentRecipe = recipe;
+ return CheckRecipeResultRegistry.SUCCESSFUL;
+ }
+
+ /**
+ * By default, only checks fluid input.
+ *
+ * @return
+ */
+ @NotNull
+ @Override
+ public CheckRecipeResult checkProcessing() {
+ this.storedFluids = this.getStoredFluids();
+ CheckRecipeResult result = overrideRecipeCheck();
+ if (result == null) result = findRecipeForInputs(storedFluids.toArray(new FluidStack[] {}));
+
+ // If we had a successful result, calculate effective parallel
+ if (result.wasSuccessful()) {
+ FluidStack waterInput = this.currentRecipe.mFluidInputs[0];
+ // Count total available purified water input of the previous step
+ long amountAvailable = 0;
+ for (FluidStack fluid : this.storedFluids) {
+ if (fluid.isFluidEqual(waterInput)) {
+ amountAvailable += fluid.amount;
+ }
+ }
+ // Determine effective parallel
+ effectiveParallel = (int) Math.min(maxParallel, Math.floorDiv(amountAvailable, waterInput.amount));
+ // This should not happen, throw an error
+ if (effectiveParallel == 0) return CheckRecipeResultRegistry.INTERNAL_ERROR;
+ }
+
+ return result;
+ }
+
+ public CheckRecipeResult overrideRecipeCheck() {
+ return null;
+ }
+
+ /**
+ * Equivalent to checkRecipe(), but public because the purification plant needs to access it and checkRecipe()
+ * is protected.
+ *
+ * @return True if successfully found a recipe and/or started processing/
+ */
+ public boolean doPurificationRecipeCheck() {
+ effectiveParallel = 1;
+ return this.checkRecipe();
+ }
+
+ /**
+ * Get the success chance of the recipe, from 0 to 100. Never call this while a recipe is running, because items
+ * or modifiers used to boost might disappear by the time recipe check comes around,
+ * which would invalidate this result.
+ */
+ public float calculateBoostedSuccessChance() {
+ // If this.currentRecipe is null, there is a bug, so throwing a NPE is fine.
+ float recipeChance = this.currentRecipe.getMetadataOrDefault(PurificationPlantBaseChanceKey.INSTANCE, 0.0f);
+ // Apply water boost if available.
+ if (isWaterBoosted(this.currentRecipe)) {
+ recipeChance = Math.min(recipeChance + WATER_BOOST_BONUS_CHANCE * 100.0f, 100.0f);
+ }
+ return recipeChance;
+ }
+
+ /**
+ * By default, the final recipe success chance is simply the success chance calculated on recipe check.
+ * This applies water boosts when needed to the base chance. Purification units can override this to perform
+ * more complex success chance calculations, that even take into account what happened during the runtime of the
+ * recipe.
+ *
+ * @return The success chance of the recipe, at the point in time the outputs are to be produced.
+ */
+ public float calculateFinalSuccessChance() {
+ return this.currentRecipeChance;
+ }
+
+ /**
+ * Get the tier of water this unit makes. Starts at 1.
+ */
+ public abstract int getWaterTier();
+
+ /**
+ * Get the amount of water needed to execute a water boost, in mb.
+ */
+ public FluidStack getWaterBoostAmount(GT_Recipe recipe) {
+ // Recipes should always be constructed so that output water is always the first fluid output
+ FluidStack outputWater = recipe.mFluidOutputs[0];
+ int amount = Math.round(outputWater.amount * WATER_BOOST_NEEDED_FLUID);
+ return new FluidStack(outputWater.getFluid(), amount);
+ }
+
+ /**
+ * Returns true if this purification unit contains enough water to apply a water boost for the selected recipe.
+ * This should only be called during recipe check! Never call this while a recipe is running, because water used to
+ * boost might disappear by the time recipe check comes around, which would invalidate this result.
+ *
+ * @param recipe The recipe to check the water boost of
+ */
+ public boolean isWaterBoosted(GT_Recipe recipe) {
+ FluidStack inputWater = getWaterBoostAmount(recipe);
+ // Simulate input drain to see if we can water boost
+ return depleteInput(inputWater, true);
+ }
+
+ /**
+ * Consumes all <b>fluid</b> inputs of the current recipe.
+ */
+ public void depleteRecipeInputs() {
+ for (FluidStack input : this.currentRecipe.mFluidInputs) {
+ FluidStack copyWithParallel = input.copy();
+ copyWithParallel.amount = input.amount * effectiveParallel;
+ this.depleteInput(copyWithParallel);
+ }
+ }
+
+ /**
+ * Called after a recipe is found and accepted.
+ *
+ * @param cycleTime Time for a full cycle to complete
+ * @param progressTime Current progress time
+ */
+ public void startCycle(int cycleTime, int progressTime) {
+ // Important to calculate this before depleting inputs, otherwise we may get issues with boost items
+ // disappearing.
+ this.currentRecipeChance = this.calculateBoostedSuccessChance();
+
+ // Deplete inputs from water boost if enabled.
+ if (isWaterBoosted(this.currentRecipe)) {
+ FluidStack inputWater = this.getWaterBoostAmount(this.currentRecipe);
+ this.depleteInput(inputWater);
+ }
+
+ // Consume inputs
+ this.depleteRecipeInputs();
+ // Initialize recipe and progress information.
+ this.mMaxProgresstime = cycleTime;
+ this.mProgresstime = progressTime;
+ this.mEfficiency = 10000;
+ // These need to be set so the GUI code can display the produced outputs
+
+ // Make sure to scale purified water output with parallel amount.
+ // Make sure to make a full copy of the array, so we don't go modifying recipes
+ FluidStack[] fluidOutputs = new FluidStack[this.currentRecipe.mFluidOutputs.length];
+ for (int i = 0; i < this.currentRecipe.mFluidOutputs.length; ++i) {
+ fluidOutputs[i] = this.currentRecipe.mFluidOutputs[i].copy();
+ }
+ fluidOutputs[0].amount *= effectiveParallel;
+ this.mOutputFluids = fluidOutputs;
+ this.mOutputItems = this.currentRecipe.mOutputs;
+ // Set this value, so it can be displayed in Waila. Note that the logic for the units is
+ // specifically overridden so setting this value does not actually drain power.
+ // Instead, power is drained by the main purification plant controller.
+ this.lEUt = -this.getActualPowerUsage();
+ }
+
+ public void addRecipeOutputs() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ this.addFluidOutputs(mOutputFluids);
+ // If this recipe has random item outputs, roll on it and add outputs
+ if (this.currentRecipe.mChances != null) {
+ // Roll on each output individually
+ for (int i = 0; i < this.currentRecipe.mOutputs.length; ++i) {
+ // Recipes store probabilities as a value ranging from 1-10000
+ int roll = random.nextInt(10000);
+ if (roll <= this.currentRecipe.mChances[i]) {
+ this.addOutput(this.currentRecipe.mOutputs[i]);
+ }
+ }
+ } else {
+ // Guaranteed item output
+ for (int i = 0; i < this.currentRecipe.mOutputs.length; ++i) {
+ this.addOutput(this.currentRecipe.mOutputs[i]);
+ }
+ }
+ }
+
+ public void endCycle() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+
+ // First see if the recipe succeeded. For some reason random.nextFloat does not compile, so we use this
+ // hack instead.
+ float successRoll = random.nextInt(0, 10000) / 100.0f;
+ if (successRoll <= calculateFinalSuccessChance()) {
+ addRecipeOutputs();
+ } else {
+ onRecipeFail();
+ }
+
+ // Reset recipe values for next iteration
+ this.mMaxProgresstime = 0;
+ this.mProgresstime = 0;
+ this.lEUt = 0;
+ this.mEfficiency = 0;
+ this.currentRecipe = null;
+ this.currentRecipeChance = 0.0f;
+ this.mOutputItems = null;
+ this.mOutputFluids = null;
+ this.effectiveParallel = 1;
+ }
+
+ /**
+ * Outputs fluid when recipe fails.
+ */
+ private void onRecipeFail() {
+ // Possibly output lower quality water.
+ // Note that if there is no space for this, it will be voided regardless of fluid void setting!
+ FluidStack outputWater = getDegradedOutputWater();
+ this.addOutput(outputWater);
+ }
+
+ /**
+ * On recipe fail, water quality may degrade to the same or lower tier. This function returns the water to output
+ * in this case, or null if no water is produced at all.
+ */
+ private FluidStack getDegradedOutputWater() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ int roll = random.nextInt(0, 2);
+ // 50% chance to not output anything at all
+ if (roll == 0) return null;
+
+ for (int waterTier = getWaterTier(); waterTier > 0; --waterTier) {
+ // 50% chance every time of degrading into the previous tier
+ roll = random.nextInt(0, 2);
+ if (roll == 1) {
+ // Rolled good, stop the loop and output water below current tier
+ int amount = mOutputFluids[0].amount;
+ // For tier 1, this is distilled water, so we cannot use the helper function!
+ if (waterTier == 1) {
+ return GT_ModHandler.getDistilledWater(amount);
+ }
+ Materials water = PurifiedWaterHelpers.getPurifiedWaterTier(waterTier - 1);
+ return water.getFluid(amount);
+ }
+ // Bad roll, keep looping and degrade quality even further
+ }
+ // Rolled bad on every iteration, no output for you
+ return null;
+ }
+
+ /**
+ * Get the EU/t usage of this unit while it is running.
+ */
+ public abstract long getBasePowerUsage();
+
+ public long getActualPowerUsage() {
+ return getBasePowerUsage() * effectiveParallel;
+ }
+
+ @Override
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ // The individual purification unit structures cannot have maintenance issues, so fix them all.
+ this.mCrowbar = true;
+ this.mWrench = true;
+ this.mHardHammer = true;
+ this.mSoftHammer = true;
+ this.mSolderingTool = true;
+ this.mScrewdriver = true;
+ return true;
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ // If a linked controller was found, load its coordinates.
+ // The unit will try to link to the real controller block periodically in onPostTick()
+ // We cannot do this linking here yet because the controller block might not be loaded yet.
+ // TODO: We could try though?
+ if (aNBT.hasKey("controller")) {
+ NBTTagCompound controllerNBT = aNBT.getCompoundTag("controller");
+ controllerX = controllerNBT.getInteger("x");
+ controllerY = controllerNBT.getInteger("y");
+ controllerZ = controllerNBT.getInteger("z");
+ controllerSet = true;
+ }
+ currentRecipeChance = aNBT.getFloat("currentRecipeChance");
+ if (aNBT.hasKey("configuredParallel")) {
+ maxParallel = aNBT.getInteger("configuredParallel");
+ }
+ if (aNBT.hasKey("effectiveParallel")) {
+ effectiveParallel = aNBT.getInteger("effectiveParallel");
+ }
+ }
+
+ public NBTTagCompound saveLinkDataToNBT() {
+ NBTTagCompound controllerNBT = new NBTTagCompound();
+ controllerNBT.setInteger("x", controllerX);
+ controllerNBT.setInteger("y", controllerY);
+ controllerNBT.setInteger("z", controllerZ);
+ return controllerNBT;
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ if (controllerSet) {
+ NBTTagCompound controllerNBT = saveLinkDataToNBT();
+ aNBT.setTag("controller", controllerNBT);
+ }
+ aNBT.setFloat("currentRecipeChance", currentRecipeChance);
+ aNBT.setInteger("configuredParallel", maxParallel);
+ aNBT.setInteger("effectiveParallel", effectiveParallel);
+ }
+
+ private LinkResult trySetControllerFromCoord(int x, int y, int z) {
+ IGregTechTileEntity ourBaseMetaTileEntity = this.getBaseMetaTileEntity();
+ // First check whether the controller we try to link to is within range. The range is defined
+ // as a max distance in each axis.
+ if (Math.abs(ourBaseMetaTileEntity.getXCoord() - x) > GT_MetaTileEntity_PurificationPlant.MAX_UNIT_DISTANCE)
+ return LinkResult.TOO_FAR;
+ if (Math.abs(ourBaseMetaTileEntity.getYCoord() - y) > GT_MetaTileEntity_PurificationPlant.MAX_UNIT_DISTANCE)
+ return LinkResult.TOO_FAR;
+ if (Math.abs(ourBaseMetaTileEntity.getZCoord() - z) > GT_MetaTileEntity_PurificationPlant.MAX_UNIT_DISTANCE)
+ return LinkResult.TOO_FAR;
+
+ // Find the block at the requested coordinated and check if it is a purification plant controller.
+ var tileEntity = getBaseMetaTileEntity().getWorld()
+ .getTileEntity(x, y, z);
+ if (tileEntity == null) return LinkResult.NO_VALID_PLANT;
+ if (!(tileEntity instanceof IGregTechTileEntity gtTileEntity)) return LinkResult.NO_VALID_PLANT;
+ var metaTileEntity = gtTileEntity.getMetaTileEntity();
+ if (!(metaTileEntity instanceof GT_MetaTileEntity_PurificationPlant)) return LinkResult.NO_VALID_PLANT;
+
+ // Before linking, unlink from current controller, so we don't end up with units linked to multiple
+ // controllers.
+ GT_MetaTileEntity_PurificationPlant oldController = getController();
+ if (oldController != null) {
+ oldController.unregisterLinkedUnit(this);
+ this.unlinkController();
+ }
+
+ // Now link to new controller
+ controllerX = x;
+ controllerY = y;
+ controllerZ = z;
+ controllerSet = true;
+ controller = (GT_MetaTileEntity_PurificationPlant) metaTileEntity;
+ controller.registerLinkedUnit(this);
+ return LinkResult.SUCCESS;
+ }
+
+ private boolean tryLinkDataStick(EntityPlayer aPlayer) {
+ // Make sure the held item is a data stick
+ ItemStack dataStick = aPlayer.inventory.getCurrentItem();
+ if (!ItemList.Tool_DataStick.isStackEqual(dataStick, false, true)) {
+ return false;
+ }
+
+ // Make sure this data stick is a proper purification plant link data stick.
+ if (!dataStick.hasTagCompound() || !dataStick.stackTagCompound.getString("type")
+ .equals("PurificationPlant")) {
+ return false;
+ }
+
+ // Now read link coordinates from the data stick.
+ NBTTagCompound nbt = dataStick.stackTagCompound;
+ int x = nbt.getInteger("x");
+ int y = nbt.getInteger("y");
+ int z = nbt.getInteger("z");
+
+ // Try to link, and report the result back to the player.
+ LinkResult result = trySetControllerFromCoord(x, y, z);
+ if (result == LinkResult.SUCCESS) {
+ aPlayer.addChatMessage(new ChatComponentText("Link successful"));
+ } else if (result == LinkResult.TOO_FAR) {
+ aPlayer.addChatMessage(new ChatComponentText("Link failed: Out of range."));
+ } else if (result == LinkResult.NO_VALID_PLANT) {
+ aPlayer.addChatMessage(new ChatComponentText("Link failed: No Purification Plant found at link location"));
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onRightclick(IGregTechTileEntity aBaseMetaTileEntity, EntityPlayer aPlayer) {
+ if (!(aPlayer instanceof EntityPlayerMP)) {
+ return false;
+ }
+
+ // Right-clicking could be a data stick linking action, so try this first.
+ if (tryLinkDataStick(aPlayer)) {
+ return true;
+ }
+
+ return super.onRightclick(aBaseMetaTileEntity, aPlayer);
+ }
+
+ public GT_MetaTileEntity_PurificationPlant getController() {
+ if (controller == null) return null;
+ // Controller disappeared
+ if (controller.getBaseMetaTileEntity() == null) return null;
+ return controller;
+ }
+
+ // If the controller is broken this can be called to explicitly unlink the controller, so we don't have any
+ // references lingering around
+ public void unlinkController() {
+ this.controllerSet = false;
+ this.controller = null;
+ this.controllerX = 0;
+ this.controllerY = 0;
+ this.controllerZ = 0;
+ }
+
+ @Override
+ public void onBlockDestroyed() {
+ // When this block is destroyed, explicitly unlink it from the controller if there is any.
+ GT_MetaTileEntity_PurificationPlant controller = getController();
+ if (controller != null) {
+ controller.unregisterLinkedUnit(this);
+ }
+ super.onBlockDestroyed();
+ }
+
+ @Override
+ public String[] getInfoData() {
+ var ret = new ArrayList<String>();
+ // If this purification unit is linked to a controller, add this info to the scanner output.
+ if (getController() != null) {
+ ret.add(
+ "This Purification Unit is linked to the Water Purification Plant at " + controllerX
+ + ", "
+ + controllerY
+ + ", "
+ + controllerZ
+ + ".");
+
+ // If recipe is running, display success chance
+ if (this.mMaxProgresstime != 0) {
+ ret.add(
+ "Success chance: " + EnumChatFormatting.YELLOW
+ + GT_Utility.formatNumbers(this.calculateFinalSuccessChance())
+ + "%"
+ + EnumChatFormatting.RESET);
+ }
+
+ } else ret.add("This Purification Unit is not linked to any Water Purification Plant.");
+ ret.add("Current parallel: " + EnumChatFormatting.YELLOW + this.effectiveParallel);
+ return ret.toArray(new String[0]);
+ }
+
+ @Override
+ public void getWailaBody(ItemStack itemStack, List<String> currenttip, IWailaDataAccessor accessor,
+ IWailaConfigHandler config) {
+ NBTTagCompound tag = accessor.getNBTData();
+
+ // Display linked controller in Waila.
+ if (tag.getBoolean("linked")) {
+ currenttip.add(
+ EnumChatFormatting.AQUA + "Linked to Purification Plant at "
+ + EnumChatFormatting.WHITE
+ + tag.getInteger("controllerX")
+ + ", "
+ + tag.getInteger("controllerY")
+ + ", "
+ + tag.getInteger("controllerZ")
+ + EnumChatFormatting.RESET);
+ } else {
+ currenttip.add(EnumChatFormatting.AQUA + "Unlinked");
+ }
+
+ super.getWailaBody(itemStack, currenttip, accessor, config);
+ }
+
+ @Override
+ public void getWailaNBTData(EntityPlayerMP player, TileEntity tile, NBTTagCompound tag, World world, int x, int y,
+ int z) {
+
+ tag.setBoolean("linked", getController() != null);
+ if (getController() != null) {
+ tag.setInteger("controllerX", controllerX);
+ tag.setInteger("controllerY", controllerY);
+ tag.setInteger("controllerZ", controllerZ);
+ }
+
+ super.getWailaNBTData(player, tile, tag, world, x, y, z);
+ }
+
+ public PurificationUnitStatus status() {
+ if (!this.mMachine) {
+ return PurificationUnitStatus.INCOMPLETE_STRUCTURE;
+ } else if (!this.isAllowedToWork()) {
+ return PurificationUnitStatus.DISABLED;
+ } else {
+ return PurificationUnitStatus.ONLINE;
+ }
+ }
+
+ /**
+ * Creates all widgets needed to sync this unit's status with the client
+ */
+ public Widget makeSyncerWidgets() {
+ return new MultiChildWidget()
+ .addChild(new FakeSyncWidget.BooleanSyncer(() -> this.mMachine, machine -> this.mMachine = machine))
+ .addChild(new FakeSyncWidget.BooleanSyncer(this::isAllowedToWork, _work -> {}));
+ }
+
+ private static final int PARALLEL_WINDOW_ID = 10;
+
+ @Override
+ public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) {
+ buildContext.addSyncedWindow(PARALLEL_WINDOW_ID, this::createParallelWindow);
+ builder.widget(new ButtonWidget().setOnClick((clickData, widget) -> {
+ if (!widget.isClient()) {
+ widget.getContext()
+ .openSyncedWindow(PARALLEL_WINDOW_ID);
+ }
+ })
+ .setPlayClickSound(true)
+ .setBackground(() -> {
+ List<UITexture> ret = new ArrayList<>();
+ ret.add(GT_UITextures.BUTTON_STANDARD);
+ ret.add(GT_UITextures.OVERLAY_BUTTON_BATCH_MODE_ON);
+ return ret.toArray(new IDrawable[0]);
+ })
+ .addTooltip(translateToLocal("GT5U.tpm.parallelwindow"))
+ .setTooltipShowUpDelay(TOOLTIP_DELAY)
+ .setPos(174, 129)
+ .setSize(16, 16));
+ super.addUIWidgets(builder, buildContext);
+ }
+
+ protected ModularWindow createParallelWindow(final EntityPlayer player) {
+ final int WIDTH = 158;
+ final int HEIGHT = 52;
+ final int PARENT_WIDTH = getGUIWidth();
+ final int PARENT_HEIGHT = getGUIHeight();
+ ModularWindow.Builder builder = ModularWindow.builder(WIDTH, HEIGHT);
+ builder.setBackground(GT_UITextures.BACKGROUND_SINGLEBLOCK_DEFAULT);
+ builder.setGuiTint(getGUIColorization());
+ builder.setDraggable(true);
+ builder.setPos(
+ (size, window) -> Alignment.Center.getAlignedPos(size, new Size(PARENT_WIDTH, PARENT_HEIGHT))
+ .add(
+ Alignment.BottomRight.getAlignedPos(new Size(PARENT_WIDTH, PARENT_HEIGHT), new Size(WIDTH, HEIGHT))
+ .add(WIDTH - 3, 0)
+ .subtract(0, 10)));
+ builder.widget(
+ TextWidget.localised("GTPP.CC.parallel")
+ .setPos(3, 4)
+ .setSize(150, 20))
+ .widget(
+ new NumericWidget().setSetter(val -> maxParallel = (int) val)
+ .setGetter(() -> maxParallel)
+ .setBounds(1, Integer.MAX_VALUE)
+ .setDefaultValue(1)
+ .setScrollValues(1, 4, 64)
+ .setTextAlignment(Alignment.Center)
+ .setTextColor(Color.WHITE.normal)
+ .setSize(150, 18)
+ .setPos(4, 25)
+ .setBackground(GT_UITextures.BACKGROUND_TEXT_FIELD)
+ .attachSyncer(
+ new FakeSyncWidget.IntegerSyncer(() -> maxParallel, (val) -> maxParallel = val),
+ builder));
+ return builder.build();
+ }
+
+ @Override
+ public boolean supportsVoidProtection() {
+ return false;
+ }
+
+ @Override
+ public Set<VoidingMode> getAllowedVoidingModes() {
+ return EnumSet.of(VoidingMode.VOID_NONE);
+ }
+
+ @Override
+ protected boolean supportsCraftingMEBuffer() {
+ return false;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitClarifier.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitClarifier.java
new file mode 100644
index 0000000000..fd97132ac1
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitClarifier.java
@@ -0,0 +1,333 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.GT_HatchElement.InputBus;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputBus;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_DISTILLATION_TOWER;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_DISTILLATION_TOWER_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_DISTILLATION_TOWER_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_DISTILLATION_TOWER_GLOW;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.IAlignmentLimits;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.recipe.check.CheckRecipeResult;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+
+public class GT_MetaTileEntity_PurificationUnitClarifier
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitClarifier>
+ implements ISurvivalConstructable {
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final String STRUCTURE_PIECE_MAIN_SURVIVAL = "main_survival";
+
+ private static final int STRUCTURE_X_OFFSET = 5;
+ private static final int STRUCTURE_Y_OFFSET = 2;
+ private static final int STRUCTURE_Z_OFFSET = 1;
+
+ // Chance that the filter is damaged every cycle.
+ public static final float FILTER_DAMAGE_RATE = 20.0f;
+
+ private static final int CASING_TEXTURE_INDEX = getTextureIndex(GregTech_API.sBlockCasings9, 5);
+
+ private static final String[][] structure =
+ // spotless:off
+ new String[][] {
+ { " ", " ", " ", " " },
+ { " ", " AAAAA ", " AH~HA ", " AAAAA " },
+ { " ", " A A ", " AWWWWWA ", " AAAAAAA " },
+ { " ", " A A ", " AWWWWWWWA ", " AAAAAAAAA " },
+ { " ", "A A", "AWWWCCCWWWA", "AAAAFFFAAAA" },
+ { " DDD ", "A A", "HWWCWWWCWWH", "AAAFFFFFAAA" },
+ { "DDDDDBD ", "A B A", "AWWCWBWCWWA", "AAAFFFFFAAA" },
+ { " DDD ", "A A", "HWWCWWWCWWH", "AAAFFFFFAAA" },
+ { " ", "A A", "AWWWCCCWWWA", "AAAAFFFAAAA" },
+ { " ", " A A ", " AWWWWWWWA ", " AAAAAAAAA " },
+ { " ", " A A ", " AWWWWWA ", " AAAAAAA " },
+ { " ", " AAAAA ", " AHAHA ", " AAAAA " } };
+ // spotless:on
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitClarifier> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitClarifier>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ .addShape(
+ STRUCTURE_PIECE_MAIN_SURVIVAL,
+ Arrays.stream(structure)
+ .map(
+ sa -> Arrays.stream(sa)
+ .map(s -> s.replaceAll("W", " "))
+ .toArray(String[]::new))
+ .toArray(String[][]::new))
+ // Hatches
+ .addElement(
+ 'H',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitClarifier>buildHatchAdder()
+ .atLeastList(t.getAllowedHatches())
+ .casingIndex(CASING_TEXTURE_INDEX)
+ .dot(1)
+ .build()),
+ // Reinforced Sterile Water Plant Casing
+ ofBlock(GregTech_API.sBlockCasings9, 5)))
+ // Reinforced Sterile Water Plant Casing
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings9, 5))
+ // PTFE pipe casing
+ .addElement('B', ofBlock(GregTech_API.sBlockCasings8, 1))
+ .addElement('C', ofFrame(Materials.Iridium))
+ .addElement('D', ofFrame(Materials.DamascusSteel))
+ .addElement('W', ofChain(ofBlock(Blocks.water, 0)))
+ // Filter machine casing
+ .addElement('F', ofBlock(GregTech_API.sBlockCasings3, 11))
+ .build();
+
+ public GT_MetaTileEntity_PurificationUnitClarifier(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitClarifier(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 1;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_LuV;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationClarifierRecipes;
+ }
+
+ @NotNull
+ @Override
+ public CheckRecipeResult overrideRecipeCheck() {
+ // Clarifier needs to check item inputs from recipe as well to find filter item
+ return findRecipeForInputs(
+ this.storedFluids.toArray(new FluidStack[] {}),
+ this.getStoredInputs()
+ .toArray(new ItemStack[] {}));
+ }
+
+ @Override
+ public void depleteRecipeInputs() {
+ super.depleteRecipeInputs();
+
+ // Now do random roll to determine if the filter should be destroyed
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ int roll = random.nextInt(1, 101);
+ if (roll < FILTER_DAMAGE_RATE) {
+ this.depleteInput(this.currentRecipe.mInputs[0]);
+ }
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitClarifier> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected IAlignmentLimits getInitialAlignmentLimits() {
+ // Rotated sifter not allowed, water will flow out.
+ return (d, r, f) -> d.offsetY == 0 && r.isNotRotated() && !f.isVerticallyFliped();
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ final GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Clarifier Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo("Requires a filter made of Activated Carbon to work.")
+ .addInfo(
+ "Every cycle, has a " + EnumChatFormatting.RED
+ + GT_Utility.formatNumbers(FILTER_DAMAGE_RATE)
+ + "%"
+ + EnumChatFormatting.GRAY
+ + " chance to destroy the filter.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The first step to acquiring purified water is to filter out macroscopic contaminants through the")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "use of large physical filters. As more contaminants are captured, the efficacy of the filter")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "decreases so continual replacements must be supplied to maintain full function of the Clarifier.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(11, 4, 11, false)
+ .addSeparator()
+ .addCasingInfoRangeColored(
+ "Reinforced Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 123,
+ 131,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Filter Machine Casing",
+ EnumChatFormatting.GRAY,
+ 21,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Iridium Frame Box",
+ EnumChatFormatting.GRAY,
+ 21,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Damascus Steel Frame Box",
+ EnumChatFormatting.GRAY,
+ 12,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored("PTFE Pipe Casing", EnumChatFormatting.GRAY, 3, EnumChatFormatting.GOLD, false)
+ .addController("Front center")
+ .addInputBus(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addOutputBus(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addInputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addOutputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addStructureInfo("Requires water to be placed in the structure.")
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ int built = survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN_SURVIVAL,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ if (built == -1) {
+ GT_Utility.sendChatToPlayer(
+ env.getActor(),
+ EnumChatFormatting.GREEN + "Auto placing done ! Now go place the water yourself !");
+ return 0;
+ }
+ return built;
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitClarifier(this.mName);
+ }
+
+ private List<IHatchElement<? super GT_MetaTileEntity_PurificationUnitClarifier>> getAllowedHatches() {
+ return ImmutableList.of(InputBus, InputHatch, OutputBus, OutputHatch);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection sideDirection,
+ ForgeDirection facingDirection, int colorIndex, boolean active, boolean redstoneLevel) {
+ if (sideDirection == facingDirection) {
+ if (active) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_TEXTURE_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_DISTILLATION_TOWER_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_DISTILLATION_TOWER_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_TEXTURE_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_DISTILLATION_TOWER)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_DISTILLATION_TOWER_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_TEXTURE_INDEX) };
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.GT_MACHINES_PURIFICATIONPLANT_LOOP.resourceLocation;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitDegasifier.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitDegasifier.java
new file mode 100644
index 0000000000..d0257ca3c2
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitDegasifier.java
@@ -0,0 +1,838 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.onElementPass;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Supplier;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import com.github.bartimaeusnek.bartworks.system.material.WerkstoffLoader;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_Input;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_MultiInput;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.IGT_HatchAdder;
+
+public class GT_MetaTileEntity_PurificationUnitDegasifier
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitDegasifier>
+ implements ISurvivalConstructable {
+
+ private static final int CASING_INDEX_MAIN = getTextureIndex(GregTech_API.sBlockCasings9, 11);
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+
+ // spotless:off
+ private static final String[][] structure = new String[][] {
+ { " AAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAA " },
+ { " AAAAAAAAA ", " AAAAA ", " ", " C ", " CC ", " C ", " CC ", " C ", " ", " ", " ", " C ", " CC ", " C ", " CC ", " C ", " ", " ", " ", " C ", " CC ", " C ", " CC ", " CCAA~AA ", " AAAAAAAAA " },
+ { " AAAAAAAAAAAAA ", " CAAAAAAAAA ", " BBBBB CC ", " BBBBB ", " BBBBB ", " BBB ", " B ", " ", " C ", " ", " C ", " ", " ", " ", " ", " ", " C ", " ", " B C ", " BBB ", " BBBBB ", " BBBBB ", " BBBBB ", " AAAAAAAAA ", " AAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAA ", " AAAAAAAAAAAC ", " C BBB BB ", " BB BB ", " B B ", " BB BB ", " BBB BBB ", " BBBBB ", " BBB ", " C C ", " ", " ", " ", " ", " ", " ", " BBB ", " C BBBBB C ", " BBB BBB ", " BB BB ", " B B ", " BB BB ", " BB BB ", " AAAAAAAAAAA ", " AAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAAAA ", " AAAAAAAAAAAAA ", " CB B ", " B B ", " B B ", " B B ", " B B ", " BB BB ", " BB BB C ", " BBBBB ", " C BBB ", " ", " ", " ", " BBB ", " BBBBB ", " BB BB C ", " BB BB ", " C B B ", " B B ", " B B ", " B B ", " B B ", " AAAAAAAAAAAAAC ", " AAAAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAAAA ", " AAAAAAAAAAAAA ", " B B ", " C B B ", " B B ", " B B ", " B B ", " B B C ", " B B ", " B B ", " B B ", " C BBB ", " ", " BBB ", " B B ", " B B C ", " B B ", " B B ", " B B ", " C B B ", " B B ", " B B ", " B B ", " AAAAAAAAAAAAAC ", " AAAAAAAAAAAAAAA " },
+ { "AAAAAAAAAAAAAAAAA", " AAAAAAAAAAAAAAA ", " B B ", " B B ", " CB B ", " B B ", " B B C ", " B B ", " B B ", " B B ", " B B ", " BBBBB ", " C BBB ", " BBBBB ", " B B C ", " B B ", " B B ", " B B ", " B B ", " B B ", " CB B ", " B B ", " B BC ", " AAAAAAAAAAAAAAA ", "AAAAAAAAAAAAAAAAA" },
+ { "AAAAAAAAAAAAAAAAA", " AAAAAAAAAAAAAAA ", " B B ", " B B ", " CB B ", " B B ", " B B C ", " B B ", " B B ", " B B ", " B B ", " BB BB ", " C BBBBB ", " BB BB ", " B B C ", " B B ", " B B ", " B B ", " B B ", " B B ", " CB B ", " B B ", " B BC ", " AAAAAAAAAAAAAAA ", "AAAAAAAAAAAAAAAAA" },
+ { "AAAAAAAAAAAAAAAAA", " AAAAAAAAAAAAAAA ", " B B ", " B B ", " B B ", " CB BC ", " B B ", " B B ", " B B ", " B B ", " B B ", " BB BB ", " BBBBB ", " C BB BB C ", " B B ", " B B ", " B B ", " B B ", " B B ", " B B ", " B B ", " CB BC ", " B B ", " AAAAAAAAAAAAAAA ", "AAAAAAAAAAAAAAAAA" },
+ { "AAAAAAAAAAAAAAAAA", " AAAAAAAAAAAAAAA ", " B B ", " B B ", " B BC ", " B B ", " C B B ", " B B ", " B B ", " B B ", " B B ", " BB BB ", " BBBBB C ", " BB BB ", " C B B ", " B B ", " B B ", " B B ", " B B ", " B B ", " B BC ", " B B ", " CB B ", " AAAAAAAAAAAAAAA ", "AAAAAAAAAAAAAAAAA" },
+ { "AAAAAAAAAAAAAAAAA", " AAAAAAAAAAAAAAA ", " B B ", " B B ", " B BC ", " B B ", " C B B ", " B B ", " B B ", " B B ", " B B ", " BBBBB ", " BBB C ", " BBBBB ", " C B B ", " B B ", " B B ", " B B ", " B B ", " B B ", " B BC ", " B B ", " CB B ", " AAAAAAAAAAAAAAA ", "AAAAAAAAAAAAAAAAA" },
+ { " AAAAAAAAAAAAAAA ", " AAAAAAAAAAAAA ", " B B ", " B B C ", " B B ", " B B ", " B B ", " C B B ", " B B ", " B B ", " B B ", " BBB C ", " ", " BBB ", " B B ", " C B B ", " B B ", " B B ", " B B ", " B B C ", " B B ", " B B ", " B B ", " CAAAAAAAAAAAAA ", " AAAAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAAAA ", " AAAAAAAAAAAAA ", " B BC ", " B B ", " B B ", " B B ", " B B ", " BB BB ", " C BB BB ", " BBBBB ", " BBB C ", " ", " ", " ", " BBB ", " BBBBB ", " C BB BB ", " BB BB ", " B B C ", " B B ", " B B ", " B B ", " B B ", " CAAAAAAAAAAAAA ", " AAAAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAA ", " CAAAAAAAAAAA ", " BB BB C ", " BB BB ", " B B ", " BB BB ", " BBB BBB ", " BBBBB ", " BBB ", " C C ", " ", " ", " ", " ", " ", " ", " BBB ", " C BBBBB C ", " BBB BBB ", " BB BB ", " B B ", " BB BB ", " BB BB ", " AAAAAAAAAAA ", " AAAAAAAAAAAAA " },
+ { " AAAAAAAAAAAAA ", " AAAAAAAAAC ", " CC BBBBB ", " BBBBB ", " BBBBB ", " BBB ", " B ", " ", " C ", " ", " C ", " ", " ", " ", " ", " ", " C ", " ", " C B ", " BBB ", " BBBBB ", " BBBBB ", " BBBBB ", " AAAAAAAAA ", " AAAAAAAAAAAAA " },
+ { " AAAAAAAAA ", " AAAAA ", " ", " C ", " CC ", " C ", " CC ", " C ", " ", " ", " ", " C ", " CC ", " C ", " CC ", " C ", " ", " ", " ", " C ", " CC ", " C ", " CC ", " AAAAACC ", " AAAAAAAAA " },
+ { " AAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAA " } };
+ // spotless:on
+
+ private int casingCount = 0;
+ private static final int MIN_CASING = 780;
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitDegasifier> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitDegasifier>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ .addElement(
+ 'A',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitDegasifier>buildHatchAdder()
+ .atLeastList(Arrays.asList(InputHatch, OutputHatch, SpecialHatchElement.ControlHatch))
+ .casingIndex(CASING_INDEX_MAIN)
+ .dot(1)
+ .cacheHint(() -> "Input Hatch, Output Hatch, Control Hatch")
+ .build()),
+ onElementPass(t -> t.casingCount++, ofBlock(GregTech_API.sBlockCasings9, 11))))
+ // Omni-purpose infinity fused glass
+ .addElement('B', ofBlock(GregTech_API.sBlockGlass1, 2))
+ .addElement('C', ofFrame(Materials.Bedrockium))
+ .build();
+
+ private static final int STRUCTURE_X_OFFSET = 8;
+ private static final int STRUCTURE_Y_OFFSET = 23;
+ private static final int STRUCTURE_Z_OFFSET = 1;
+
+ // Supplier because werkstoff loads later than multiblock controllers... fml
+ private static final Supplier<FluidStack[]> INERT_GASES = () -> new FluidStack[] { Materials.Helium.getGas(10000L),
+ WerkstoffLoader.Neon.getFluidOrGas(7500), WerkstoffLoader.Krypton.getFluidOrGas(5000),
+ WerkstoffLoader.Xenon.getFluidOrGas(2500) };
+
+ private static final class SuperconductorMaterial {
+
+ public FluidStack fluid;
+ public float multiplier;
+
+ SuperconductorMaterial(FluidStack fluid, float multiplier) {
+ this.fluid = fluid;
+ this.multiplier = multiplier;
+ }
+ }
+
+ private static final long SUPERCON_FLUID_AMOUNT = 1440L;
+
+ private static final Supplier<SuperconductorMaterial[]> SUPERCONDUCTOR_MATERIALS = () -> new SuperconductorMaterial[] {
+ new SuperconductorMaterial(Materials.Longasssuperconductornameforuvwire.getMolten(SUPERCON_FLUID_AMOUNT), 1.0f),
+ new SuperconductorMaterial(
+ Materials.Longasssuperconductornameforuhvwire.getMolten(SUPERCON_FLUID_AMOUNT),
+ 1.25f),
+ new SuperconductorMaterial(Materials.SuperconductorUEVBase.getMolten(SUPERCON_FLUID_AMOUNT), 1.5f),
+ new SuperconductorMaterial(Materials.SuperconductorUIVBase.getMolten(SUPERCON_FLUID_AMOUNT), 1.75f),
+ new SuperconductorMaterial(Materials.SuperconductorUMVBase.getMolten(SUPERCON_FLUID_AMOUNT), 2.0f), };
+
+ private static final FluidStack CATALYST_FLUID = Materials.Neutronium.getMolten(4608L);
+ private static final FluidStack COOLANT_FLUID = Materials.SuperCoolant.getFluid(10000L);
+
+ private static final long CONSUME_INTERVAL = 20;
+
+ private static class ControlSignal {
+
+ private byte signal;
+
+ public ControlSignal(byte sig) {
+ signal = sig;
+ }
+
+ public void randomize() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ // We want to give the final bit a lower chance at being on, since this bypasses most of the automation.
+ // If this bit has a 50% chance of being on, you could opt to not automate the degasser at all and never
+ // insert anything. This way you still get 50% output which might just be good enough.
+
+ // To do this weighting, we simply only generate the lower 3 bits, and then with a smaller chance we add
+ // 8 to the result
+ signal = (byte) random.nextInt(0, 8);
+ if (random.nextInt(0, 5) == 0) {
+ signal += 8;
+ }
+ }
+
+ public boolean getBit(int bit) {
+ if (bit < 0 || bit > 3) {
+ throw new IllegalArgumentException("Invalid bit index for degasser control signal");
+ }
+
+ // Shift signal so the requested bit is in the lowermost bit
+ // Then only keep the lowermost bit
+ // Then test if this bit is on.
+ return ((signal >> bit) & 1) == 1;
+ }
+
+ public byte getSignal() {
+ return signal;
+ }
+
+ // Get integer value representing control bits 1 and 2
+ public int getControlBit12() {
+ return (signal >> 1) & 0b11;
+ }
+
+ public boolean isZero() {
+ return signal == (byte) 0;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toBinaryString(((int) signal) & 0b1111);
+ }
+ }
+
+ private static class ControlBitStatus {
+
+ public FluidStack stack;
+ public boolean satisfied;
+
+ public ControlBitStatus(FluidStack stack, boolean satisfied) {
+ this.stack = stack;
+ this.satisfied = satisfied;
+ }
+ }
+
+ private ControlSignal controlSignal = new ControlSignal((byte) 0);
+
+ private final HashMap<Fluid, FluidStack> insertedStuffThisCycle = new HashMap<>();
+
+ private float outputMultiplier = 1.0f;
+
+ private GT_MetaTileEntity_Hatch_DegasifierControlHatch controlHatch = null;
+
+ public GT_MetaTileEntity_PurificationUnitDegasifier(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ protected GT_MetaTileEntity_PurificationUnitDegasifier(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitDegasifier(mName);
+ }
+
+ @Override
+ protected void setHatchRecipeMap(GT_MetaTileEntity_Hatch_Input hatch) {
+ // Do nothing, we don't want to lock hatches to recipe maps since this can cause
+ // them to reject our catalyst fluids
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection side, ForgeDirection facing,
+ int colorIndex, boolean active, boolean redstoneLevel) {
+ if (side == facing) {
+ if (active) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitDegasifier> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Residual Decontaminant Degasser Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "At the start of the operation, the " + EnumChatFormatting.WHITE
+ + "Degasser Control Hatch"
+ + EnumChatFormatting.GRAY
+ + " will output a redstone signal.")
+ .addInfo("To succeed the recipe, you will need to successfully decode the instructions in the signal.")
+ .addInfo("To decode the signal, interpret the signal strength as a 4-bit number from 0-15.")
+ .addInfo("Denote the lowest bit as bit 1, and the highest as bit 4.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.WHITE + ""
+ + EnumChatFormatting.BOLD
+ + "Bit 1: "
+ + EnumChatFormatting.BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "Ozone Sparging by Inert Gas")
+ .addInfo(
+ "If this bit is on, you must insert an " + EnumChatFormatting.WHITE
+ + "inert gas"
+ + EnumChatFormatting.GRAY
+ + " into the machine.")
+ .addInfo(
+ "To determine which gas to insert, interpret bits " + EnumChatFormatting.WHITE
+ + "2-3"
+ + EnumChatFormatting.GRAY
+ + " as a 2-bit number.")
+ .addInfo(
+ EnumChatFormatting.GRAY + "0: "
+ + EnumChatFormatting.RED
+ + "10000L "
+ + EnumChatFormatting.WHITE
+ + "Helium"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + "1: "
+ + EnumChatFormatting.RED
+ + "7500L "
+ + EnumChatFormatting.WHITE
+ + "Neon"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + "2: "
+ + EnumChatFormatting.RED
+ + "5000L "
+ + EnumChatFormatting.WHITE
+ + "Krypton"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + "3: "
+ + EnumChatFormatting.RED
+ + "2500L "
+ + EnumChatFormatting.WHITE
+ + "Xenon")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.WHITE + ""
+ + EnumChatFormatting.BOLD
+ + "Bit 2: "
+ + EnumChatFormatting.BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "Superconductive Deionization")
+ .addInfo(
+ "If this bit is on, you must insert " + EnumChatFormatting.RED
+ + "1440L "
+ + EnumChatFormatting.WHITE
+ + "Molten Superconductor Base.")
+ .addInfo("Using higher tier superconductor provides bonus output.")
+ .addInfo(
+ "Output multiplier: " + EnumChatFormatting.DARK_GREEN
+ + "UV"
+ + EnumChatFormatting.GRAY
+ + ": "
+ + EnumChatFormatting.WHITE
+ + "1x"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + EnumChatFormatting.DARK_RED
+ + "UHV"
+ + EnumChatFormatting.GRAY
+ + ": "
+ + EnumChatFormatting.WHITE
+ + "1.25x"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + EnumChatFormatting.DARK_PURPLE
+ + "UEV"
+ + EnumChatFormatting.GRAY
+ + ": "
+ + EnumChatFormatting.WHITE
+ + "1.5x"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + EnumChatFormatting.DARK_BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "UIV"
+ + EnumChatFormatting.GRAY
+ + ": "
+ + EnumChatFormatting.WHITE
+ + "1.75x"
+ + EnumChatFormatting.GRAY
+ + " / "
+ + EnumChatFormatting.RED
+ + ""
+ + EnumChatFormatting.BOLD
+ + "UMV"
+ + EnumChatFormatting.GRAY
+ + ": "
+ + EnumChatFormatting.WHITE
+ + "2x")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.WHITE + ""
+ + EnumChatFormatting.BOLD
+ + "Bit 3: "
+ + EnumChatFormatting.BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "Gravitationally-Generated Differential Vacuum Extraction")
+ .addInfo(
+ "If this bit is on, you must insert " + EnumChatFormatting.RED
+ + "4608L "
+ + EnumChatFormatting.WHITE
+ + "Molten Neutronium")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.WHITE + ""
+ + EnumChatFormatting.BOLD
+ + "Bit 4: "
+ + EnumChatFormatting.BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "Seldonian Settlement Process")
+ .addInfo(
+ "If this bit is on," + EnumChatFormatting.RED
+ + " DISREGARD "
+ + EnumChatFormatting.GRAY
+ + "all other bits and do not insert anything.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.WHITE + ""
+ + EnumChatFormatting.BOLD
+ + "No bits: "
+ + EnumChatFormatting.BLUE
+ + ""
+ + EnumChatFormatting.BOLD
+ + "Machine Overload")
+ .addInfo("In rare cases, the machine may overload and output no control signal at all.")
+ .addInfo(
+ "To prevent machine damage, insert " + EnumChatFormatting.RED
+ + "10000L "
+ + EnumChatFormatting.WHITE
+ + "Super Coolant.")
+ .addSeparator()
+ .addInfo("The recipe can only succeed if the entire signal is decoded correctly.")
+ .addInfo("Inserting any fluid not requested by the signal will always void the recipe.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The penultimate stage of water purification, step seven, is an irregular series of complex")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "processes designed to remove any residual materials left by the decontaminants from the previous")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "steps such as any energetic ions, acids, clarifiers, or gasses. Depending on what the Degasser")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "detects in the water, it will request various materials to complete the processes listed above.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(17, 25, 17, false)
+ .addCasingInfoRangeColored(
+ "Heat-Resistant Trinium Plated Casing",
+ EnumChatFormatting.GRAY,
+ MIN_CASING,
+ 803,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Omni-Purpose Infinity Fused Glass",
+ EnumChatFormatting.GRAY,
+ 622,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Bedrockium Frame Box",
+ EnumChatFormatting.GRAY,
+ 126,
+ EnumChatFormatting.GOLD,
+ false)
+ .addController("Front center")
+ .addOutputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+, Any Trinium Casing", 1)
+ .addInputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+, Any Trinium Casing", 1)
+ .addOtherStructurePart(
+ "Degasser Control Hatch",
+ EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + ", Any Trinium Casing",
+ 1)
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationDegasifierRecipes;
+ }
+
+ // Whether this fluid stack is accepted as a fluid in the inputs that the unit requires at runtime.
+ public static boolean isValidFluid(FluidStack stack) {
+ return stack.isFluidEqual(CATALYST_FLUID) || stack.isFluidEqual(COOLANT_FLUID)
+ || Arrays.stream(INERT_GASES.get())
+ .anyMatch(stack::isFluidEqual)
+ || Arrays.stream(SUPERCONDUCTOR_MATERIALS.get())
+ .anyMatch(mat -> stack.isFluidEqual(mat.fluid));
+ }
+
+ // Check if an exact match for this FluidStack was found in the map of inserted fluids
+ private boolean wasFluidInsertedExact(FluidStack toFind) {
+ FluidStack candidate = insertedStuffThisCycle.get(toFind.getFluid());
+ // Fluid was inserted if found and the amount matches
+ return candidate != null && candidate.amount == toFind.amount;
+ }
+
+ private ControlBitStatus isBit0Satisfied() {
+ // Check if instructions matching the first bit are satisfied.
+ // Instructions:
+ // If bit 0 is on, insert an inert gas. Bits 1-2 of the control signal determine which inert
+ // gas to insert.
+
+ if (controlSignal.getBit(0)) {
+ // Grab the gas to insert from the control bits
+ int gasToInsert = controlSignal.getControlBit12();
+ FluidStack gasStack = INERT_GASES.get()[gasToInsert];
+ // Check if it was inserted
+ if (wasFluidInsertedExact(gasStack)) return new ControlBitStatus(gasStack, true);
+ else return new ControlBitStatus(null, false);
+ }
+
+ // Bit 0 is not on, so this is always satisfied
+ return new ControlBitStatus(null, true);
+ }
+
+ private ControlBitStatus isBit1Satisfied() {
+ // Check if instructions matching the second bit (bit 1) are satisfied.
+ // Instructions:
+ // If bit 1 is on, insert molten superconductor.
+ // Higher tier superconductor gives a better bonus.
+ // Only one type of superconductor may be inserted or the operation fails,
+ // so we don't care about the order in which we find it.
+ if (controlSignal.getBit(1)) {
+ // Find the first superconductor material in the list that was inserted with an exact match
+ Optional<SuperconductorMaterial> material = Arrays.stream(SUPERCONDUCTOR_MATERIALS.get())
+ .filter(candidate -> wasFluidInsertedExact(candidate.fluid))
+ .findFirst();
+ if (material.isPresent()) {
+ // Get the material and set the output multiplier, then
+ // report success with the matching fluid.
+ SuperconductorMaterial scMaterial = material.get();
+ this.outputMultiplier = scMaterial.multiplier;
+ return new ControlBitStatus(scMaterial.fluid, true);
+ }
+ // No superconductor was inserted but bit is on fail.
+ return new ControlBitStatus(null, false);
+ }
+
+ return new ControlBitStatus(null, true);
+ }
+
+ private ControlBitStatus isBit2Satisfied() {
+ // Check if instructions matching the third bit (bit 2) are satisfied.
+ // Instructions:
+ // If bit 2 is on, insert molten neutronium.
+ if (controlSignal.getBit(2)) {
+ // If steel was inserted, return it and report success.
+ if (wasFluidInsertedExact(CATALYST_FLUID)) return new ControlBitStatus(CATALYST_FLUID, true);
+ // Otherwise report failure.
+ return new ControlBitStatus(null, false);
+ }
+
+ return new ControlBitStatus(null, true);
+ }
+
+ private ControlBitStatus isBit3Satisfied() {
+ // Check if instructions matching the fourth bit (bit 3) are satisfied.
+ // Instructions:
+ // If bit 3 is on, do not insert anything.
+ if (controlSignal.getBit(3)) {
+ // Simply check if the map of inserted fluids is empty
+ if (insertedStuffThisCycle.isEmpty()) return new ControlBitStatus(null, true);
+ return new ControlBitStatus(null, false);
+ }
+ return new ControlBitStatus(null, true);
+ }
+
+ private boolean areAllBitsSatisfied() {
+ // Check if each individual bit is satisfied.
+ // Additional instructions: If no bits are on, insert super coolant
+
+ if (controlSignal.isZero()) {
+ return wasFluidInsertedExact(COOLANT_FLUID);
+ }
+
+ ControlBitStatus bit0 = isBit0Satisfied();
+ ControlBitStatus bit1 = isBit1Satisfied();
+ ControlBitStatus bit2 = isBit2Satisfied();
+ ControlBitStatus bit3 = isBit3Satisfied();
+
+ // If bit 3 is satisfied and on, all other bits are automatically satisfied,
+ // with no fluids being allowed to be inserted.
+ if (controlSignal.getBit(3) && bit3.satisfied) {
+ bit0 = bit1 = bit2 = new ControlBitStatus(null, true);
+ }
+
+ if (bit0.satisfied && bit1.satisfied && bit2.satisfied && bit3.satisfied) {
+ // Check if the map contains any other stacks than the ones in the control bit statuses
+ for (FluidStack inserted : insertedStuffThisCycle.values()) {
+ // If the inserted stack is null, or any of the fluids required, this stack is fine.
+ if (inserted == null) continue;
+ if (bit0.stack != null && inserted.isFluidEqual(bit0.stack)) continue;
+ if (bit1.stack != null && inserted.isFluidEqual(bit1.stack)) continue;
+ if (bit2.stack != null && inserted.isFluidEqual(bit2.stack)) continue;
+ if (bit3.stack != null && inserted.isFluidEqual(bit3.stack)) continue;
+ // Otherwise it's a nonrequested stack and the recipe should fail.
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ this.controlSignal.randomize();
+ this.insertedStuffThisCycle.clear();
+ this.outputMultiplier = 1.0f;
+ // Make sure to output the hatch control signal.
+ this.controlHatch.updateOutputSignal(this.controlSignal.getSignal());
+ }
+
+ private static ArrayList<FluidStack> getDrainableFluidsFromHatch(GT_MetaTileEntity_Hatch_Input hatch) {
+ // Need special handling for quad input hatches, otherwise it only returns the first fluid in the hatch
+ if (hatch instanceof GT_MetaTileEntity_Hatch_MultiInput) {
+ return new ArrayList<>(Arrays.asList(((GT_MetaTileEntity_Hatch_MultiInput) hatch).getStoredFluid()));
+ }
+ return new ArrayList<>(Collections.singletonList(hatch.getFluid()));
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+
+ // If machine is running, continuously consume all valid inputs
+ if (mMaxProgresstime > 0 && aTick % CONSUME_INTERVAL == 0) {
+ // For each hatch, check if each fluid inside is one of the valid fluids. If so, consume it all.
+ for (GT_MetaTileEntity_Hatch_Input hatch : mInputHatches) {
+ ArrayList<FluidStack> drainableFluids = getDrainableFluidsFromHatch(hatch);
+ for (FluidStack fluid : drainableFluids) {
+ if (fluid != null && isValidFluid(fluid)) {
+ // Apparently this parameter is mostly ignored, but might as well get it right.
+ ForgeDirection front = hatch.getBaseMetaTileEntity()
+ .getFrontFacing();
+ // Drain the fluid and save it
+ FluidStack drainedFluid = hatch.drain(front, fluid, true);
+ // If the fluid does not yet exist in the map, insert it.
+ // Otherwise, merge the amounts
+ insertedStuffThisCycle.merge(
+ fluid.getFluid(),
+ drainedFluid,
+ (a, b) -> new FluidStack(a.getFluid(), a.amount + b.amount));
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addRecipeOutputs() {
+ super.addRecipeOutputs();
+ if (outputMultiplier > 1.01f) {
+ FluidStack waterOutput = currentRecipe.mFluidOutputs[0];
+ FluidStack bonusOutput = new FluidStack(
+ waterOutput.getFluid(),
+ (int) (waterOutput.amount * (outputMultiplier - 1.0f)));
+ this.addOutput(bonusOutput);
+ }
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ // Success chance is 100% when all bits are satisfied, 0% otherwise.
+ if (areAllBitsSatisfied()) {
+ return 100.0f;
+ } else {
+ return 0.0f;
+ }
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 7;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_UHV;
+ }
+
+ public boolean addControlHatchToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_Hatch_DegasifierControlHatch) {
+ // Only allow a single control hatch, so fail structure check if there is already one
+ if (this.controlHatch == null) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ this.controlHatch = (GT_MetaTileEntity_Hatch_DegasifierControlHatch) aMetaTileEntity;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ this.casingCount = 0;
+ this.controlHatch = null;
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ if (casingCount < MIN_CASING) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ controlSignal = new ControlSignal(aNBT.getByte("controlSignal"));
+ outputMultiplier = aNBT.getFloat("outputMultiplier");
+ NBTTagCompound fluidMap = aNBT.getCompoundTag("insertedFluidMap");
+ for (String key : fluidMap.func_150296_c()) {
+ FluidStack fluid = FluidStack.loadFluidStackFromNBT(fluidMap.getCompoundTag(key));
+ // Ignore if fluid failed to load, for example if the fluid ID was changed between versions
+ if (fluid == null) {
+ continue;
+ }
+ insertedStuffThisCycle.put(fluid.getFluid(), fluid);
+ }
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ aNBT.setByte("controlSignal", controlSignal.getSignal());
+ aNBT.setFloat("outputMultiplier", outputMultiplier);
+ NBTTagCompound fluidMap = new NBTTagCompound();
+ for (FluidStack stack : insertedStuffThisCycle.values()) {
+ NBTTagCompound compound = new NBTTagCompound();
+ stack.writeToNBT(compound);
+ fluidMap.setTag(
+ stack.getFluid()
+ .getName(),
+ compound);
+ }
+ aNBT.setTag("insertedFluidMap", fluidMap);
+ }
+
+ private static String generateInfoStringForBit(int i, ControlBitStatus status) {
+ String base = "Bit " + (i + 1) + " status: ";
+ if (status.satisfied) {
+ return base + EnumChatFormatting.GREEN + "OK";
+ } else {
+ return base + EnumChatFormatting.RED + "NOT OK";
+ }
+ }
+
+ @Override
+ public String[] getInfoData() {
+ ArrayList<String> info = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ info.add("Current control signal: " + EnumChatFormatting.YELLOW + controlSignal.toString());
+ info.add("Current output multiplier: " + EnumChatFormatting.YELLOW + outputMultiplier);
+ for (FluidStack stack : insertedStuffThisCycle.values()) {
+ info.add(
+ "Fluid inserted this cycle: " + EnumChatFormatting.YELLOW
+ + stack.amount
+ + "L "
+ + stack.getLocalizedName());
+ }
+ info.add(generateInfoStringForBit(0, isBit0Satisfied()));
+ info.add(generateInfoStringForBit(1, isBit1Satisfied()));
+ info.add(generateInfoStringForBit(2, isBit2Satisfied()));
+ info.add(generateInfoStringForBit(3, isBit3Satisfied()));
+ return info.toArray(new String[] {});
+ }
+
+ private enum SpecialHatchElement implements IHatchElement<GT_MetaTileEntity_PurificationUnitDegasifier> {
+
+ ControlHatch(GT_MetaTileEntity_PurificationUnitDegasifier::addControlHatchToMachineList,
+ GT_MetaTileEntity_Hatch_DegasifierControlHatch.class) {
+
+ @Override
+ public long count(GT_MetaTileEntity_PurificationUnitDegasifier mte) {
+ return mte.controlHatch == null ? 0 : 1;
+ }
+ };
+
+ private final List<Class<? extends IMetaTileEntity>> mteClasses;
+ private final IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitDegasifier> adder;
+
+ @SafeVarargs
+ SpecialHatchElement(IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitDegasifier> adder,
+ Class<? extends IMetaTileEntity>... mteClasses) {
+ this.mteClasses = Collections.unmodifiableList(Arrays.asList(mteClasses));
+ this.adder = adder;
+ }
+
+ @Override
+ public List<? extends Class<? extends IMetaTileEntity>> mteClasses() {
+ return mteClasses;
+ }
+
+ public IGT_HatchAdder<? super GT_MetaTileEntity_PurificationUnitDegasifier> adder() {
+ return adder;
+ }
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitFlocculation.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitFlocculation.java
new file mode 100644
index 0000000000..8090b32f30
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitFlocculation.java
@@ -0,0 +1,496 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlockAnyMeta;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.onElementPass;
+import static gregtech.api.enums.GT_HatchElement.InputBus;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputBus;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.util.GT_RecipeBuilder.SECONDS;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.FluidStack;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.IAlignmentLimits;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_Input;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.shutdown.ShutDownReasonRegistry;
+
+public class GT_MetaTileEntity_PurificationUnitFlocculation
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitFlocculation>
+ implements ISurvivalConstructable {
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final String STRUCTURE_PIECE_MAIN_SURVIVAL = "main_survival";
+
+ private static final int STRUCTURE_X_OFFSET = 4;
+ private static final int STRUCTURE_Y_OFFSET = 3;
+ private static final int STRUCTURE_Z_OFFSET = 0;
+
+ /**
+ * Amount of input fluid needed to boost the success chance by another level
+ */
+ public static final long INPUT_CHEMICAL_PER_LEVEL = 100000;
+ /**
+ * Amount of waste water produced for each success chance level. This matches the amount of input fluid
+ * so it can be perfectly recycled into each other.
+ */
+ private static final long WASTE_WATER_PER_LEVEL = INPUT_CHEMICAL_PER_LEVEL;
+ /**
+ * Additive boost to success chance for each level of input fluid supplied
+ */
+ public static final float SUCCESS_PER_LEVEL = 10.0f;
+ /**
+ * Amount of ticks between each tick the unit will try to consume input fluid
+ */
+ private static final int CONSUME_INTERVAL = 1 * SECONDS;
+
+ /**
+ * Fluid that needs to be supplied to boost success chance
+ */
+ private static final Materials INPUT_CHEMICAL = Materials.PolyAluminiumChloride;
+ /**
+ * Output fluid to be produced as waste. The intended behaviour is that this output fluid can be cycled
+ * semi-perfectly into the input fluid.
+ */
+ private static final Materials OUTPUT_WASTE = Materials.FlocculationWasteLiquid;
+
+ /**
+ * Total amount of input fluid consumed during this recipe cycle.
+ */
+ private long inputFluidConsumed = 0;
+
+ private static final String[][] structure = new String[][]
+ // spotless:off
+ {
+ { " ", " ", " BBBBBBB ", " BBB~BBB ", " BBBBBBB " },
+ { " ", " ", " B B ", " BWWWWWB ", " BCCCCCB " },
+ { " ", " ", " B B ", " GWWWWWG ", " BCAAACB " },
+ { " ", " ", " B B ", " GWWWWWG ", " BCAAACB " },
+ { " ", " ", " B B ", " GWWWWWG ", " BCAAACB " },
+ { " ", " EE EE ", " BE EB ", " BEWWWEB ", " BCCCCCB " },
+ { "D D", "DEE EED", "DBBBBBBBD", "DBBBBBBBD", "DBBBBBBBD" },
+ { "DD DD", "DD DD", "DD DD", "DD DD", "DD DD" }
+ };
+ // spotless:on
+
+ private static final int MAIN_CASING_INDEX = getTextureIndex(GregTech_API.sBlockCasings9, 6);
+
+ private int casingCount = 0;
+ private static final int MIN_CASING = 56;
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitFlocculation> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitFlocculation>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ .addShape(
+ STRUCTURE_PIECE_MAIN_SURVIVAL,
+ Arrays.stream(structure)
+ .map(
+ sa -> Arrays.stream(sa)
+ .map(s -> s.replaceAll("W", " "))
+ .toArray(String[]::new))
+ .toArray(String[][]::new))
+ // Filter machine casing
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings3, 11))
+ .addElement(
+ 'B',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitFlocculation>buildHatchAdder()
+ .atLeastList(t.getAllowedHatches())
+ .casingIndex(MAIN_CASING_INDEX)
+ .dot(1)
+ .build()),
+ // Clean Flocculation Casing
+ onElementPass(t -> t.casingCount++, ofBlock(GregTech_API.sBlockCasings9, 6))))
+ // Reinforced Sterile Water Plant Casing
+ .addElement('C', ofBlock(GregTech_API.sBlockCasings9, 5))
+ // Sterile Water Plant Casing
+ .addElement('D', ofBlock(GregTech_API.sBlockCasings9, 4))
+ .addElement('E', ofFrame(Materials.Adamantium))
+ .addElement('W', ofBlock(Blocks.water, 0))
+ // Tinted industrial glass
+ .addElement('G', ofBlockAnyMeta(GregTech_API.sBlockTintedGlass))
+ .build();
+
+ List<IHatchElement<? super GT_MetaTileEntity_PurificationUnitFlocculation>> getAllowedHatches() {
+ return ImmutableList.of(InputBus, InputHatch, OutputBus, OutputHatch);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitFlocculation(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitFlocculation(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitFlocculation(this.mName);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity aBaseMetaTileEntity, ForgeDirection side, ForgeDirection aFacing,
+ int colorIndex, boolean aActive, boolean redstoneLevel) {
+ if (side == aFacing) {
+ if (aActive) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ int built = survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN_SURVIVAL,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ if (built == -1) {
+ GT_Utility.sendChatToPlayer(
+ env.getActor(),
+ EnumChatFormatting.GREEN + "Auto placing done ! Now go place the water yourself !");
+ return 0;
+ }
+ return built;
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitFlocculation> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected IAlignmentLimits getInitialAlignmentLimits() {
+ // Do not allow rotation when water would flow out
+ return (d, r, f) -> d.offsetY == 0 && r.isNotRotated() && !f.isVerticallyFliped();
+ }
+
+ @Override
+ protected void setHatchRecipeMap(GT_MetaTileEntity_Hatch_Input hatch) {
+ // Do nothing, we don't want to lock hatches to recipe maps since this can cause
+ // them to reject our catalyst fluids
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ casingCount = 0;
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+
+ // At most two input hatches allowed
+ if (mInputHatches.size() > 2) {
+ return false;
+ }
+
+ // At most two output hatches allowed
+ if (mOutputHatches.size() > 2) {
+ return false;
+ }
+
+ if (casingCount < MIN_CASING) return false;
+
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ final GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Flocculation Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "Supply with " + EnumChatFormatting.WHITE
+ + INPUT_CHEMICAL.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " to operate.")
+ .addInfo(
+ "Outputs " + EnumChatFormatting.WHITE
+ + OUTPUT_WASTE.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " that can be recycled.")
+ .addSeparator()
+ .addInfo(
+ "During operation, will consume ALL " + EnumChatFormatting.WHITE
+ + INPUT_CHEMICAL.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " in the input hatch.")
+ .addInfo(
+ "At the end of the recipe, for every " + EnumChatFormatting.RED
+ + INPUT_CHEMICAL_PER_LEVEL
+ + "L "
+ + EnumChatFormatting.GRAY
+ + "of "
+ + EnumChatFormatting.WHITE
+ + INPUT_CHEMICAL.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " consumed")
+ .addInfo(
+ "gain an additive " + EnumChatFormatting.RED
+ + SUCCESS_PER_LEVEL
+ + "%"
+ + EnumChatFormatting.GRAY
+ + " increase to success. If total fluid supplied is not")
+ .addInfo(
+ "a multiple of " + EnumChatFormatting.RED
+ + INPUT_CHEMICAL_PER_LEVEL
+ + "L"
+ + EnumChatFormatting.GRAY
+ + ", a penalty to success is applied using the following formula:")
+ .addInfo(EnumChatFormatting.GREEN + "Success = Success * 2^(-10 * Overflow ratio)")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "Step three in purifying water is to remove microscopic contaminants such as dusts, microplastics and other")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "pollutants using a clarifying agent (In this case, polyaluminium chloride) to cause flocculation - the process")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "of aggregating dispersed suspended particles from a solution into larger clumps for further filtration.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(7, 4, 7, false)
+ .addCasingInfoRangeColored(
+ "Slick Sterile Flocculation Casing",
+ EnumChatFormatting.GRAY,
+ MIN_CASING,
+ 65,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 16,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Reinforced Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 30,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Tinted Industrial Glass",
+ EnumChatFormatting.GRAY,
+ 6,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Adamantium Frame Box",
+ EnumChatFormatting.GRAY,
+ 12,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Filter Machine Casing",
+ EnumChatFormatting.GRAY,
+ 9,
+ EnumChatFormatting.GOLD,
+ false)
+ .addController("Front center")
+ .addOutputBus(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addInputHatch(
+ EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "-" + EnumChatFormatting.GOLD + "2",
+ 1)
+ .addOutputHatch(
+ EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "-" + EnumChatFormatting.GOLD + "2",
+ 1)
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ // Reset amount of fluid consumed in this cycle.
+ this.inputFluidConsumed = 0;
+ }
+
+ @Override
+ public void endCycle() {
+ super.endCycle();
+ // Output waste water proportional to amount of boost levels. We do this even when the recipe fails, so you can
+ // always fully recycle.
+ // NOTE: If this process ever PRODUCES excess chlorine, there is a recipe bug.
+ int levels = calculateBoostLevels();
+ long amount = levels * WASTE_WATER_PER_LEVEL;
+ this.addFluidOutputs(new FluidStack[] { OUTPUT_WASTE.getFluid(amount) });
+ // Make sure to reset consumed fluid (again)
+ this.inputFluidConsumed = 0;
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+
+ // Consume all input fluid periodically, only when running
+ if (aTick % CONSUME_INTERVAL == 0 && mMaxProgresstime > 0) {
+ // Iterate over all fluids stored
+ List<FluidStack> fluids = this.getStoredFluids();
+ for (FluidStack fluid : fluids) {
+ // If this FluidStack is the input chemical, consume it all
+ if (fluid.getFluid()
+ .equals(INPUT_CHEMICAL.mFluid)) {
+ this.inputFluidConsumed += fluid.amount;
+ if (!this.depleteInput(fluid)) {
+ stopMachine(ShutDownReasonRegistry.outOfFluid(fluid));
+ }
+ }
+ }
+ }
+ }
+
+ private int calculateBoostLevels() {
+ return (int) Math.floor((float) this.inputFluidConsumed / (float) INPUT_CHEMICAL_PER_LEVEL);
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ // Amount of times the required amount of input fluid was fully inserted
+ int levels = calculateBoostLevels();
+ // Target amount of fluid needed to reach this amount of boost levels
+ long targetAmount = levels * INPUT_CHEMICAL_PER_LEVEL;
+ // Amount of excess fluid inserted.
+ long overflow = inputFluidConsumed - targetAmount;
+ // Base boost chance, before applying overflow penalty
+ float boost = SUCCESS_PER_LEVEL * levels;
+ // If there was any overflow, apply an exponential penalty multiplier based on percentage overflow
+ if (overflow > 0) {
+ float overflowPct = (float) overflow / INPUT_CHEMICAL_PER_LEVEL;
+ float penaltyMultiplier = (float) Math.pow(2.0f, overflowPct * -10.0);
+ return Math.max(0.0f, (this.currentRecipeChance + boost) * penaltyMultiplier);
+ } else {
+ return Math.min(100.0f, this.currentRecipeChance + boost);
+ }
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 3;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_ZPM;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationFlocculationRecipes;
+ }
+
+ @Override
+ public String[] getInfoData() {
+ ArrayList<String> infoData = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ infoData.add(
+ INPUT_CHEMICAL.mLocalizedName + " consumed this cycle: "
+ + EnumChatFormatting.RED
+ + inputFluidConsumed
+ + "L");
+ return infoData.toArray(new String[] {});
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ aNBT.setLong("mInputFluidConsumed", inputFluidConsumed);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ this.inputFluidConsumed = aNBT.getLong("mInputFluidConsumed");
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.GT_MACHINES_COAGULATION_LOOP.resourceLocation;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitOzonation.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitOzonation.java
new file mode 100644
index 0000000000..f569273445
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitOzonation.java
@@ -0,0 +1,299 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlockAnyMeta;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.onElementPass;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputBus;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.recipe.check.CheckRecipeResult;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+
+public class GT_MetaTileEntity_PurificationUnitOzonation
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitOzonation>
+ implements ISurvivalConstructable {
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final String STRUCTURE_PIECE_MAIN_SURVIVAL = "main_survival";
+
+ private static final String[][] structure = new String[][] {
+ // spotless:off
+ { " ", " ", " A ", " A ", " AAA ", " AAA ", " A A ", " A A ", " A A ", " A~A " },
+ { " A ", " A ", " A A ", " A A ", "BBBBA A", "BDDBA A", "BBBBA D A", "E A D A", "E A D A", "E AAAAA" },
+ { " AAA ", " A A ", " A A", " A A", "BDDBA A", "O BA A", "BBBBA A", " C A A", " CCA A", " AAAAA" },
+ { " A ", " A ", " A A ", " A A ", "BBBBA A", "BDDBA A", "BBBBA A", "E A A", "E A A", "E AAAAA" },
+ { " ", " ", " A ", " A ", " AAA ", " AAA ", " AAA ", " AAA ", " AAA ", " AAA " } };
+ // spotless:on
+
+ private static final int MAIN_CASING_INDEX = getTextureIndex(GregTech_API.sBlockCasings9, 10);
+
+ private static final int OFFSET_X = 6;
+ private static final int OFFSET_Y = 9;
+ private static final int OFFSET_Z = 0;
+
+ /**
+ * If the player inserts more ozone gas than this amount, the multi will explode.
+ */
+ public static final int MAX_OZONE_GAS_FOR_EXPLOSION = 1000 * (int) Math.pow(2, 10);
+
+ private int casingCount = 0;
+ private static final int MIN_CASING = 96;
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitOzonation> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitOzonation>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ // Inert Filtration Casing
+ .addElement(
+ 'A',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitOzonation>buildHatchAdder()
+ .atLeastList(ImmutableList.of(InputHatch, OutputHatch, OutputBus))
+ .casingIndex(getTextureIndex(GregTech_API.sBlockCasings9, 10))
+ .dot(1)
+ .build()),
+ onElementPass(t -> t.casingCount++, ofBlock(GregTech_API.sBlockCasings9, 10))))
+ // High Pressure Resistant Casing (possibly placeholder name)
+ .addElement('B', ofBlock(GregTech_API.sBlockCasings9, 9))
+ // PTFE pipe casing
+ .addElement('C', ofBlock(GregTech_API.sBlockCasings8, 1))
+ // Any tinted industrial glass
+ .addElement('D', ofBlockAnyMeta(GregTech_API.sBlockTintedGlass))
+ .addElement('E', ofFrame(Materials.TungstenSteel))
+ // Ozone input hatch
+ .addElement(
+ 'O',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitOzonation>buildHatchAdder()
+ .atLeast(InputHatch)
+ .casingIndex(getTextureIndex(GregTech_API.sBlockCasings9, 9))
+ .dot(2)
+ .buildAndChain(ofBlock(GregTech_API.sBlockCasings9, 9))))
+ .build();
+
+ public GT_MetaTileEntity_PurificationUnitOzonation(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ protected GT_MetaTileEntity_PurificationUnitOzonation(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitOzonation(this.mName);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity aBaseMetaTileEntity, ForgeDirection side, ForgeDirection aFacing,
+ int colorIndex, boolean aActive, boolean redstoneLevel) {
+ if (side == aFacing) {
+ if (aActive) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(MAIN_CASING_INDEX) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(STRUCTURE_PIECE_MAIN, stackSize, hintsOnly, OFFSET_X, OFFSET_Y, OFFSET_Z);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ OFFSET_X,
+ OFFSET_Y,
+ OFFSET_Z,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitOzonation> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ final GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Ozonation Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "Will explode if the input hatch contains more than " + EnumChatFormatting.RED
+ + MAX_OZONE_GAS_FOR_EXPLOSION
+ + "L "
+ + EnumChatFormatting.WHITE
+ + "Ozone Gas.")
+ .addInfo(
+ "Receives a " + EnumChatFormatting.RED
+ + "20%"
+ + EnumChatFormatting.GRAY
+ + " bonus to success chance for every doubling of "
+ + EnumChatFormatting.WHITE
+ + "Ozone Gas.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The second step in water purification is ozonation, which involves injecting large quantities of small")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "bubbles of highly reactive ozone gas into the water. This removes trace element contaminants like")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "sulfur, iron and manganese, creating insoluble oxide compounds which are then filtered out.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(9, 10, 5, false)
+ .addCasingInfoRangeColored(
+ "Inert Filtration Casing",
+ EnumChatFormatting.GRAY,
+ MIN_CASING,
+ 102,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Reactive Gas Containment Casing",
+ EnumChatFormatting.GRAY,
+ 27,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Any Tinted Industrial Glass",
+ EnumChatFormatting.GRAY,
+ 9,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Tungstensteel Frame Box",
+ EnumChatFormatting.GRAY,
+ 6,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored("PTFE Pipe Casing", EnumChatFormatting.GRAY, 3, EnumChatFormatting.GOLD, false)
+ .addOutputBus(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addInputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addOutputHatch(EnumChatFormatting.GOLD + "1" + EnumChatFormatting.GRAY + "+", 1)
+ .addOtherStructurePart("Input Hatch (Ozone)", EnumChatFormatting.GOLD + "1", 2)
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationOzonationRecipes;
+ }
+
+ @NotNull
+ @Override
+ public CheckRecipeResult checkProcessing() {
+ // First do recipe checking logic
+ CheckRecipeResult result = super.checkProcessing();
+ if (!result.wasSuccessful()) return result;
+ // Look for ozone, blow up if more than max allowed
+ for (FluidStack fluid : this.storedFluids) {
+ if (fluid.isFluidEqual(Materials.Ozone.getGas(1L))) {
+ if (fluid.amount > MAX_OZONE_GAS_FOR_EXPLOSION) {
+ // TODO: Fix crash in hatch
+ // this.explodeMultiblock();
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.GT_MACHINES_OZONATION_LOOP.resourceLocation;
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 2;
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ casingCount = 0;
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, OFFSET_X, OFFSET_Y, OFFSET_Z)) return false;
+ if (casingCount < MIN_CASING) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_LuV;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitParticleExtractor.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitParticleExtractor.java
new file mode 100644
index 0000000000..54c5ad9c09
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitParticleExtractor.java
@@ -0,0 +1,484 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.GT_HatchElement.InputBus;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputBus;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_PROCESSING_ARRAY_GLOW;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.ThreadLocalRandom;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.ItemList;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.common.items.GT_MetaGenerated_Item_03;
+import gregtech.common.items.ID_MetaItem_03;
+
+public class GT_MetaTileEntity_PurificationUnitParticleExtractor
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitParticleExtractor>
+ implements ISurvivalConstructable {
+
+ public static long BARYONIC_MATTER_OUTPUT = 2000L;
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final int STRUCTURE_X_OFFSET = 8;
+ private static final int STRUCTURE_Y_OFFSET = 8;
+ private static final int STRUCTURE_Z_OFFSET = 0;
+
+ static final String[][] structure = new String[][] {
+ // spotless:off
+ { " ", " ", " ", " ", " ", " ", " AAAAA ", " AAAAA ", " AA~AA ", " AAAAA ", " AAAAA ", " ", " ", " ", " ", " ", " " },
+ { " ", " E ", " E ", " E ", " E ", " E ", " AAAAA ", " AAAAA ", " EEEEEAAAAAEEEEE ", " AAAAA ", " AAAAA ", " E ", " E ", " E ", " E ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " CCCCC ", " CDDDC ", " E CDBDC E ", " CDDDC ", " CCCCC ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " "," E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " ", " E ", " " },
+ { " AAAAA ", " AAAAA ", " CCCCC ", " ", " ", " ", "AAC AAAAA CAA", "AAC ADDDA CAA", "AAC ADBDA CAA", "AAC ADDDA CAA", "AAC AAAAA CAA", " ", " ", " ", " CCCCC ", " AAAAA ", " AAAAA " },
+ { " AAAAA ", " AAAAA ", " CDDDC ", " DDD ", " DDD ", " DDD ", "AAC ADDDA CAA", "AADDDDD DDDDDAA", "AADDDDD B DDDDDAA", "AADDDDD DDDDDAA", "AAC ADDDA CAA", " DDD ", " DDD ", " DDD ", " CDDDC ", " AAAAA ", " AAAAA " },
+ { " AAAAA ", " EEEEEAAAAAEEEEE ", " E CDBDC E ", " E DBD E ", " E DBD E ", " E DBD E ", "AAC ADBDA CAA", "AADDDDD B DDDDDAA", "AABBBBBBBBBBBBBAA", "AADDDDD B DDDDDAA", "AAC ADBDA CAA", " E DBD E ", " E DBD E ", " E DBD E ", " E CDBDC E ", " EEEEEAAAAAEEEEE ", " AAAAA " },
+ { " AAAAA ", " AAAAA ", " CDDDC ", " DDD ", " DDD ", " DDD ", "AAC ADDDA CAA", "AADDDDD DDDDDAA", "AADDDDD B DDDDDAA", "AADDDDD DDDDDAA", "AAC ADDDA CAA", " DDD ", " DDD ", " DDD ", " CDDDC ", " AAAAA ", " AAAAA " },
+ { " AAAAA ", " AAAAA ", " CCCCC ", " ", " ", " ", "AAC AAAAA CAA", "AAC ADDDA CAA", "AAC ADBDA CAA", "AAC ADDDA CAA", "AAC AAAAA CAA", " ", " ", " ", " CCCCC ", " AAAAA ", " AAAAA " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " ", " DDD ", " E DBD E ", " DDD ", " ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " ", " ", " ", " ", " CCCCC ", " CDDDC ", " E CDBDC E ", " CDDDC ", " CCCCC ", " ", " ", " ", " ", " E ", " " },
+ { " ", " E ", " E ", " E ", " E ", " E ", " AAAAA ", " AAAAA ", " EEEEEAAAAAEEEEE ", " AAAAA ", " AAAAA ", " E ", " E ", " E ", " E ", " E ", " " },
+ { " ", " ", " ", " ", " ", " ", " AAAAA ", " AAAAA ", " AAAAA ", " AAAAA ", " AAAAA ", " ", " ", " ", " ", " ", " " } };
+ // spotless:on
+
+ // Dimensionally transcendent casing (placeholder)
+ private static final int CASING_INDEX_MAIN = getTextureIndex(GregTech_API.sBlockCasings10, 2);
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitParticleExtractor> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitParticleExtractor>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ // Quark Exclusion Casing
+ .addElement(
+ 'A',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitParticleExtractor>buildHatchAdder()
+ .atLeastList(Arrays.asList(InputBus, OutputBus, InputHatch, OutputHatch))
+ .dot(1)
+ .casingIndex(CASING_INDEX_MAIN)
+ .build()),
+ ofBlock(GregTech_API.sBlockCasings10, 2)))
+ // Particle Beam Guidance Pipe Casing
+ .addElement('B', ofBlock(GregTech_API.sBlockCasings9, 14))
+ // Femtometer-Calibrated Particle Beam Casing
+ .addElement('C', ofBlock(GregTech_API.sBlockCasings9, 15))
+ // Non-Photonic Matter Exclusion Glass
+ .addElement('D', ofBlock(GregTech_API.sBlockGlass1, 3))
+ .addElement('E', ofFrame(Materials.Bedrockium))
+ .build();
+
+ private static class CatalystCombination {
+
+ public ItemStack firstCatalyst;
+ public ItemStack secondCatalyst;
+
+ public static ItemList[] CATALYST_ITEMS = new ItemList[] { ItemList.Quark_Creation_Catalyst_Up,
+ ItemList.Quark_Creation_Catalyst_Down, ItemList.Quark_Creation_Catalyst_Bottom,
+ ItemList.Quark_Creation_Catalyst_Top, ItemList.Quark_Creation_Catalyst_Strange,
+ ItemList.Quark_Creation_Catalyst_Charm };
+
+ public CatalystCombination(ItemStack first, ItemStack second) {
+ firstCatalyst = first;
+ secondCatalyst = second;
+ }
+
+ public boolean matches(ItemStack a, ItemStack b) {
+ return (a.isItemEqual(firstCatalyst) && b.isItemEqual(secondCatalyst))
+ || (b.isItemEqual(firstCatalyst) && a.isItemEqual(secondCatalyst));
+ }
+
+ public NBTTagCompound saveToNBT() {
+ NBTTagCompound nbt = new NBTTagCompound();
+ NBTTagCompound first = new NBTTagCompound();
+ NBTTagCompound second = new NBTTagCompound();
+ firstCatalyst.writeToNBT(first);
+ secondCatalyst.writeToNBT(second);
+ nbt.setTag("first", first);
+ nbt.setTag("second", second);
+ return nbt;
+ }
+
+ public static CatalystCombination readFromNBT(NBTTagCompound nbt) {
+ NBTTagCompound first = nbt.getCompoundTag("first");
+ NBTTagCompound second = nbt.getCompoundTag("second");
+ return new CatalystCombination(
+ ItemStack.loadItemStackFromNBT(first),
+ ItemStack.loadItemStackFromNBT(second));
+ }
+ }
+
+ private static CatalystCombination generateNewCombination() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ // Generate two unique indices into the list
+ int firstIndex = random.nextInt(0, CatalystCombination.CATALYST_ITEMS.length);
+ int secondIndex = random.nextInt(0, CatalystCombination.CATALYST_ITEMS.length);
+ while (secondIndex == firstIndex) {
+ secondIndex = random.nextInt(0, CatalystCombination.CATALYST_ITEMS.length);
+ }
+
+ return new CatalystCombination(
+ CatalystCombination.CATALYST_ITEMS[firstIndex].get(1),
+ CatalystCombination.CATALYST_ITEMS[secondIndex].get(1));
+ }
+
+ private CatalystCombination currentCombination = null;
+
+ private ArrayList<ItemStack> insertedCatalysts = new ArrayList<>();
+
+ private int correctIndexA = -1, correctIndexB = -1;
+
+ public GT_MetaTileEntity_PurificationUnitParticleExtractor(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitParticleExtractor(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitParticleExtractor(mName);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection side, ForgeDirection facing,
+ int colorIndex, boolean active, boolean redstoneLevel) {
+ if (side == facing) {
+ if (active) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_PROCESSING_ARRAY_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitParticleExtractor> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Absolute Baryonic Perfection Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "Insert " + EnumChatFormatting.WHITE
+ + "Quark Releasing Catalysts "
+ + EnumChatFormatting.GRAY
+ + "into the input bus while running.")
+ .addInfo(
+ "Every recipe cycle, a different combination of " + EnumChatFormatting.RED
+ + "2"
+ + EnumChatFormatting.GRAY
+ + " different "
+ + EnumChatFormatting.WHITE
+ + "Quark Releasing Catalysts")
+ .addInfo("will correctly identify the lone quark and succeed the recipe.")
+ .addSeparator()
+ .addInfo(
+ "Every " + EnumChatFormatting.RED
+ + "20"
+ + EnumChatFormatting.GRAY
+ + " ticks, consumes ALL catalysts in the input bus.")
+ .addInfo(
+ "If the two most recently inserted catalysts were the correct combination, immediately outputs "
+ + EnumChatFormatting.WHITE
+ + "Stabilised Baryonic Matter")
+ .addInfo("At the end of the recipe, all incorrectly inserted catalysts are returned in the output bus.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The final stage of water purification goes beyond subatomic particles and identifies the smallest")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "possible imperfections within the baryons themselves. By correctly identifying which pairs of quark")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "flavors are required, the unit will activate the catalysts, stabilizing the errant particles.")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "This ultimately creates both Stabilised Baryonic Matter and, most importantly, absolutely perfectly purified water.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(17, 17, 17, false)
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationParticleExtractionRecipes;
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ this.insertedCatalysts.clear();
+ this.currentCombination = generateNewCombination();
+ correctIndexA = -1;
+ correctIndexB = -1;
+ }
+
+ private boolean isCatalyst(ItemStack stack) {
+ if (stack.getItem() instanceof GT_MetaGenerated_Item_03) {
+ int meta = stack.getItemDamage() - 32000; // why, greg.
+ return meta >= ID_MetaItem_03.Quark_Creation_Catalyst_Up.ID
+ && meta <= ID_MetaItem_03.Quark_Creation_Catalyst_Top.ID;
+ }
+ return false;
+ }
+
+ @Override
+ public void endCycle() {
+ super.endCycle();
+ // Output incorrect indices unchanged, the spent ones will follow if recipe was successful from the actual
+ // recipe outputs
+ for (int i = 0; i < insertedCatalysts.size(); ++i) {
+ if (i == correctIndexA || i == correctIndexB) continue;
+
+ addOutput(insertedCatalysts.get(i));
+ }
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ // Only succeed if correct combination was inserted
+ if (correctIndexA >= 0) return 100.0f;
+ else return 0.0f;
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+ // Every 20 ticks, add all catalysts from the input bus to the internal inventory.
+ if (mMaxProgresstime > 0 && aTick % 20 == 0) {
+ ArrayList<ItemStack> storedInputs = getStoredInputs();
+ // For each stack in the input, check if it is a valid catalyst item and if so consume it
+ for (ItemStack stack : storedInputs) {
+ if (isCatalyst(stack)) {
+ this.insertedCatalysts.add(stack.copy());
+ this.depleteInput(stack);
+ }
+ }
+
+ // Only do this check if we didn't find a correct combination yet
+ if (correctIndexA >= 0) return;
+
+ // After draining all catalyst inputs, find the 2 most recently inserted items
+ if (insertedCatalysts.isEmpty()) return;
+ int firstIndex = insertedCatalysts.size() - 1;
+ ItemStack first = insertedCatalysts.get(firstIndex);
+ // Since correct combinations are always different catalysts, we can require that there is a second item in
+ // the history
+ if (first.stackSize == 1 && insertedCatalysts.size() >= 2) {
+ int secondIndex = insertedCatalysts.size() - 2;
+ ItemStack second = insertedCatalysts.get(secondIndex);
+ // Now check if this combination matches the current correct one.
+ if (currentCombination.matches(first, second)) {
+ // It does, we can save that the indices are correct. This means we need to
+ // - stop checking if any future insertions are the correct combination (but still consume them)
+ // - output baryonic matter
+ correctIndexA = firstIndex;
+ correctIndexB = secondIndex;
+ addOutput(Materials.StableBaryonicMatter.getFluid(BARYONIC_MATTER_OUTPUT));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ if (this.currentCombination != null) {
+ aNBT.setTag("currentCombination", this.currentCombination.saveToNBT());
+ }
+ NBTTagCompound insertedNBT = new NBTTagCompound();
+ for (int i = 0; i < insertedCatalysts.size(); ++i) {
+ ItemStack inserted = insertedCatalysts.get(i);
+ NBTTagCompound itemNBT = new NBTTagCompound();
+ inserted.writeToNBT(itemNBT);
+ insertedNBT.setTag(Integer.toString(i), itemNBT);
+ }
+ aNBT.setTag("insertedItems", insertedNBT);
+ aNBT.setInteger("correctIndexA", correctIndexA);
+ aNBT.setInteger("correctIndexB", correctIndexB);
+ super.saveNBTData(aNBT);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ if (aNBT.hasKey("currentCombination")) {
+ currentCombination = CatalystCombination.readFromNBT(aNBT.getCompoundTag("currentCombination"));
+ }
+ if (aNBT.hasKey("insertedItems")) {
+ NBTTagCompound insertedList = aNBT.getCompoundTag("insertedItems");
+ // Initialize empty list with correct size
+ this.insertedCatalysts = new ArrayList<>(
+ Collections.nCopies(
+ insertedList.func_150296_c()
+ .size(),
+ null));
+ for (String key : insertedList.func_150296_c()) {
+ NBTTagCompound itemCompound = insertedList.getCompoundTag(key);
+ int index = Integer.parseInt(key);
+ this.insertedCatalysts.set(index, ItemStack.loadItemStackFromNBT(itemCompound));
+ }
+ }
+ if (aNBT.hasKey("correctIndexA")) {
+ correctIndexA = aNBT.getInteger("correctIndexA");
+ }
+ if (aNBT.hasKey("correctIndexB")) {
+ correctIndexB = aNBT.getInteger("correctIndexB");
+ }
+ super.loadNBTData(aNBT);
+ }
+
+ private String getCorrectlyDecodedString() {
+ if (correctIndexA >= 0) {
+ return EnumChatFormatting.GREEN + "Yes";
+ }
+ return EnumChatFormatting.RED + "No";
+ }
+
+ public EnumChatFormatting getQuarkColor(ItemStack stack) {
+ int meta = stack.getItemDamage() - 32000;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Up.ID) return EnumChatFormatting.RED;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Down.ID) return EnumChatFormatting.YELLOW;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Strange.ID) return EnumChatFormatting.DARK_PURPLE;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Charm.ID) return EnumChatFormatting.LIGHT_PURPLE;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Bottom.ID) return EnumChatFormatting.GREEN;
+ if (meta == ID_MetaItem_03.Quark_Creation_Catalyst_Top.ID) return EnumChatFormatting.BLUE;
+ return EnumChatFormatting.GRAY;
+ }
+
+ public String[] getInfoData() {
+ ArrayList<String> info = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ info.add("Catalyst insertion history for this recipe cycle (most recent first): ");
+ for (int i = insertedCatalysts.size() - 1; i >= 0; --i) {
+ ItemStack stack = insertedCatalysts.get(i);
+ String name = stack.getDisplayName();
+ String[] split = name.split("-");
+ info.add(
+ EnumChatFormatting.YELLOW + ""
+ + (i + 1)
+ + ": "
+ + getQuarkColor(stack)
+ + split[0]
+ + EnumChatFormatting.GRAY
+ + "-"
+ + split[1]);
+ }
+ info.add("Quark Combination correctly identified: " + getCorrectlyDecodedString());
+ return info.toArray(new String[] {});
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 8;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_UEV;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPhAdjustment.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPhAdjustment.java
new file mode 100644
index 0000000000..5c65e105dd
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPhAdjustment.java
@@ -0,0 +1,609 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.GT_HatchElement.InputBus;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Mods.GoodGenerator;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.util.GT_RecipeBuilder.SECONDS;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidRegistry;
+import net.minecraftforge.fluids.FluidStack;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_Input;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_InputBus;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.IGT_HatchAdder;
+
+public class GT_MetaTileEntity_PurificationUnitPhAdjustment
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitPhAdjustment>
+ implements ISurvivalConstructable {
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final int STRUCTURE_X_OFFSET = 7;
+ private static final int STRUCTURE_Y_OFFSET = 4;
+ private static final int STRUCTURE_Z_OFFSET = 1;
+
+ private static final String[][] structure = new String[][] {
+ // spotless:off
+ { "E E E E", "EAAAE EAAAE", "EAGAE EAHAE", "EAGAE EAHAE", "EAGAE EAHAE", "EAAAE EAAAE" },
+ { " AAA AAA ", "A A A A", "A A A A", "A A A A", "A ABB~BBA A", "AAAAA AAAAA" },
+ { " AXA AYA ", "A A A A", "G A A H", "G ABBBBBA H", "G H", "AAAAABRBRBAAAAA" },
+ { " AAA AAA ", "A A A A", "A A A A", "A A A A", "A AIIIIIA A", "AAAAA AAAAA" },
+ { "E E E E", "EAAAE EAAAE", "EAGAE EAHAE", "EAGAE EAHAE", "EAGAE EAHAE", "EAAAE EAAAE" } };
+ // spotless:on
+
+ private static final int CASING_INDEX_MIDDLE = getTextureIndex(GregTech_API.sBlockCasings9, 7);
+ private static final int CASING_INDEX_TOWER = getTextureIndex(GregTech_API.sBlockCasings9, 8);
+
+ /**
+ * The current pH value of the water inside the multiblock
+ */
+ private float currentpHValue = 0.0f;
+
+ /**
+ * The multiblock will try to consume catalyst every CONSUME_INTERVAL ticks.
+ */
+ private static final int CONSUME_INTERVAL = 1 * SECONDS;
+
+ /**
+ * Maximum deviation the initial pH value can have away from the neutral value.
+ */
+ private static final float INITIAL_PH_DEVIATION = 2.5f;
+
+ /**
+ * pH value of entirely pH neutral water.
+ */
+ private static final float PH_NEUTRAL_VALUE = 7.0f;
+
+ /**
+ * Maximum deviation from the neutral value that is allowed for the recipe to succeed.
+ */
+ private static final float PH_MAX_DEVIATION = 0.05f;
+
+ /**
+ * Change in pH value for each piece of alkaline dust supplied.
+ */
+ public static final float PH_PER_ALKALINE_DUST = 0.01f;
+
+ /**
+ * Change in pH value for every 10L of acid supplied.
+ */
+ public static final float PH_PER_10_ACID_LITER = -0.01f;
+
+ /**
+ * Alkaline catalyst material
+ */
+ public static final Materials ALKALINE_MATERIAL = Materials.SodiumHydroxide;
+
+ /**
+ * Acidic catalyst material
+ */
+ public static final Materials ACIDIC_MATERIAL = Materials.HydrochloricAcid;
+
+ /**
+ * The input hatch for the acidic material
+ */
+ private GT_MetaTileEntity_Hatch_Input acidInputHatch;
+ /**
+ * The input bus for the alkaline material
+ */
+ private GT_MetaTileEntity_Hatch_InputBus alkalineInputBus;
+
+ /**
+ * List of all placed sensor hatches in the multi, so we can update them with the proper pH value when it changes.
+ */
+ private final ArrayList<GT_MetaTileEntity_pHSensor> sensorHatches = new ArrayList<>();
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitPhAdjustment> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitPhAdjustment>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ // Extreme Corrosion Resistant Casing
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings9, 8))
+ // Naquadah Reinforced Water Plant Casing
+ .addElement('B', ofBlock(GregTech_API.sBlockCasings9, 7))
+ .addElement('E', ofFrame(Materials.NaquadahAlloy))
+ // pH Resistant Glass
+ .addElement('G', ofBlock(GregTech_API.sBlockGlass1, 0))
+ .addElement('H', ofBlock(GregTech_API.sBlockGlass1, 0))
+ // Regular I/O hatches
+ .addElement(
+ 'I',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPhAdjustment>buildHatchAdder()
+ .atLeastList(t.getAllowedHatches())
+ .dot(1)
+ .casingIndex(CASING_INDEX_MIDDLE)
+ .build()),
+ // Naquadah Reinforced Water Plant Casing
+ ofBlock(GregTech_API.sBlockCasings9, 7)))
+ .addElement(
+ 'R',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPhAdjustment>buildHatchAdder()
+ .atLeast(SpecialHatchElement.PhSensor)
+ .dot(2)
+ .cacheHint(() -> "pH Sensor Hatch")
+ .casingIndex(CASING_INDEX_MIDDLE)
+ .build()),
+ // Naquadah Reinforced Water Plant Casing
+ ofBlock(GregTech_API.sBlockCasings9, 7)))
+ // Special I/O hatches
+ .addElement(
+ 'X',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPhAdjustment>buildHatchAdder()
+ .atLeast(InputBus)
+ .dot(3)
+ .adder(GT_MetaTileEntity_PurificationUnitPhAdjustment::addAlkalineBusToMachineList)
+ .cacheHint(() -> "Input Bus (" + ALKALINE_MATERIAL.mLocalizedName + ")")
+ .casingIndex(CASING_INDEX_TOWER)
+ .allowOnly(ForgeDirection.UP)
+ .build()))
+ .addElement(
+ 'Y',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPhAdjustment>buildHatchAdder()
+ .atLeast(InputHatch)
+ .dot(4)
+ .adder(GT_MetaTileEntity_PurificationUnitPhAdjustment::addAcidHatchToMachineList)
+ .cacheHint(() -> "Input Hatch (" + ACIDIC_MATERIAL.mLocalizedName + ")")
+ .casingIndex(CASING_INDEX_TOWER)
+ .allowOnly(ForgeDirection.UP)
+ .build()))
+ .build();
+
+ private List<IHatchElement<? super GT_MetaTileEntity_PurificationUnitPhAdjustment>> getAllowedHatches() {
+ return ImmutableList.of(InputHatch, OutputHatch);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity aBaseMetaTileEntity, ForgeDirection side, ForgeDirection aFacing,
+ int colorIndex, boolean aActive, boolean redstoneLevel) {
+ if (side == aFacing) {
+ if (aActive) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MIDDLE),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MIDDLE),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MIDDLE) };
+ }
+
+ public GT_MetaTileEntity_PurificationUnitPhAdjustment(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitPhAdjustment(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitPhAdjustment(this.mName);
+ }
+
+ @Override
+ protected void setHatchRecipeMap(GT_MetaTileEntity_Hatch_Input hatch) {
+ // Do nothing, we don't want to lock hatches to recipe maps since this can cause
+ // them to reject our catalyst fluids
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitPhAdjustment> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationPhAdjustmentRecipes;
+ }
+
+ public boolean addAcidHatchToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity == null) return false;
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_Hatch_Input) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ ((GT_MetaTileEntity_Hatch_Input) aMetaTileEntity).mRecipeMap = null;
+ acidInputHatch = (GT_MetaTileEntity_Hatch_Input) aMetaTileEntity;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean addAlkalineBusToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity == null) return false;
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_Hatch_InputBus) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ ((GT_MetaTileEntity_Hatch_InputBus) aMetaTileEntity).mRecipeMap = null;
+ alkalineInputBus = (GT_MetaTileEntity_Hatch_InputBus) aMetaTileEntity;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean addSensorHatchToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_pHSensor) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ return this.sensorHatches.add((GT_MetaTileEntity_pHSensor) aMetaTileEntity);
+ }
+ return false;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ final GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the pH Neutralization Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "Initial pH value every cycle varies from " + EnumChatFormatting.RED
+ + (PH_NEUTRAL_VALUE - INITIAL_PH_DEVIATION)
+ + EnumChatFormatting.GRAY
+ + " - "
+ + EnumChatFormatting.RED
+ + (PH_NEUTRAL_VALUE + INITIAL_PH_DEVIATION)
+ + " pH"
+ + EnumChatFormatting.GRAY
+ + ".")
+ .addInfo(
+ "If the pH value is within " + EnumChatFormatting.RED
+ + PH_MAX_DEVIATION
+ + " pH "
+ + EnumChatFormatting.GRAY
+ + "of 7.0 pH at the end of the cycle, the recipe always succeeds.")
+ .addInfo("Otherwise, the recipe always fails.")
+ .addInfo("Use a pH Sensor Hatch to read the current pH value.")
+ .addSeparator()
+ .addInfo(
+ "Every " + EnumChatFormatting.RED
+ + CONSUME_INTERVAL
+ + EnumChatFormatting.GRAY
+ + " ticks, consumes ALL "
+ + EnumChatFormatting.WHITE
+ + ALKALINE_MATERIAL.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " and "
+ + EnumChatFormatting.WHITE
+ + ACIDIC_MATERIAL.mLocalizedName
+ + EnumChatFormatting.GRAY
+ + " in the special hatches.")
+ .addInfo(
+ EnumChatFormatting.RED + "Raises "
+ + EnumChatFormatting.GRAY
+ + "the pH value by "
+ + EnumChatFormatting.RED
+ + PH_PER_ALKALINE_DUST
+ + " pH "
+ + EnumChatFormatting.GRAY
+ + "per piece of "
+ + EnumChatFormatting.WHITE
+ + ALKALINE_MATERIAL.getDust(1)
+ .getDisplayName()
+ + EnumChatFormatting.GRAY
+ + ".")
+ .addInfo(
+ EnumChatFormatting.RED + "Lowers "
+ + EnumChatFormatting.GRAY
+ + "the pH value by "
+ + EnumChatFormatting.RED
+ + -PH_PER_10_ACID_LITER
+ + " pH "
+ + EnumChatFormatting.GRAY
+ + "per "
+ + EnumChatFormatting.RED
+ + "10L "
+ + EnumChatFormatting.GRAY
+ + "of "
+ + EnumChatFormatting.WHITE
+ + ACIDIC_MATERIAL.getFluid(1L)
+ .getLocalizedName()
+ + EnumChatFormatting.GRAY
+ + ".")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The fourth step of water purification is to neutralize the solution and bring its pH to exactly 7, rendering")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "the solution inert with no hydrogen ion activity beyond water’s natural amphiproticity. Acids and bases from soils")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "and geology cause natural alkalinity variations in water which can cause corrosive reactions with sensitive")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "materials. This necessitates the use of the corresponding neutralizing agents to pH balance the water.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(7, 4, 7, false)
+ .addCasingInfoExactlyColored(
+ "Stabilized Naquadah Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 15,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Chemical Grade Glass",
+ EnumChatFormatting.GRAY,
+ 18,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Naquadah Alloy Frame Box",
+ EnumChatFormatting.GRAY,
+ 48,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Inert Neutralization Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 67 * 2,
+ EnumChatFormatting.GOLD,
+ false)
+ .addController("Front center")
+ .addOtherStructurePart("Input Hatch (Water)", EnumChatFormatting.GOLD + "1+", 1)
+ .addOtherStructurePart("Output Hatch", EnumChatFormatting.GOLD + "1", 1)
+ .addOtherStructurePart("pH Sensor Hatch", EnumChatFormatting.GOLD + "2", 2)
+ .addOtherStructurePart("Input Bus (Sodium Hydroxide)", EnumChatFormatting.GOLD + "1", 3)
+ .addOtherStructurePart("Input Hatch (Hydrochloric Acid)", EnumChatFormatting.GOLD + "1", 4)
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ // Randomize initial pH value
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ // Generate random integer in [-RNG_PRECISION, RNG_PRECISION]
+ final int RNG_PRECISION = 1000;
+ int rng = random.nextInt(-RNG_PRECISION, RNG_PRECISION);
+ // Remap to [-1.0, 1.0] and then to [-INITIAL_PH_DEVIATION, INITIAL_PH_DEVIATION]
+ float deviation = ((float) rng / RNG_PRECISION) * INITIAL_PH_DEVIATION;
+ // Round to 2 digits
+ this.currentpHValue = Math.round((PH_NEUTRAL_VALUE + deviation) * 100.0f) / 100.0f;
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+ // Eat all acid and alkaline material every second
+ if (mMaxProgresstime > 0 && aTick % CONSUME_INTERVAL == 0) {
+ // Important that we drain backwards, since draining stacks can auto-sort the bus
+ long totalAlkalineDrained = 0;
+ for (int i = alkalineInputBus.getSizeInventory() - 1; i >= 0; --i) {
+ ItemStack stack = alkalineInputBus.getStackInSlot(i);
+ // If this ItemStack is the alkaline material, drain it entirely and record the amount drained
+ if (stack != null && stack.isItemEqual(ALKALINE_MATERIAL.getDust(1))) {
+ totalAlkalineDrained += stack.stackSize;
+ alkalineInputBus.decrStackSize(i, stack.stackSize);
+ }
+ }
+
+ // Now do fluid, this is simpler since we only need to bother with one slot
+ FluidStack stack = acidInputHatch.getDrainableStack();
+ int numMultiples = 0;
+ if (stack != null && stack.isFluidEqual(ACIDIC_MATERIAL.getFluid(1))) {
+ int acidAvailable = stack.amount;
+ // We only care about multiples of 10, but we still drain all.
+ numMultiples = Math.floorDiv(acidAvailable, 10);
+ acidInputHatch.drain(acidAvailable, true);
+ } else {
+ // Little easier egg: Fluoroantimonic acid has a pH value of -31, it's an acid so strong it will
+ // instantly shatter the glass in the structure.
+ if (GoodGenerator.isModLoaded()) {
+ Fluid acid = FluidRegistry.getFluid("fluoroantimonic acid");
+ if (stack != null && stack.getFluid()
+ .equals(acid)) {
+ // TODO: Actually break the glass and trigger achievement lol
+ }
+ }
+ }
+
+ // Adjust pH with to new value
+ this.currentpHValue = this.currentpHValue + totalAlkalineDrained * PH_PER_ALKALINE_DUST
+ + numMultiples * PH_PER_10_ACID_LITER;
+
+ // Clamp pH to sensible values
+ this.currentpHValue = Math.min(Math.max(this.currentpHValue, 0.0f), 14.0f);
+
+ // Round to 2 decimals
+ this.currentpHValue = Math.round(this.currentpHValue * 100.0f) / 100.0f;
+ }
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTimer) {
+ super.onPostTick(aBaseMetaTileEntity, aTimer);
+ // Update sensor hatch
+ for (GT_MetaTileEntity_pHSensor hatch : sensorHatches) {
+ hatch.updateRedstoneOutput(this.currentpHValue);
+ }
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ // Success chance is 100% when inside target range, 0% otherwise
+ float distance = Math.abs(this.currentpHValue - PH_NEUTRAL_VALUE);
+ if (distance <= PH_MAX_DEVIATION) {
+ return 100.0f;
+ } else {
+ return 0.0f;
+ }
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 4;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_ZPM;
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ public String[] getInfoData() {
+ ArrayList<String> infoData = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ infoData.add("Current pH Value: " + EnumChatFormatting.YELLOW + currentpHValue + " pH");
+ return infoData.toArray(new String[] {});
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ aNBT.setFloat("mCurrentpH", this.currentpHValue);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ this.currentpHValue = aNBT.getFloat("mCurrentpH");
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.GT_MACHINES_PURIFICATION_PH_LOOP.resourceLocation;
+ }
+
+ private enum SpecialHatchElement implements IHatchElement<GT_MetaTileEntity_PurificationUnitPhAdjustment> {
+
+ PhSensor(GT_MetaTileEntity_PurificationUnitPhAdjustment::addSensorHatchToMachineList,
+ GT_MetaTileEntity_pHSensor.class) {
+
+ @Override
+ public long count(
+ GT_MetaTileEntity_PurificationUnitPhAdjustment gtMetaTileEntityPurificationUnitPhAdjustment) {
+ return gtMetaTileEntityPurificationUnitPhAdjustment.sensorHatches.size();
+ }
+ };
+
+ private final List<Class<? extends IMetaTileEntity>> mteClasses;
+ private final IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitPhAdjustment> adder;
+
+ @SafeVarargs
+ SpecialHatchElement(IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitPhAdjustment> adder,
+ Class<? extends IMetaTileEntity>... mteClasses) {
+ this.mteClasses = Collections.unmodifiableList(Arrays.asList(mteClasses));
+ this.adder = adder;
+ }
+
+ @Override
+ public List<? extends Class<? extends IMetaTileEntity>> mteClasses() {
+ return mteClasses;
+ }
+
+ public IGT_HatchAdder<? super GT_MetaTileEntity_PurificationUnitPhAdjustment> adder() {
+ return adder;
+ }
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPlasmaHeater.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPlasmaHeater.java
new file mode 100644
index 0000000000..3fdd408ec1
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitPlasmaHeater.java
@@ -0,0 +1,570 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlockAnyMeta;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.onElementPass;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.recipe.RecipeMaps.purificationPlasmaHeatingRecipes;
+import static gregtech.api.util.GT_RecipeBuilder.SECONDS;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.minecraft.block.Block;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.FluidStack;
+
+import com.google.common.collect.ImmutableList;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import cpw.mods.fml.common.registry.GameRegistry;
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.Mods;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_Input;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_ModHandler;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+
+public class GT_MetaTileEntity_PurificationUnitPlasmaHeater
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitPlasmaHeater>
+ implements ISurvivalConstructable {
+
+ private static final int CASING_INDEX_HEATER = getTextureIndex(GregTech_API.sBlockCasings9, 11);
+ private static final int CASING_INDEX_TOWER = getTextureIndex(GregTech_API.sBlockCasings9, 5);
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final int STRUCTURE_X_OFFSET = 2;
+ private static final int STRUCTURE_Y_OFFSET = 14;
+ private static final int STRUCTURE_Z_OFFSET = 5;
+
+ /**
+ * Fluid is consumed every CONSUME_INTERVAL ticks
+ */
+ private static final long CONSUME_INTERVAL = 1 * SECONDS;
+
+ /**
+ * Current internal temperature of the multiblock
+ */
+ private long currentTemperature = 0;
+ /**
+ * Amount of successful heating cycles completed
+ */
+ private int cyclesCompleted = 0;
+ /**
+ * Whether this recipe is ruined due to high temperature
+ */
+ private boolean ruinedCycle = false;
+
+ private enum CycleState {
+ // Was previously at 0K, currently waiting to heat to 10000K
+ Heating,
+ // Was previously at 10000K, currently waiting to cool down to 0K
+ Cooling
+ }
+
+ private CycleState state = CycleState.Heating;
+
+ // A cycle is 30s at shortest, a purification plant cycle is 120s. 33% chance per heating cycle
+ // will give you plenty of room for delay and still get to 99% chance.
+ public static final long SUCCESS_PER_CYCLE = 33;
+
+ // Consumption rates in liters/second
+ public static final long MAX_PLASMA_PER_SEC = 10;
+ public static final long MAX_COOLANT_PER_SEC = 100;
+ // Change in temperature per consumed liter of plasma
+ public static final long PLASMA_TEMP_PER_LITER = 100;
+ // Change in temperature per consumed liter of coolant
+ public static final long COOLANT_TEMP_PER_LITER = -5;
+ // Temperature at which the batch is ruined
+ public static final long MAX_TEMP = 12500;
+ // Point at which the heating point of the cycle is reached
+ public static final long HEATING_POINT = 10000;
+
+ private static final Materials plasmaMaterial = Materials.Helium;
+ private static final Materials coolantMaterial = Materials.SuperCoolant;
+
+ private GT_MetaTileEntity_Hatch_Input plasmaInputHatch;
+ private GT_MetaTileEntity_Hatch_Input coolantInputHatch;
+
+ private static final String[][] structure = new String[][] {
+ // spotless:off
+ { " DDDDDDD ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " DDDDD ", " DDDDD ", " DDKDD " },
+ { " DD DD ", " DDDDDDD ", " DDDDD ", " ", " ", " ", " ", " ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DD DD ", " DD DD ", " DDDDDDDDD " },
+ { " D D ", " DDD DDD ", " DDDDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " D D ", " D D ", " DDDDDDDDDDD " },
+ { " D D ", " D DD ", " DD D ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " DDDDDDDDDDDDD " },
+ { " D D", " DD DD ", " DD D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " DDDDDDDDDDDDD " },
+ { " D D", " D D ", " DD D ", " D D ", " D D ", "GBBBG D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D", "G G D D", "GB~BG DDDDDDDDDDDDDDD" },
+ { " D D", " D D ", " DD D ", " D D ", " BBB D D ", "BBBBB D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D", " EEEBBBBD D", "BAAAB DDDDDDDDDDDDDDD" },
+ { " D D", " D D ", " DD D ", " D D ", " BBB D D ", "BBBBB D D ", " EFE D D ", " EFE D D ", " EFE D D ", " EFE D D ", " EFE D D ", " EFE D D ", " EFEBBBBD D", " EFE D D", "PAAABBBBDDDDDDDDDDDDDDD" },
+ { " D D", " D D ", " DD D ", " D D ", " BBB D D ", "BBBBB D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D ", " EEE D D", " EEEBBBBD D", "BAAAB DDDDDDDDDDDDDDD" },
+ { " D D", " D D ", " DD D ", " D D ", " D D ", "GBBBG D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D ", "G G D D", "G G D D", "GBBBG DDDDDDDDDDDDDDD" },
+ { " D D", " D D ", " D D ", " D DD ", " D DD ", " D DD ", " D DD ", " D DD ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " DDDDDDDDDDDDD " },
+ { " D DD", " D D ", " D D ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " D D ", " D D ", " D D ", " D D ", " D D ", " D D ", " DDDDDDDDDDDDD " },
+ { " D D ", " DD DD ", " D D ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DD DD ", " DD DD ", " DD DD ", " DD DD ", " D D ", " D D ", " DDDDDDDDDDD " },
+ { " DD DD ", " DDDDDDD ", " DDDDD ", " ", " ", " ", " ", " ", " DDDDD ", " DDDDD ", " DDDDD ", " DDDDD ", " DD DD ", " DD DD ", " DDDDDDDDD " },
+ { " DDDDDDD ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " DDDDD ", " DDDDD ", " DDDDD " } };
+ // spotless:on
+
+ private int casingCount = 0;
+ private static final int MIN_CASING = 50;
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitPlasmaHeater> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitPlasmaHeater>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ // Superconducting coil block
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings1, 15))
+ // Plasma Heating Casing
+ .addElement(
+ 'B',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPlasmaHeater>buildHatchAdder()
+ .atLeastList(t.getAllowedHatches())
+ .dot(1)
+ .casingIndex(CASING_INDEX_HEATER)
+ .build()),
+ onElementPass(t -> t.casingCount++, ofBlock(GregTech_API.sBlockCasings9, 11))))
+ // Reinforced Sterile Water Plant Casing
+ .addElement('D', ofBlock(GregTech_API.sBlockCasings9, 5))
+ // Any Tinted Glass
+ .addElement('E', ofBlockAnyMeta(GregTech_API.sBlockTintedGlass, 0))
+ // Neonite, with fallback to air
+ .addElement('F', lazy(t -> {
+ if (Mods.Chisel.isModLoaded()) {
+ Block neonite = GameRegistry.findBlock(Mods.Chisel.ID, "neonite");
+ return ofBlockAnyMeta(neonite, 7);
+ } else {
+ return ofBlockAnyMeta(Blocks.air);
+ }
+ }))
+ // Superconductor Base ZPM frame box
+ .addElement('G', ofFrame(Materials.Tetranaquadahdiindiumhexaplatiumosminid))
+ // Coolant input hatch
+ .addElement(
+ 'K',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPlasmaHeater>buildHatchAdder()
+ .atLeast(InputHatch)
+ .dot(2)
+ .adder(GT_MetaTileEntity_PurificationUnitPlasmaHeater::addCoolantHatchToMachineList)
+ .cacheHint(() -> "Input Hatch (Coolant)")
+ .casingIndex(CASING_INDEX_TOWER)
+ .buildAndChain(ofBlock(GregTech_API.sBlockCasings9, 5))))
+ // Plasma input hatch
+ .addElement(
+ 'P',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitPlasmaHeater>buildHatchAdder()
+ .atLeast(InputHatch)
+ .dot(3)
+ .adder(GT_MetaTileEntity_PurificationUnitPlasmaHeater::addPlasmaHatchToMachineList)
+ .cacheHint(() -> "Input Hatch (Plasma)")
+ .casingIndex(CASING_INDEX_HEATER)
+ .buildAndChain(ofBlock(GregTech_API.sBlockCasings9, 11))))
+ .build();
+
+ private List<IHatchElement<? super GT_MetaTileEntity_PurificationUnitPlasmaHeater>> getAllowedHatches() {
+ return ImmutableList.of(InputHatch, OutputHatch);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitPlasmaHeater(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitPlasmaHeater(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitPlasmaHeater(this.mName);
+ }
+
+ @Override
+ protected void setHatchRecipeMap(GT_MetaTileEntity_Hatch_Input hatch) {
+ // Do nothing, we don't want to lock hatches to recipe maps since this can cause
+ // them to reject our catalyst fluids
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection side, ForgeDirection facing,
+ int colorIndex, boolean active, boolean redstoneLevel) {
+ if (side == facing) {
+ if (active) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_HEATER),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_HEATER),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_HEATER) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitPlasmaHeater> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return purificationPlasmaHeatingRecipes;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the Extreme Temperature Fluctuation Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "Complete heating cycles by first heating the water to " + EnumChatFormatting.RED
+ + HEATING_POINT
+ + "K"
+ + EnumChatFormatting.GRAY
+ + ",")
+ .addInfo(
+ "and then cooling it back down to " + EnumChatFormatting.RED + "0K" + EnumChatFormatting.GRAY + ".")
+ .addInfo(
+ "Initial temperature is reset to " + EnumChatFormatting.RED
+ + "0K"
+ + EnumChatFormatting.GRAY
+ + " on recipe start.")
+ .addInfo(
+ // TODO: Refer to heating cycles in another way to avoid confusion
+ "Each completed heating cycle boosts success chance by " + EnumChatFormatting.RED
+ + SUCCESS_PER_CYCLE
+ + "%.")
+ .addInfo(
+ "If the temperature ever reaches " + EnumChatFormatting.RED
+ + MAX_TEMP
+ + "K"
+ + EnumChatFormatting.GRAY
+ + " the recipe will fail and output steam.")
+ .addSeparator()
+ .addInfo(
+ "Consumes up to " + EnumChatFormatting.RED
+ + MAX_PLASMA_PER_SEC
+ + "L/s "
+ + EnumChatFormatting.WHITE
+ + plasmaMaterial.getPlasma(1)
+ .getLocalizedName()
+ + EnumChatFormatting.GRAY
+ + " and up to "
+ + EnumChatFormatting.RED
+ + MAX_COOLANT_PER_SEC
+ + "L/s "
+ + EnumChatFormatting.WHITE
+ + coolantMaterial.getFluid(1)
+ .getLocalizedName())
+ .addInfo(
+ EnumChatFormatting.RED + "Raises "
+ + EnumChatFormatting.GRAY
+ + "the temperature by "
+ + EnumChatFormatting.RED
+ + PLASMA_TEMP_PER_LITER
+ + "K"
+ + EnumChatFormatting.GRAY
+ + " per liter of plasma consumed.")
+ .addInfo(
+ EnumChatFormatting.RED + "Lowers "
+ + EnumChatFormatting.GRAY
+ + "the temperature by "
+ + EnumChatFormatting.RED
+ + -COOLANT_TEMP_PER_LITER
+ + "K"
+ + EnumChatFormatting.GRAY
+ + " per liter of coolant consumed.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "Step five of water purification is to evaporate complex organic polymers and extremophile organisms")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "that might be resistant to simple acids, clarifying agents, and filters. Using an ultra high")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "pressure chamber in combination with extreme temperature fluctuations allows the water to remain")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "supercritical while evaporating any remaining contaminants, ready for filtration.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(23, 15, 15, false)
+ .addCasingInfoExactlyColored(
+ "Reinforced Sterile Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 1091,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Heat-Resistant Trinium Plated Casing",
+ EnumChatFormatting.GRAY,
+ 54,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Any Tinted Industrial Glass",
+ EnumChatFormatting.GRAY,
+ 64,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Superconductor Base ZPM Frame Box",
+ EnumChatFormatting.GRAY,
+ 40,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored("Any Neonite", EnumChatFormatting.GRAY, 8, EnumChatFormatting.GOLD, false)
+ .addCasingInfoExactlyColored(
+ "Superconducting Coil Block",
+ EnumChatFormatting.GRAY,
+ 8,
+ EnumChatFormatting.GOLD,
+ false)
+ .addController("Front center")
+ .addOtherStructurePart("Input Hatch (Water)", EnumChatFormatting.GOLD + "1+", 1)
+ .addOtherStructurePart("Output Hatch", EnumChatFormatting.GOLD + "1", 1)
+ .addOtherStructurePart("Input Hatch (Coolant)", EnumChatFormatting.GOLD + "1", 2)
+ .addOtherStructurePart("Input Hatch (Plasma)", EnumChatFormatting.GOLD + "1", 3)
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ // Reset internal state
+ this.cyclesCompleted = 0;
+ this.currentTemperature = 0;
+ this.ruinedCycle = false;
+ this.state = CycleState.Heating;
+ }
+
+ // Drains up to maxAmount of a fluid if it is the same fluid as given, returns the amount drained
+ private long drainFluidLimited(GT_MetaTileEntity_Hatch_Input inputHatch, FluidStack fluid, long maxAmount) {
+ FluidStack hatchStack = inputHatch.getDrainableStack();
+ if (hatchStack == null) return 0;
+ if (hatchStack.isFluidEqual(fluid)) {
+ long amountToDrain = Math.min(maxAmount, hatchStack.amount);
+ if (amountToDrain > 0) {
+ inputHatch.drain((int) amountToDrain, true);
+ }
+ return amountToDrain;
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void addRecipeOutputs() {
+ super.addRecipeOutputs();
+ // If the cycle was ruined, output steam
+ if (this.ruinedCycle) {
+ FluidStack insertedWater = currentRecipe.mFluidInputs[0];
+ // Multiply by 60 since that's the water:steam ratio in GTNH
+ long steamAmount = insertedWater.amount * 60L;
+ addOutput(GT_ModHandler.getSteam(steamAmount));
+ }
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+ if (mMaxProgresstime > 0 && aTick % CONSUME_INTERVAL == 0) {
+ // Drain plasma and coolant up to limited amount per second
+ long plasmaDrained = drainFluidLimited(plasmaInputHatch, plasmaMaterial.getPlasma(1L), MAX_PLASMA_PER_SEC);
+ long coolantDrained = drainFluidLimited(
+ coolantInputHatch,
+ coolantMaterial.getFluid(1L),
+ MAX_COOLANT_PER_SEC);
+ // Calculate temperature change
+ long tempChance = plasmaDrained * PLASMA_TEMP_PER_LITER + coolantDrained * COOLANT_TEMP_PER_LITER;
+ currentTemperature = Math.max(0, currentTemperature + tempChance);
+ // Check if batch was ruined
+ if (currentTemperature > MAX_TEMP) {
+ ruinedCycle = true;
+ }
+ // Update cycle state.
+ switch (state) {
+ case Heating -> {
+ // Heating state can change to cooling when temperature exceeds 10000K
+ if (currentTemperature >= HEATING_POINT) {
+ state = CycleState.Cooling;
+ }
+ }
+ case Cooling -> {
+ if (currentTemperature == 0) {
+ state = CycleState.Heating;
+ cyclesCompleted += 1;
+ }
+ }
+ }
+ }
+ }
+
+ public boolean addCoolantHatchToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity == null) return false;
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_Hatch_Input) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ ((GT_MetaTileEntity_Hatch_Input) aMetaTileEntity).mRecipeMap = null;
+ coolantInputHatch = (GT_MetaTileEntity_Hatch_Input) aMetaTileEntity;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean addPlasmaHatchToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity == null) return false;
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_Hatch_Input) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ ((GT_MetaTileEntity_Hatch_Input) aMetaTileEntity).mRecipeMap = null;
+ plasmaInputHatch = (GT_MetaTileEntity_Hatch_Input) aMetaTileEntity;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ if (ruinedCycle) return 0.0f;
+ // Success chance directly depends on number of cycles completed.
+ return cyclesCompleted * SUCCESS_PER_CYCLE + currentRecipeChance;
+ }
+
+ @Override
+ public String[] getInfoData() {
+ ArrayList<String> infoData = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ infoData.add("Current temperature: " + EnumChatFormatting.YELLOW + currentTemperature + "K");
+ infoData.add("Heating cycles completed this run: " + EnumChatFormatting.YELLOW + cyclesCompleted);
+ return infoData.toArray(new String[] {});
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ aNBT.setLong("mCurrentTemperature", currentTemperature);
+ aNBT.setInteger("mCyclesCompleted", cyclesCompleted);
+ aNBT.setBoolean("mRuinedCycle", ruinedCycle);
+ aNBT.setString("mCycleState", state.toString());
+ super.saveNBTData(aNBT);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ currentTemperature = aNBT.getLong("mCurrentTemperature");
+ cyclesCompleted = aNBT.getInteger("mCyclesCompleted");
+ ruinedCycle = aNBT.getBoolean("mRuinedCycle");
+ state = CycleState.valueOf(aNBT.getString("mCycleState"));
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 5;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_UV;
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ casingCount = 0;
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ if (casingCount < MIN_CASING) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.GT_MACHINES_PURIFICATION_PLASMA_LOOP.resourceLocation;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitUVTreatment.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitUVTreatment.java
new file mode 100644
index 0000000000..444b3cad4d
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_PurificationUnitUVTreatment.java
@@ -0,0 +1,524 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.lazy;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlock;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.GT_HatchElement.InputHatch;
+import static gregtech.api.enums.GT_HatchElement.OutputHatch;
+import static gregtech.api.enums.GT_Values.AuthorNotAPenguin;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW;
+import static gregtech.api.util.GT_RecipeBuilder.SECONDS;
+import static gregtech.api.util.GT_StructureUtility.ofFrame;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.structure.StructureDefinition;
+
+import gregtech.api.GregTech_API;
+import gregtech.api.enums.Materials;
+import gregtech.api.enums.SoundResource;
+import gregtech.api.enums.Textures;
+import gregtech.api.enums.TierEU;
+import gregtech.api.interfaces.IHatchElement;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch_InputBus;
+import gregtech.api.recipe.RecipeMap;
+import gregtech.api.recipe.RecipeMaps;
+import gregtech.api.recipe.check.CheckRecipeResult;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_StructureUtility;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.IGT_HatchAdder;
+
+public class GT_MetaTileEntity_PurificationUnitUVTreatment
+ extends GT_MetaTileEntity_PurificationUnitBase<GT_MetaTileEntity_PurificationUnitUVTreatment>
+ implements ISurvivalConstructable {
+
+ private static final int CASING_INDEX_MAIN = getTextureIndex(GregTech_API.sBlockCasings9, 12);
+
+ private static final String STRUCTURE_PIECE_MAIN = "main";
+ private static final int STRUCTURE_X_OFFSET = 6;
+ private static final int STRUCTURE_Y_OFFSET = 8;
+ private static final int STRUCTURE_Z_OFFSET = 0;
+
+ private GT_MetaTileEntity_Hatch_InputBus lensInputBus;
+ private GT_MetaTileEntity_LensIndicator lensIndicator;
+
+ private UVTreatmentLensCycle lensCycle = null;
+
+ /**
+ * Bonus chance to success for each lens swap
+ */
+ public static final float SUCCESS_PER_LENS = 10.0f;
+
+ /**
+ * Maximum amount of ticks between two lens swaps
+ */
+ public static final int MAX_TIME_BETWEEN_SWAPS = GT_MetaTileEntity_PurificationPlant.CYCLE_TIME_TICKS / 8;
+ /**
+ * Minimum amount of time between two lens swaps
+ */
+ public static final int MIN_TIME_BETWEEN_SWAPS = MAX_TIME_BETWEEN_SWAPS / 4;
+
+ private int numSwapsPerformed = 0;
+ private int timeUntilNextSwap = 0;
+
+ private boolean removedTooEarly = false;
+
+ private static final String[][] structure = new String[][] {
+ // spotless:off
+ { " ", " DDD ", " ", " ", " ", " ", " ", " DDD ", " H~H " },
+ { " AAA ", " DDAAADD ", " BBB ", " BBB ", " BBB ", " BBB ", " BBB ", " DDBBBDD ", " AAAAAAA " },
+ { " AAAAAAA ", " DDAACCCAADD ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " DDBB BBDD ", " AAAAAAAAAAA " },
+ { " AAAAAAAAAAA ", "DAACCCCCCCAAD", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", "DBB BBD", "HAAAAAAAAAAAH" },
+ { " AAAAALAAAAA ", "DACCCCCCCCCAD", " B B ", " B B ", " B B ", " B B ", " B B ", "DB BD", "HAAAAAAAAAAAH" },
+ { " AAAAAAAAAAA ", "DAACCCCCCCAAD", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", "DBB BBD", "HAAAAAAAAAAAH" },
+ { " AAAAAAA ", " DDAACCCAADD ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " DDBB BBDD ", " AAAAAAAAAAA " },
+ { " AIA ", " DDAAADD ", " BBB ", " BBB ", " BBB ", " BBB ", " BBB ", " DDBBBDD ", " AAAAAAA " },
+ { " ", " DDD ", " ", " ", " ", " ", " ", " DDD ", " HHH " } };
+ // spotless:on
+
+ private static final IStructureDefinition<GT_MetaTileEntity_PurificationUnitUVTreatment> STRUCTURE_DEFINITION = StructureDefinition
+ .<GT_MetaTileEntity_PurificationUnitUVTreatment>builder()
+ .addShape(STRUCTURE_PIECE_MAIN, structure)
+ // Naquadria-Reinforced Water Plant Casing
+ .addElement('A', ofBlock(GregTech_API.sBlockCasings9, 12))
+ // Neutronium-Coated UV-Resistant Glass
+ .addElement('B', ofBlock(GregTech_API.sBlockGlass1, 1))
+ // UV Backlight sterilizer casing
+ .addElement('C', ofBlock(GregTech_API.sBlockCasings9, 13))
+ .addElement('D', ofFrame(Materials.StellarAlloy))
+ // Lens housing bus
+ .addElement(
+ 'L',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitUVTreatment>buildHatchAdder()
+ .atLeast(SpecialHatchElement.LensHousing)
+ .dot(2)
+ .cacheHint(() -> "Lens Housing")
+ .casingIndex(CASING_INDEX_MAIN)
+ .build()))
+ // Lens indicator hatch
+ .addElement(
+ 'I',
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitUVTreatment>buildHatchAdder()
+ .atLeast(SpecialHatchElement.LensIndicator)
+ .dot(3)
+ .cacheHint(() -> "Lens Indicator")
+ .casingIndex(CASING_INDEX_MAIN)
+ .build()))
+ // Input or output hatch
+ .addElement(
+ 'H',
+ ofChain(
+ lazy(
+ t -> GT_StructureUtility.<GT_MetaTileEntity_PurificationUnitUVTreatment>buildHatchAdder()
+ .atLeastList(Arrays.asList(InputHatch, OutputHatch))
+ .dot(1)
+ .cacheHint(() -> "Input Hatch, Output Hatch")
+ .casingIndex(CASING_INDEX_MAIN)
+ .build()),
+ // Naquadria-reinforced Water Plant Casing
+ ofBlock(GregTech_API.sBlockCasings9, 12)))
+ .build();
+
+ public GT_MetaTileEntity_PurificationUnitUVTreatment(int aID, String aName, String aNameRegional) {
+ super(aID, aName, aNameRegional);
+ }
+
+ public GT_MetaTileEntity_PurificationUnitUVTreatment(String aName) {
+ super(aName);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_PurificationUnitUVTreatment(this.mName);
+ }
+
+ @Override
+ public ITexture[] getTexture(IGregTechTileEntity baseMetaTileEntity, ForgeDirection side, ForgeDirection facing,
+ int colorIndex, boolean active, boolean redstoneLevel) {
+ if (side == facing) {
+ if (active) return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_ACTIVE_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR)
+ .extFacing()
+ .build(),
+ TextureFactory.builder()
+ .addIcon(OVERLAY_FRONT_LARGE_CHEMICAL_REACTOR_GLOW)
+ .extFacing()
+ .glow()
+ .build() };
+ }
+ return new ITexture[] { Textures.BlockIcons.getCasingTextureForId(CASING_INDEX_MAIN) };
+ }
+
+ @Override
+ public void construct(ItemStack stackSize, boolean hintsOnly) {
+ buildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ hintsOnly,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET);
+ }
+
+ @Override
+ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBuildEnvironment env) {
+ return survivialBuildPiece(
+ STRUCTURE_PIECE_MAIN,
+ stackSize,
+ STRUCTURE_X_OFFSET,
+ STRUCTURE_Y_OFFSET,
+ STRUCTURE_Z_OFFSET,
+ elementBudget,
+ env,
+ true);
+ }
+
+ @Override
+ public IStructureDefinition<GT_MetaTileEntity_PurificationUnitUVTreatment> getStructureDefinition() {
+ return STRUCTURE_DEFINITION;
+ }
+
+ @Override
+ protected GT_Multiblock_Tooltip_Builder createTooltip() {
+ GT_Multiblock_Tooltip_Builder tt = new GT_Multiblock_Tooltip_Builder();
+ tt.addMachineType("Purification Unit");
+ tt.addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.BOLD
+ + "Water Tier: "
+ + EnumChatFormatting.WHITE
+ + GT_Utility.formatNumbers(getWaterTier())
+ + EnumChatFormatting.RESET)
+ .addInfo("Controller block for the High Energy Laser Purification Unit.")
+ .addInfo("Must be linked to a Purification Plant to work.")
+ .addSeparator()
+ .addInfo(
+ "During operation, swap the lens in the " + EnumChatFormatting.WHITE
+ + "Lens Housing"
+ + EnumChatFormatting.GRAY
+ + ".")
+ .addInfo(
+ "The multiblock will output a signal through the " + EnumChatFormatting.WHITE + "Lens Indicator Hatch")
+ .addInfo("when the current lens must be swapped.")
+ .addInfo(
+ "Lens swaps will be requested in random intervals of " + EnumChatFormatting.RED
+ + (MIN_TIME_BETWEEN_SWAPS / SECONDS)
+ + " to "
+ + (MAX_TIME_BETWEEN_SWAPS / SECONDS)
+ + "s"
+ + EnumChatFormatting.GRAY
+ + ".")
+ .addSeparator()
+ .addInfo(
+ "Success chance is boosted by " + EnumChatFormatting.RED
+ + SUCCESS_PER_LENS
+ + "% "
+ + EnumChatFormatting.GRAY
+ + "for each successful swap performed.")
+ .addInfo("Removing a lens too early will fail the recipe.")
+ .addInfo("Find the order of lenses in the recipe in NEI,")
+ .addInfo("or use a portable scanner to view the currently requested lens.")
+ .addSeparator()
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "The sixth step of water purification involves identifying any remaining negatively charged ions within")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "the water which may cause electrical faults in future wafer manufacturing. Bombarding the water with varying")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "wavelengths of photon beams will impart energy into outer-shell electrons, causing them to detach from the")
+ .addInfo(
+ EnumChatFormatting.AQUA + ""
+ + EnumChatFormatting.ITALIC
+ + "atoms themselves and pass through the walls of the tank, ensuring the water is perfectly electrically polar.")
+ .addInfo(AuthorNotAPenguin)
+ .beginStructureBlock(13, 9, 9, true)
+ .addCasingInfoRangeColored(
+ "Naquadria-Reinforced Water Plant Casing",
+ EnumChatFormatting.GRAY,
+ 147,
+ 155,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Electron-Permeable Neutronium Coated Glass",
+ EnumChatFormatting.GRAY,
+ 144,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "High Energy Ultraviolet Emitter Casing",
+ EnumChatFormatting.GRAY,
+ 24,
+ EnumChatFormatting.GOLD,
+ false)
+ .addCasingInfoExactlyColored(
+ "Stellar Alloy Frame Box",
+ EnumChatFormatting.GRAY,
+ 56,
+ EnumChatFormatting.GOLD,
+ false)
+ .addController("Front center")
+ .addOtherStructurePart("Input Hatch, Output Hatch", EnumChatFormatting.GOLD + "1+", 1)
+ .addOtherStructurePart("Lens Housing", EnumChatFormatting.GOLD + "1", 2)
+ .addOtherStructurePart("Lens Indicator", EnumChatFormatting.GOLD + "1", 3)
+ .addStructureInfo("Use the StructureLib Hologram Projector to build the structure.")
+ .toolTipFinisher("GregTech");
+ return tt;
+ }
+
+ @Override
+ public RecipeMap<?> getRecipeMap() {
+ return RecipeMaps.purificationUVTreatmentRecipes;
+ }
+
+ @NotNull
+ @Override
+ public CheckRecipeResult checkProcessing() {
+ CheckRecipeResult result = super.checkProcessing();
+ if (result.wasSuccessful()) {
+ // Note that this cast is fine, look at GT_PurifiedWaterRecipes.java
+ this.lensCycle = new UVTreatmentLensCycle((List<ItemStack>) this.currentRecipe.mSpecialItems);
+ }
+ return result;
+ }
+
+ private int generateNextSwapTime() {
+ ThreadLocalRandom random = ThreadLocalRandom.current();
+ return random.nextInt(MIN_TIME_BETWEEN_SWAPS, MAX_TIME_BETWEEN_SWAPS);
+ }
+
+ @Override
+ public void startCycle(int cycleTime, int progressTime) {
+ super.startCycle(cycleTime, progressTime);
+ // Reset internal state
+ this.timeUntilNextSwap = 0;
+ this.numSwapsPerformed = 0;
+ this.lensCycle.reset();
+ this.removedTooEarly = false;
+ }
+
+ private ItemStack getCurrentlyInsertedLens() {
+ return this.lensInputBus.getStackInSlot(0);
+ }
+
+ @Override
+ protected void runMachine(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ super.runMachine(aBaseMetaTileEntity, aTick);
+
+ // Do no processing if no recipe is running
+ if (mMaxProgresstime <= 0) return;
+
+ // This can happen because the lens cycle isn't saved to NBT correctly yet, FIXME
+ if (this.lensCycle == null) {
+ // FIXME: Properly save current recipe in NBT instead of exiting early
+ return;
+ }
+
+ ItemStack currentLens = getCurrentlyInsertedLens();
+
+ // If we are currently counting down to a next swap, do so
+ if (timeUntilNextSwap > 0) {
+ timeUntilNextSwap -= 1;
+ // Set the indicator to not output a signal for now
+ lensIndicator.updateRedstoneOutput(false);
+
+ // If we are counting down to the next swap, and there is no correct lens in the bus, we removed a lens
+ // too early
+ if (currentLens == null || !currentLens.isItemEqual(lensCycle.current())) {
+ removedTooEarly = true;
+ }
+
+ // If the time until the next swap became zero, move on to the next requested lens
+ if (timeUntilNextSwap == 0) {
+ boolean advanced = lensCycle.advance();
+ if (!advanced) {
+ // cycle didn't advance, we arrived at the end. This mainly means we want to stop the cycle
+ // The easiest way to do this is by setting the time until next swap larger than the recipe time
+ timeUntilNextSwap = mMaxProgresstime + 1;
+ }
+ }
+ }
+
+ // Time until next swap is zero, this means we are waiting for the user to output a lens.
+ else if (timeUntilNextSwap == 0) {
+ // Set the indicator to output a signal
+ lensIndicator.updateRedstoneOutput(true);
+
+ // If we now have a matching lens, we can accept it and move on to the next swap
+ if (currentLens != null && currentLens.isItemEqual(lensCycle.current())) {
+ numSwapsPerformed += 1;
+ timeUntilNextSwap = generateNextSwapTime();
+ }
+ }
+ }
+
+ @Override
+ public boolean isCorrectMachinePart(ItemStack aStack) {
+ return true;
+ }
+
+ @Override
+ public int getWaterTier() {
+ return 6;
+ }
+
+ @Override
+ public long getBasePowerUsage() {
+ return TierEU.RECIPE_UV;
+ }
+
+ @Override
+ public float calculateFinalSuccessChance() {
+ if (removedTooEarly) return 0.0f;
+ return numSwapsPerformed * SUCCESS_PER_LENS + currentRecipeChance;
+ }
+
+ @Override
+ public String[] getInfoData() {
+ ArrayList<String> infoData = new ArrayList<>(Arrays.asList(super.getInfoData()));
+ if (this.lensCycle != null) {
+ infoData.add("Lens swaps performed this run: " + EnumChatFormatting.YELLOW + numSwapsPerformed);
+ infoData.add(
+ "Current lens requested: " + EnumChatFormatting.GREEN
+ + lensCycle.current()
+ .getDisplayName());
+ if (removedTooEarly) {
+ infoData.add("Removed lens too early. Failing this recipe.");
+ }
+ }
+ return infoData.toArray(new String[] {});
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ super.saveNBTData(aNBT);
+ aNBT.setInteger("numSwapsPerformed", numSwapsPerformed);
+ aNBT.setInteger("timeUntilNextSwap", timeUntilNextSwap);
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ super.loadNBTData(aNBT);
+ numSwapsPerformed = aNBT.getInteger("numSwapsPerformed");
+ timeUntilNextSwap = aNBT.getInteger("timeUntilNextSwap");
+ }
+
+ public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
+ if (!checkPiece(STRUCTURE_PIECE_MAIN, STRUCTURE_X_OFFSET, STRUCTURE_Y_OFFSET, STRUCTURE_Z_OFFSET)) return false;
+ return super.checkMachine(aBaseMetaTileEntity, aStack);
+ }
+
+ public boolean addLensHousingToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_LensHousing) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ this.lensInputBus = (GT_MetaTileEntity_LensHousing) aMetaTileEntity;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean addLensIndicatorToMachineList(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
+ if (aTileEntity == null) return false;
+ IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
+ if (aMetaTileEntity instanceof GT_MetaTileEntity_LensIndicator) {
+ ((GT_MetaTileEntity_Hatch) aMetaTileEntity).updateTexture(aBaseCasingIndex);
+ this.lensIndicator = (GT_MetaTileEntity_LensIndicator) aMetaTileEntity;
+ lensIndicator.updateRedstoneOutput(false);
+ return true;
+ }
+ return false;
+ }
+
+ private enum SpecialHatchElement implements IHatchElement<GT_MetaTileEntity_PurificationUnitUVTreatment> {
+
+ LensHousing(GT_MetaTileEntity_PurificationUnitUVTreatment::addLensHousingToMachineList,
+ GT_MetaTileEntity_LensHousing.class) {
+
+ @Override
+ public long count(
+ GT_MetaTileEntity_PurificationUnitUVTreatment gtMetaTileEntityPurificationUnitUVTreatment) {
+ if (gtMetaTileEntityPurificationUnitUVTreatment.lensInputBus == null) return 0;
+ else return 1;
+ }
+ },
+
+ LensIndicator(GT_MetaTileEntity_PurificationUnitUVTreatment::addLensIndicatorToMachineList,
+ GT_MetaTileEntity_LensHousing.class) {
+
+ @Override
+ public long count(
+ GT_MetaTileEntity_PurificationUnitUVTreatment gtMetaTileEntityPurificationUnitUVTreatment) {
+ if (gtMetaTileEntityPurificationUnitUVTreatment.lensIndicator == null) return 0;
+ else return 1;
+ }
+ };
+
+ private final List<Class<? extends IMetaTileEntity>> mteClasses;
+ private final IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitUVTreatment> adder;
+
+ @SafeVarargs
+ SpecialHatchElement(IGT_HatchAdder<GT_MetaTileEntity_PurificationUnitUVTreatment> adder,
+ Class<? extends IMetaTileEntity>... mteClasses) {
+ this.mteClasses = Collections.unmodifiableList(Arrays.asList(mteClasses));
+ this.adder = adder;
+ }
+
+ @Override
+ public List<? extends Class<? extends IMetaTileEntity>> mteClasses() {
+ return mteClasses;
+ }
+
+ public IGT_HatchAdder<? super GT_MetaTileEntity_PurificationUnitUVTreatment> adder() {
+ return adder;
+ }
+ }
+
+ @Override
+ protected ResourceLocation getActivitySoundLoop() {
+ return SoundResource.IC2_MACHINES_MAGNETIZER_LOOP.resourceLocation;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_pHSensor.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_pHSensor.java
new file mode 100644
index 0000000000..6bb4c0952c
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/GT_MetaTileEntity_pHSensor.java
@@ -0,0 +1,196 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.StatCollector;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import com.gtnewhorizons.modularui.api.math.Alignment;
+import com.gtnewhorizons.modularui.api.math.Color;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+import com.gtnewhorizons.modularui.common.widget.TextWidget;
+import com.gtnewhorizons.modularui.common.widget.textfield.NumericWidget;
+
+import gregtech.api.enums.Textures;
+import gregtech.api.gui.modularui.GT_UIInfos;
+import gregtech.api.gui.modularui.GT_UITextures;
+import gregtech.api.interfaces.IIconContainer;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Utility;
+import gregtech.common.gui.modularui.widget.CoverCycleButtonWidget;
+
+public class GT_MetaTileEntity_pHSensor extends GT_MetaTileEntity_Hatch {
+
+ // This implementation was largely copied from the neutron sensor hatch
+
+ protected float threshold = 0;
+ protected boolean inverted = false;
+ private boolean isOn = false;
+
+ private static final IIconContainer textureFont = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR;
+ private static final IIconContainer textureFont_Glow = Textures.BlockIcons.OVERLAY_HATCH_PH_SENSOR_GLOW;
+
+ public GT_MetaTileEntity_pHSensor(int aID, String aName, String aNameRegional, int aTier) {
+ super(aID, aName, aNameRegional, aTier, 0, "Detects pH value.");
+ }
+
+ public GT_MetaTileEntity_pHSensor(String aName, int aTier, String[] aDescription, ITexture[][][] aTextures) {
+ super(aName, aTier, 0, aDescription, aTextures);
+ }
+
+ @Override
+ public boolean isValidSlot(int aIndex) {
+ return false;
+ }
+
+ @Override
+ public boolean isSimpleMachine() {
+ return true;
+ }
+
+ @Override
+ public boolean isFacingValid(ForgeDirection facing) {
+ return true;
+ }
+
+ @Override
+ public boolean isAccessAllowed(EntityPlayer aPlayer) {
+ return true;
+ }
+
+ @Override
+ public boolean allowGeneralRedstoneOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean allowPullStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection Side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public boolean allowPutStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection side,
+ ItemStack aStack) {
+ return false;
+ }
+
+ @Override
+ public void initDefaultModes(NBTTagCompound aNBT) {
+ getBaseMetaTileEntity().setActive(true);
+ }
+
+ @Override
+ public boolean onRightclick(IGregTechTileEntity aBaseMetaTileEntity, EntityPlayer aPlayer, ForgeDirection side,
+ float aX, float aY, float aZ) {
+ GT_UIInfos.openGTTileEntityUI(aBaseMetaTileEntity, aPlayer);
+ return true;
+ }
+
+ @Override
+ public boolean useModularUI() {
+ return true;
+ }
+
+ @Override
+ public String[] getDescription() {
+ return new String[] { "Can be installed in the pH Neutralization Purification Unit.",
+ "Outputs Redstone Signal according to the current pH value.",
+ "Right click to open the GUI and change settings." };
+ }
+
+ @Override
+ public void loadNBTData(NBTTagCompound aNBT) {
+ threshold = aNBT.getFloat("mThreshold");
+ inverted = aNBT.getBoolean("mInverted");
+ super.loadNBTData(aNBT);
+ }
+
+ @Override
+ public void saveNBTData(NBTTagCompound aNBT) {
+ aNBT.setFloat("mThreshold", threshold);
+ aNBT.setBoolean("mInverted", inverted);
+ super.saveNBTData(aNBT);
+ }
+
+ /**
+ * Updates redstone output strength based on the pH of the multiblock.
+ */
+ public void updateRedstoneOutput(float pH) {
+ isOn = (pH > threshold) ^ inverted;
+ }
+
+ @Override
+ public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
+ if (isOn) {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setStrongOutputRedstoneSignal(side, (byte) 15);
+ }
+ } else {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ aBaseMetaTileEntity.setStrongOutputRedstoneSignal(side, (byte) 0);
+ }
+ }
+ super.onPostTick(aBaseMetaTileEntity, aTick);
+ }
+
+ @Override
+ public IMetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) {
+ return new GT_MetaTileEntity_pHSensor(mName, mTier, mDescriptionArray, mTextures);
+ }
+
+ @Override
+ public ITexture[] getTexturesActive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont), TextureFactory.builder()
+ .addIcon(textureFont_Glow)
+ .glow()
+ .build() };
+ }
+
+ @Override
+ public ITexture[] getTexturesInactive(ITexture aBaseTexture) {
+ return new ITexture[] { aBaseTexture, TextureFactory.of(textureFont) };
+ }
+
+ public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) {
+ final String INVERTED = GT_Utility.trans("INVERTED", "Inverted");
+ final String NORMAL = GT_Utility.trans("NORMAL", "Normal");
+
+ builder.widget(
+ new CoverCycleButtonWidget().setToggle(() -> inverted, (val) -> inverted = val)
+ .setTextureGetter(
+ (state) -> state == 1 ? GT_UITextures.OVERLAY_BUTTON_REDSTONE_ON
+ : GT_UITextures.OVERLAY_BUTTON_REDSTONE_OFF)
+ .addTooltip(0, NORMAL)
+ .addTooltip(1, INVERTED)
+ .setPos(10, 8))
+ .widget(
+ new TextWidget().setStringSupplier(() -> inverted ? INVERTED : NORMAL)
+ .setDefaultColor(COLOR_TEXT_GRAY.get())
+ .setTextAlignment(Alignment.CenterLeft)
+ .setPos(28, 12))
+ .widget(
+ new NumericWidget().setBounds(0, 14.0)
+ .setGetter(() -> (double) threshold)
+ .setSetter((value) -> threshold = (float) value)
+ .setScrollValues(0.1, 0.01, 1.0)
+ .setMaximumFractionDigits(2)
+ .setTextColor(Color.WHITE.dark(1))
+ .setTextAlignment(Alignment.CenterLeft)
+ .setFocusOnGuiOpen(true)
+ .setBackground(GT_UITextures.BACKGROUND_TEXT_FIELD.withOffset(-1, -1, 2, 2))
+ .setPos(10, 28)
+ .setSize(77, 12))
+ .widget(
+ new TextWidget(StatCollector.translateToLocal("GT5U.gui.text.ph_sensor"))
+ .setDefaultColor(COLOR_TEXT_GRAY.get())
+ .setTextAlignment(Alignment.CenterLeft)
+ .setPos(90, 30));
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/LinkedPurificationUnit.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/LinkedPurificationUnit.java
new file mode 100644
index 0000000000..104cbe300f
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/LinkedPurificationUnit.java
@@ -0,0 +1,125 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import static gregtech.GT_Mod.gregtechproxy;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.world.World;
+import net.minecraftforge.common.DimensionManager;
+
+import gregtech.api.interfaces.tileentity.IGregTechTileEntity;
+import gregtech.api.util.GT_Util;
+
+/**
+ * Small wrapper around a GT_MetaTileEntity_PurificationUnitBase, to be stored in the main purification plant
+ * controller. May be useful for storing additional data in the controller that the individual units do not need
+ * to know about.
+ */
+public class LinkedPurificationUnit {
+
+ /**
+ * Whether this unit is active in the current cycle. We need to keep track of this so units cannot come online
+ * in the middle of a cycle and suddenly start processing.
+ */
+ private boolean mIsActive = false;
+
+ private final GT_MetaTileEntity_PurificationUnitBase<?> mMetaTileEntity;
+
+ public LinkedPurificationUnit(GT_MetaTileEntity_PurificationUnitBase<?> unit) {
+ this.mMetaTileEntity = unit;
+ }
+
+ /**
+ * Construct new link data from a NBT compound. This is intended to sync the linked units to the client.
+ *
+ * @param nbtData NBT data obtained from writeLinkDataToNBT()
+ */
+ public LinkedPurificationUnit(NBTTagCompound nbtData) {
+ this.mIsActive = nbtData.getBoolean("active");
+ NBTTagCompound linkData = nbtData.getCompoundTag("linkData");
+ World world = null;
+ if (!gregtechproxy.isClientSide()) {
+ world = DimensionManager.getWorld(nbtData.getInteger("worldID"));
+ } else {
+ world = Minecraft.getMinecraft().thePlayer.worldObj;
+ }
+
+ // Load coordinates from link data
+ int x = linkData.getInteger("x");
+ int y = linkData.getInteger("y");
+ int z = linkData.getInteger("z");
+
+ // Find a TileEntity at this location
+ TileEntity te = GT_Util.getTileEntity(world, x, y, z, true);
+ if (te == null) {
+ // This is a bug, throw a fatal error.
+ throw new NullPointerException("Unit disappeared during server sync. This is a bug.");
+ }
+
+ // Cast TileEntity to proper GT TileEntity
+ this.mMetaTileEntity = (GT_MetaTileEntity_PurificationUnitBase<?>) ((IGregTechTileEntity) te)
+ .getMetaTileEntity();
+ }
+
+ public GT_MetaTileEntity_PurificationUnitBase<?> metaTileEntity() {
+ return mMetaTileEntity;
+ }
+
+ /**
+ * Whether this unit is considered as active in the current cycle
+ *
+ * @return true if this unit is active in the current cycle
+ */
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ public void setActive(boolean active) {
+ this.mIsActive = active;
+ }
+
+ public String getStatusString() {
+ if (this.isActive()) {
+ return EnumChatFormatting.GREEN + "Active";
+ }
+
+ PurificationUnitStatus status = this.mMetaTileEntity.status();
+ switch (status) {
+ case ONLINE -> {
+ return EnumChatFormatting.GREEN + "Online";
+ }
+ case DISABLED -> {
+ return EnumChatFormatting.YELLOW + "Disabled";
+ }
+ case INCOMPLETE_STRUCTURE -> {
+ return EnumChatFormatting.RED + "Incomplete Structure";
+ }
+ }
+
+ // If this happens, this is a bug and there is a switch case missing.
+ return null;
+ }
+
+ /**
+ * Save link data to a NBT tag, so it can be reconstructed at the client side
+ */
+ public NBTTagCompound writeLinkDataToNBT() {
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setBoolean("active,", this.mIsActive);
+
+ NBTTagCompound linkData = new NBTTagCompound();
+ IGregTechTileEntity mte = this.mMetaTileEntity.getBaseMetaTileEntity();
+ linkData.setInteger("x", mte.getXCoord());
+ linkData.setInteger("y", mte.getYCoord());
+ linkData.setInteger("z", mte.getZCoord());
+ tag.setTag("linkData", linkData);
+
+ tag.setInteger(
+ "worldID",
+ this.mMetaTileEntity.getBaseMetaTileEntity()
+ .getWorld().provider.dimensionId);
+ return tag;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationPlantStructureString.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationPlantStructureString.java
new file mode 100644
index 0000000000..7c40f9be53
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationPlantStructureString.java
@@ -0,0 +1,15 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+// Extracted to another class to not fill the main source file with bloat
+public class PurificationPlantStructureString {
+
+ public static final String[][] STRUCTURE_STRING = new String[][] {
+ { " ", " ", " ", " GBG ", " GHG ", " GHG ", " G~G ", " GHG ", "AAAAAAA" },
+ { " ", " ", " ", " GBG ", "BBBBBBB", "BCCCCCB", "BCCCCCB", "BCCCCCB", "AAAAAAA" },
+ { " G G ", " G G ", " G G ", " GGBGG ", "BCCCCCB", "C C", "C C", "C C", "AAAAAAA" },
+ { " BBB ", " BBB ", " BBB ", " BBB ", "BCCCCCB", "C C", "D C", "C C", "AAAAAAA" },
+ { " BFB ", " BFB ", " BFB ", " BFB ", "BCCCCCB", "C C", "D C", "C C", "AAAAAAA" },
+ { " BBB ", " BBB ", " BBB ", " BBB ", "BCCCCCB", "C C", "D ", "C ", "AAAAAAA" },
+ { " G G ", " G G ", " G G ", " G G ", "BCCCCCB", "C C", "C C", "C C", "AAAAAAA" },
+ { " ", " ", " ", " ", "BBBBBBB", "BCCCCCB", "BCDDDCB", "BCCCCCB", "AAAAAAA" } };
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationUnitStatus.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationUnitStatus.java
new file mode 100644
index 0000000000..403961e28d
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurificationUnitStatus.java
@@ -0,0 +1,14 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+/**
+ * Represents the status of a linked purification unit. This is updated by the main purification plant
+ * controller by checking the linked machines.
+ */
+public enum PurificationUnitStatus {
+ // The purification unit is online and ready to work
+ ONLINE,
+ // The purification unit is correctly formed, but switched off.
+ DISABLED,
+ // The purification unit has failed its structure check
+ INCOMPLETE_STRUCTURE,
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurifiedWaterHelpers.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurifiedWaterHelpers.java
new file mode 100644
index 0000000000..ec25cc394e
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/PurifiedWaterHelpers.java
@@ -0,0 +1,35 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import net.minecraftforge.fluids.FluidStack;
+
+import gregtech.api.enums.Materials;
+
+public class PurifiedWaterHelpers {
+
+ public static Materials getPurifiedWaterTier(int tier) {
+ return switch (tier) {
+ case 1 -> Materials.Grade1PurifiedWater;
+ case 2 -> Materials.Grade2PurifiedWater;
+ case 3 -> Materials.Grade3PurifiedWater;
+ case 4 -> Materials.Grade4PurifiedWater;
+ case 5 -> Materials.Grade5PurifiedWater;
+ case 6 -> Materials.Grade6PurifiedWater;
+ case 7 -> Materials.Grade7PurifiedWater;
+ case 8 -> Materials.Grade8PurifiedWater;
+ default -> throw new IllegalStateException("Unexpected value: " + tier);
+ };
+ }
+
+ public static int getWaterTier(FluidStack fluid) {
+ if (fluid == null) return 0;
+ else if (fluid.isFluidEqual(Materials.Grade1PurifiedWater.getFluid(1000L))) return 1;
+ else if (fluid.isFluidEqual(Materials.Grade2PurifiedWater.getFluid(1000L))) return 2;
+ else if (fluid.isFluidEqual(Materials.Grade3PurifiedWater.getFluid(1000L))) return 3;
+ else if (fluid.isFluidEqual(Materials.Grade4PurifiedWater.getFluid(1000L))) return 4;
+ else if (fluid.isFluidEqual(Materials.Grade5PurifiedWater.getFluid(1000L))) return 5;
+ else if (fluid.isFluidEqual(Materials.Grade6PurifiedWater.getFluid(1000L))) return 6;
+ else if (fluid.isFluidEqual(Materials.Grade7PurifiedWater.getFluid(1000L))) return 7;
+ else if (fluid.isFluidEqual(Materials.Grade8PurifiedWater.getFluid(1000L))) return 8;
+ else return 0;
+ }
+}
diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/purification/UVTreatmentLensCycle.java b/src/main/java/gregtech/common/tileentities/machines/multi/purification/UVTreatmentLensCycle.java
new file mode 100644
index 0000000000..b834cdd0fe
--- /dev/null
+++ b/src/main/java/gregtech/common/tileentities/machines/multi/purification/UVTreatmentLensCycle.java
@@ -0,0 +1,39 @@
+package gregtech.common.tileentities.machines.multi.purification;
+
+import java.util.List;
+
+import net.minecraft.item.ItemStack;
+
+public class UVTreatmentLensCycle {
+
+ private final List<ItemStack> lenses;
+
+ private int currentLens = 0;
+
+ public UVTreatmentLensCycle(List<ItemStack> lenses) {
+ this.lenses = lenses;
+ if (lenses.isEmpty()) {
+ throw new IllegalArgumentException("Supplied lens array may not be empty");
+ }
+ }
+
+ public ItemStack current() {
+ return lenses.get(currentLens);
+ }
+
+ public boolean advance() {
+ if (currentLens < lenses.size() - 1) {
+ currentLens = currentLens + 1;
+ return true;
+ }
+ return false;
+ }
+
+ public void reset() {
+ currentLens = 0;
+ }
+
+ public ItemStack first() {
+ return lenses.get(0);
+ }
+}