aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api/multitileentity/multiblock
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api/multitileentity/multiblock')
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/ComplexParallelController.java111
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/Controller.java1082
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/MultiBlockPart.java693
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/StackableController.java128
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/StackableModularController.java77
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/base/WallShareablePart.java100
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/casing/BasicCasing.java7
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/casing/CasingBehaviorBase.java10
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/casing/FunctionalCasing.java29
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/casing/Glasses.java32
-rw-r--r--src/main/java/gregtech/api/multitileentity/multiblock/casing/UpgradeCasing.java35
11 files changed, 2304 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/ComplexParallelController.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/ComplexParallelController.java
new file mode 100644
index 0000000000..cdcb77d6e5
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/ComplexParallelController.java
@@ -0,0 +1,111 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+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.EnumChatFormatting;
+import net.minecraft.util.StatCollector;
+import net.minecraft.world.World;
+
+import gregtech.api.logic.ComplexParallelProcessingLogic;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.GT_Waila;
+import mcp.mobius.waila.api.IWailaConfigHandler;
+import mcp.mobius.waila.api.IWailaDataAccessor;
+
+public abstract class ComplexParallelController<C extends ComplexParallelController<C, P>, P extends ComplexParallelProcessingLogic<P>>
+ extends Controller<C, P> {
+
+ protected int maxComplexParallels = 0;
+ protected int currentComplexParallels = 0;
+
+ public ComplexParallelController() {
+ isSimpleMachine = false;
+ }
+
+ protected void setMaxComplexParallels(int parallel, boolean stopMachine) {
+ if (parallel != maxComplexParallels && maxComplexParallels != 0 && stopMachine) {
+ stopMachine(false);
+ }
+ maxComplexParallels = parallel;
+ setProcessingUpdate(true);
+ }
+
+ @Override
+ protected void stopMachine(boolean powerShutDown) {
+ super.stopMachine(powerShutDown);
+ }
+
+ protected boolean hasPerfectOverclock() {
+ return false;
+ }
+
+ @Override
+ protected void addProgressStringToScanner(EntityPlayer player, int logLevel, ArrayList<String> list) {
+ P processing = getProcessingLogic();
+ for (int i = 0; i < maxComplexParallels; i++) {
+ list.add(
+ StatCollector.translateToLocal("GT5U.multiblock.Progress") + " "
+ + (i + 1)
+ + ": "
+ + EnumChatFormatting.GREEN
+ + GT_Utility.formatNumbers(
+ processing.getProgress(i) > 20 ? processing.getProgress(i) / 20 : processing.getProgress(i))
+ + EnumChatFormatting.RESET
+ + (processing.getProgress(i) > 20 ? " s / " : " ticks / ")
+ + EnumChatFormatting.YELLOW
+ + GT_Utility.formatNumbers(
+ processing.getDuration(i) > 20 ? processing.getDuration(i) / 20 : processing.getDuration(i))
+ + EnumChatFormatting.RESET
+ + (processing.getDuration(i) > 20 ? " s" : " ticks"));
+ }
+ }
+
+ @Override
+ public void getWailaNBTData(EntityPlayerMP player, TileEntity tile, NBTTagCompound tag, World world, int x, int y,
+ int z) {
+ super.getWailaNBTData(player, tile, tag, world, x, y, z);
+ P processing = getProcessingLogic();
+ tag.setInteger("maxComplexParallels", maxComplexParallels);
+ for (int i = 0; i < maxComplexParallels; i++) {
+ tag.setInteger("maxProgress" + i, processing.getDuration(i));
+ tag.setInteger("progress" + i, processing.getProgress(i));
+ }
+ }
+
+ @Override
+ public void getWailaBody(ItemStack itemStack, List<String> currentTip, IWailaDataAccessor accessor,
+ IWailaConfigHandler config) {
+ super.getWailaBody(itemStack, currentTip, accessor, config);
+ final NBTTagCompound tag = accessor.getNBTData();
+ maxComplexParallels = tag.getInteger("maxComplexParallels");
+ for (int i = 0; i < maxComplexParallels; i++) {
+ long maxProgress = tag.getInteger("maxProgress" + i);
+ long progress = tag.getInteger("progress" + i);
+ currentTip.add(
+ "Process " + (i + 1)
+ + ": "
+ + GT_Waila
+ .getMachineProgressString(maxProgress > 0 && maxProgress >= progress, maxProgress, progress));
+ }
+ }
+
+ @Override
+ public void setProcessingLogicPower(@Nonnull P processingLogic) {
+ processingLogic.setAmperageOC(true);
+ processingLogic.setAvailableAmperage(getPowerLogic().getMaxAmperage() / maxComplexParallels);
+ processingLogic.setAvailableVoltage(getPowerLogic().getVoltage() / maxComplexParallels);
+ }
+
+ @Override
+ public void updateProcessingLogic(@Nonnull P processingLogic) {
+ processingLogic.setMaxComplexParallel(maxComplexParallels);
+ }
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/Controller.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/Controller.java
new file mode 100644
index 0000000000..552cf6d94e
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/Controller.java
@@ -0,0 +1,1082 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import static gregtech.api.util.GT_Utility.moveMultipleItemStacks;
+import static gregtech.common.misc.WirelessNetworkManager.strongCheckOrAddUser;
+import static mcp.mobius.waila.api.SpecialChars.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.inventory.IInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.StatCollector;
+import net.minecraft.world.World;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.FluidStack;
+
+import org.jetbrains.annotations.NotNull;
+import org.lwjgl.input.Keyboard;
+
+import com.gtnewhorizon.structurelib.StructureLibAPI;
+import com.gtnewhorizon.structurelib.alignment.IAlignment;
+import com.gtnewhorizon.structurelib.alignment.IAlignmentLimits;
+import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
+import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
+import com.gtnewhorizon.structurelib.alignment.enumerable.Flip;
+import com.gtnewhorizon.structurelib.alignment.enumerable.Rotation;
+import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
+import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
+import com.gtnewhorizon.structurelib.util.Vec3Impl;
+import com.gtnewhorizons.modularui.api.screen.ModularWindow;
+
+import cpw.mods.fml.common.network.NetworkRegistry;
+import gregtech.api.enums.GT_Values.NBT;
+import gregtech.api.enums.InventoryType;
+import gregtech.api.enums.VoidingMode;
+import gregtech.api.interfaces.IDescribable;
+import gregtech.api.interfaces.fluid.IFluidStore;
+import gregtech.api.logic.ControllerFluidLogic;
+import gregtech.api.logic.ControllerItemLogic;
+import gregtech.api.logic.FluidInventoryLogic;
+import gregtech.api.logic.ItemInventoryLogic;
+import gregtech.api.logic.MuTEProcessingLogic;
+import gregtech.api.logic.PowerLogic;
+import gregtech.api.multitileentity.WeakTargetRef;
+import gregtech.api.multitileentity.enums.MultiTileCasingPurpose;
+import gregtech.api.multitileentity.interfaces.IMultiBlockController;
+import gregtech.api.multitileentity.interfaces.IMultiBlockPart;
+import gregtech.api.multitileentity.machine.MultiTileBasicMachine;
+import gregtech.api.multitileentity.multiblock.casing.FunctionalCasing;
+import gregtech.api.multitileentity.multiblock.casing.UpgradeCasing;
+import gregtech.api.net.GT_Packet_MultiTileEntity;
+import gregtech.api.objects.GT_ItemStack;
+import gregtech.api.util.GT_Multiblock_Tooltip_Builder;
+import gregtech.api.util.GT_Utility;
+import gregtech.api.util.GT_Waila;
+import mcp.mobius.waila.api.IWailaConfigHandler;
+import mcp.mobius.waila.api.IWailaDataAccessor;
+
+/**
+ * Multi Tile Entities - or MuTEs - don't have dedicated hatches, but their casings can become hatches.
+ */
+public abstract class Controller<C extends Controller<C, P>, P extends MuTEProcessingLogic<P>> extends
+ MultiTileBasicMachine<P> implements IAlignment, IMultiBlockController, IDescribable, ISurvivalConstructable {
+
+ public static final String ALL_INVENTORIES_NAME = "all";
+ protected static final int AUTO_OUTPUT_FREQUENCY_TICK = 20;
+
+ private static final Map<Integer, GT_Multiblock_Tooltip_Builder> tooltip = new ConcurrentHashMap<>();
+ private final List<WeakTargetRef<UpgradeCasing>> upgradeCasings = new ArrayList<>();
+ private final List<WeakTargetRef<FunctionalCasing>> functionalCasings = new ArrayList<>();
+ protected BuildState buildState = new BuildState();
+
+ private boolean structureOkay = false, structureChanged = false;
+ private ExtendedFacing extendedFacing = ExtendedFacing.DEFAULT;
+ private IAlignmentLimits limits = getInitialAlignmentLimits();
+ protected boolean separateInputs = getDefaultInputSeparationMode();
+ protected VoidingMode voidingMode = getDefaultVoidingMode();
+ protected boolean batchMode = getDefaultBatchMode();
+ protected boolean recipeLock = getDefaultRecipeLockingMode();
+ protected boolean shouldSort = false;
+ /** If this is set to true, the machine will get default WAILA behavior */
+ protected boolean isSimpleMachine = true;
+
+ protected boolean isCleanroom = false;
+ protected ControllerItemLogic controllerItemInput = new ControllerItemLogic();
+ protected ControllerItemLogic controllerItemOutput = new ControllerItemLogic();
+ protected ControllerFluidLogic controllerFluidInput = new ControllerFluidLogic();
+ protected ControllerFluidLogic controllerFluidOutput = new ControllerFluidLogic();
+
+ // A list of sides
+ // Each side has a list of parts that have a cover that need to be ticked
+ protected List<List<WeakTargetRef<IMultiBlockPart>>> registeredCoveredParts = Arrays.asList(
+ new ArrayList<>(),
+ new ArrayList<>(),
+ new ArrayList<>(),
+ new ArrayList<>(),
+ new ArrayList<>(),
+ new ArrayList<>());
+
+ // A list for each purpose that a casing can register to, to be ticked
+ protected List<List<WeakTargetRef<IMultiBlockPart>>> registeredTickableParts = new ArrayList<>();
+
+ public Controller() {
+ for (int i = 0; i < MultiTileCasingPurpose.values().length; i++) {
+ registeredTickableParts.add(new ArrayList<>());
+ }
+ }
+
+ /** Registry ID of the required casing */
+ public abstract short getCasingRegistryID();
+
+ /** Meta ID of the required casing */
+ public abstract int getCasingMeta();
+
+ /**
+ * Create the tooltip for this multi block controller.
+ */
+ protected abstract GT_Multiblock_Tooltip_Builder createTooltip();
+
+ /**
+ * @return The starting offset for the structure builder
+ */
+ public abstract Vec3Impl getStartingStructureOffset();
+
+ /**
+ * Due to limitation of Java type system, you might need to do an unchecked cast. HOWEVER, the returned
+ * IStructureDefinition is expected to be evaluated against current instance only, and should not be used against
+ * other instances, even for those of the same class.
+ */
+ @Override
+ public abstract IStructureDefinition<C> getStructureDefinition();
+
+ /**
+ * Checks the Machine.
+ * <p>
+ * NOTE: If using `buildState` be sure to `startBuilding()` and either `endBuilding()` or `failBuilding()`
+ */
+ public boolean checkMachine() {
+ calculateTier();
+ updatePowerLogic();
+ return tier > 0;
+ }
+
+ protected void calculateTier() {
+ if (functionalCasings.size() == 0) {
+ return;
+ }
+ double sum = 0;
+ for (WeakTargetRef<FunctionalCasing> casingRef : functionalCasings) {
+ final FunctionalCasing casing = casingRef.get();
+ if (casing == null) continue;
+ sum += casing.getPartTier() * casing.getPartModifier();
+ }
+ tier = (int) Math.min(Math.floor(sum / functionalCasings.size()), 14);
+ }
+
+ @Override
+ public void writeMultiTileNBT(NBTTagCompound nbt) {
+ super.writeMultiTileNBT(nbt);
+
+ nbt.setBoolean(NBT.STRUCTURE_OK, structureOkay);
+ nbt.setByte(
+ NBT.ROTATION,
+ (byte) extendedFacing.getRotation()
+ .getIndex());
+ nbt.setByte(
+ NBT.FLIP,
+ (byte) extendedFacing.getFlip()
+ .getIndex());
+
+ nbt.setString(NBT.VOIDING_MODE, voidingMode.name);
+ nbt.setBoolean(NBT.SEPARATE_INPUTS, separateInputs);
+ nbt.setBoolean(NBT.RECIPE_LOCK, recipeLock);
+ nbt.setBoolean(NBT.BATCH_MODE, batchMode);
+ }
+
+ @Override
+ protected void saveItemLogic(NBTTagCompound nbt) {
+ NBTTagCompound itemInputNBT = controllerItemInput.saveToNBT();
+ nbt.setTag(NBT.INV_INPUT_LIST, itemInputNBT);
+ NBTTagCompound itemOutputNBT = controllerItemOutput.saveToNBT();
+ nbt.setTag(NBT.INV_OUTPUT_LIST, itemOutputNBT);
+ }
+
+ @Override
+ protected void saveFluidLogic(NBTTagCompound nbt) {
+ NBTTagCompound fluidInputNBT = controllerFluidInput.saveToNBT();
+ nbt.setTag(NBT.TANK_IN, fluidInputNBT);
+ NBTTagCompound fluidOutputNBT = controllerFluidOutput.saveToNBT();
+ nbt.setTag(NBT.TANK_OUT, fluidOutputNBT);
+ }
+
+ @Override
+ public void readMultiTileNBT(NBTTagCompound nbt) {
+ super.readMultiTileNBT(nbt);
+
+ // Multiblock inventories are a collection of inventories. The first inventory is the default internal
+ // inventory, and the others are added by inventory extending blocks.
+
+ structureOkay = nbt.getBoolean(NBT.STRUCTURE_OK);
+ extendedFacing = ExtendedFacing
+ .of(getFrontFacing(), Rotation.byIndex(nbt.getByte(NBT.ROTATION)), Flip.byIndex(nbt.getByte(NBT.FLIP)));
+
+ voidingMode = VoidingMode.fromName(nbt.getString(NBT.VOIDING_MODE));
+ separateInputs = nbt.getBoolean(NBT.SEPARATE_INPUTS);
+ recipeLock = nbt.getBoolean(NBT.RECIPE_LOCK);
+ batchMode = nbt.getBoolean(NBT.BATCH_MODE);
+ }
+
+ @Override
+ protected void loadItemLogic(NBTTagCompound nbt) {
+ if (!nbt.hasKey(NBT.INV_INPUT_LIST) && !nbt.hasKey(NBT.INV_OUTPUT_LIST)) {
+ controllerItemInput.addInventory(new ItemInventoryLogic(16));
+ controllerItemOutput.addInventory(new ItemInventoryLogic(16));
+ return;
+ }
+ controllerItemInput.loadFromNBT(nbt.getCompoundTag(NBT.INV_INPUT_LIST));
+ controllerItemOutput.loadFromNBT(nbt.getCompoundTag(NBT.INV_OUTPUT_LIST));
+ }
+
+ @Override
+ protected void loadFluidLogic(NBTTagCompound nbt) {
+ if (!nbt.hasKey(NBT.TANK_IN) && !nbt.hasKey(NBT.TANK_OUT)) {
+ controllerFluidInput.addInventory(new FluidInventoryLogic(16, 32000));
+ controllerFluidOutput.addInventory(new FluidInventoryLogic(16, 32000));
+ return;
+ }
+ controllerFluidInput.loadFromNBT(nbt.getCompoundTag(NBT.TANK_IN));
+ controllerFluidOutput.loadFromNBT(nbt.getCompoundTag(NBT.TANK_OUT));
+ }
+
+ @Override
+ public void addToolTips(List<String> aList, ItemStack aStack, boolean aF3_H) {
+ aList.addAll(Arrays.asList(getDescription()));
+ }
+
+ @Override
+ public String[] getDescription() {
+ if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) {
+ return getTooltip().getStructureInformation();
+ }
+
+ return getTooltip().getInformation();
+ }
+
+ @Override
+ protected void addDebugInfo(EntityPlayer aPlayer, int aLogLevel, ArrayList<String> tList) {
+ super.addDebugInfo(aPlayer, aLogLevel, tList);
+ tList.add("Structure ok: " + checkStructure(false));
+ }
+
+ protected int getToolTipID() {
+ return getMultiTileEntityRegistryID() << 16 + getMultiTileEntityID();
+ }
+
+ protected GT_Multiblock_Tooltip_Builder getTooltip() {
+ GT_Multiblock_Tooltip_Builder builder = tooltip.get(getToolTipID());
+ if (builder == null) {
+ builder = createTooltip();
+ tooltip.put(getToolTipID(), builder);
+ }
+ return builder;
+ }
+
+ @Override
+ public boolean checkStructure(boolean aForceReset) {
+ if (!isServerSide()) return structureOkay;
+
+ // Only trigger an update if forced (from onPostTick, generally), or if the structure has changed
+ if ((structureChanged || aForceReset)) {
+ clearSpecialLists();
+ structureOkay = checkMachine();
+ }
+ structureChanged = false;
+ return structureOkay;
+ }
+
+ @Override
+ public void onStructureChange() {
+ structureChanged = true;
+ }
+
+ public final boolean checkPiece(String piece, Vec3Impl offset) {
+ return checkPiece(piece, offset.get0(), offset.get1(), offset.get2());
+ }
+
+ /**
+ * Explanation of the world coordinate these offset means:
+ * <p>
+ * Imagine you stand in front of the controller, with controller facing towards you not rotated or flipped.
+ * <p>
+ * The horizontalOffset would be the number of blocks on the left side of the controller, not counting controller
+ * itself. The verticalOffset would be the number of blocks on the top side of the controller, not counting
+ * controller itself. The depthOffset would be the number of blocks between you and controller, not counting
+ * controller itself.
+ * <p>
+ * All these offsets can be negative.
+ */
+ protected final boolean checkPiece(String piece, int horizontalOffset, int verticalOffset, int depthOffset) {
+ return getCastedStructureDefinition().check(
+ this,
+ piece,
+ getWorld(),
+ getExtendedFacing(),
+ getXCoord(),
+ getYCoord(),
+ getZCoord(),
+ horizontalOffset,
+ verticalOffset,
+ depthOffset,
+ !structureOkay);
+ }
+
+ public final boolean buildPiece(String piece, ItemStack trigger, boolean hintsOnly, Vec3Impl offset) {
+ return buildPiece(piece, trigger, hintsOnly, offset.get0(), offset.get1(), offset.get2());
+ }
+
+ protected final boolean buildPiece(String piece, ItemStack trigger, boolean hintOnly, int horizontalOffset,
+ int verticalOffset, int depthOffset) {
+ return getCastedStructureDefinition().buildOrHints(
+ this,
+ trigger,
+ piece,
+ getWorld(),
+ getExtendedFacing(),
+ getXCoord(),
+ getYCoord(),
+ getZCoord(),
+ horizontalOffset,
+ verticalOffset,
+ depthOffset,
+ hintOnly);
+ }
+
+ protected final int survivalBuildPiece(String piece, ItemStack trigger, Vec3Impl offset, int elementBudget,
+ ISurvivalBuildEnvironment env, boolean check) {
+ return survivalBuildPiece(
+ piece,
+ trigger,
+ offset.get0(),
+ offset.get1(),
+ offset.get2(),
+ elementBudget,
+ env,
+ check);
+ }
+
+ protected final Integer survivalBuildPiece(String piece, ItemStack trigger, int horizontalOffset,
+ int verticalOffset, int depthOffset, int elementBudget, ISurvivalBuildEnvironment env, boolean check) {
+ return getCastedStructureDefinition().survivalBuild(
+ this,
+ trigger,
+ piece,
+ getWorld(),
+ getExtendedFacing(),
+ getXCoord(),
+ getYCoord(),
+ getZCoord(),
+ horizontalOffset,
+ verticalOffset,
+ depthOffset,
+ elementBudget,
+ env,
+ check);
+ }
+
+ @SuppressWarnings("unchecked")
+ private IStructureDefinition<Controller<C, P>> getCastedStructureDefinition() {
+ return (IStructureDefinition<Controller<C, P>>) getStructureDefinition();
+ }
+
+ @Override
+ public ExtendedFacing getExtendedFacing() {
+ return extendedFacing;
+ }
+
+ @Override
+ public void setExtendedFacing(ExtendedFacing newExtendedFacing) {
+ if (extendedFacing == newExtendedFacing) {
+ return;
+ }
+
+ onStructureChange();
+ if (structureOkay) stopMachine(false);
+ extendedFacing = newExtendedFacing;
+ structureOkay = false;
+ if (isServerSide()) {
+ StructureLibAPI.sendAlignment(
+ this,
+ new NetworkRegistry.TargetPoint(
+ getWorld().provider.dimensionId,
+ getXCoord(),
+ getYCoord(),
+ getZCoord(),
+ 512));
+ } else {
+ issueTextureUpdate();
+ }
+
+ }
+
+ @Override
+ public boolean onWrenchRightClick(EntityPlayer aPlayer, ItemStack tCurrentItem, ForgeDirection wrenchSide, float aX,
+ float aY, float aZ, ItemStack aTool) {
+ if (wrenchSide != getFrontFacing())
+ return super.onWrenchRightClick(aPlayer, tCurrentItem, wrenchSide, aX, aY, aZ);
+ if (aPlayer.isSneaking()) {
+ // we won't be allowing horizontal flips, as it can be perfectly emulated by rotating twice and flipping
+ // horizontally allowing an extra round of flip make it hard to draw meaningful flip markers in
+ // GT_Proxy#drawGrid
+ toolSetFlip(getFlip().isHorizontallyFlipped() ? Flip.NONE : Flip.HORIZONTAL);
+ } else {
+ toolSetRotation(null);
+ }
+ return true;
+ }
+
+ @Override
+ public void registerCoveredPartOnSide(final ForgeDirection side, IMultiBlockPart part) {
+ if (side == ForgeDirection.UNKNOWN) return;
+
+ final List<WeakTargetRef<IMultiBlockPart>> registeredCovers = registeredCoveredParts.get(side.ordinal());
+ // TODO: Make sure that we're not already registered on this side
+ registeredCovers.add(new WeakTargetRef<>(part, true));
+ }
+
+ @Override
+ public void unregisterCoveredPartOnSide(final ForgeDirection side, IMultiBlockPart aPart) {
+ if (side == ForgeDirection.UNKNOWN) return;
+
+ final List<WeakTargetRef<IMultiBlockPart>> coveredParts = registeredCoveredParts.get(side.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> it = coveredParts.listIterator();
+ while (it.hasNext()) {
+ final IMultiBlockPart part = (it.next()).get();
+ if (part == null || part == aPart) it.remove();
+ }
+ }
+
+ @Override
+ public void registerCaseWithPurpose(MultiTileCasingPurpose purpose, IMultiBlockPart part) {
+ final List<WeakTargetRef<IMultiBlockPart>> tickableParts = registeredTickableParts.get(purpose.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> it = tickableParts.listIterator();
+ while (it.hasNext()) {
+ final IMultiBlockPart next = (it.next()).get();
+ if (next == null) {
+ it.remove();
+ } else if (next == part) {
+ return;
+ }
+ }
+ tickableParts.add(new WeakTargetRef<>(part, true));
+ }
+
+ @Override
+ public void unregisterCaseWithPurpose(MultiTileCasingPurpose purpose, IMultiBlockPart part) {
+ final List<WeakTargetRef<IMultiBlockPart>> tickableParts = registeredTickableParts.get(purpose.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> it = tickableParts.listIterator();
+ while (it.hasNext()) {
+ final IMultiBlockPart next = (it.next()).get();
+ if (next == null || next == part) it.remove();
+ }
+ }
+
+ @Override
+ public void onFirstTick(boolean isServerSide) {
+ super.onFirstTick(isServerSide);
+ if (isServerSide) {
+ checkStructure(true);
+ } else {
+ StructureLibAPI.queryAlignment(this);
+ }
+ }
+
+ private boolean tickCovers() {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ // TODO: Tick controller covers, if any
+ final List<WeakTargetRef<IMultiBlockPart>> coveredParts = this.registeredCoveredParts.get(side.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> it = coveredParts.listIterator();
+ while (it.hasNext()) {
+ final IMultiBlockPart part = (it.next()).get();
+ if (part == null) {
+ it.remove();
+ continue;
+ }
+ if (!part.tickCoverAtSide(side, mTickTimer)) it.remove();
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onTick(long tick, boolean isServerSide) {
+ tickCovers();
+ }
+
+ @Override
+ public void onPostTick(long tick, boolean isServerSide) {
+ if (!isServerSide) { // client side
+ doActivitySound(getActivitySoundLoop());
+ return;
+ }
+
+ // server side
+ if (tick % 600 == 5) {
+ // Recheck the structure every 30 seconds or so
+ if (!checkStructure(false)) checkStructure(true);
+ }
+ if (checkStructure(false)) {
+ runMachine(tick);
+ pushItemOutputs(tick);
+ pushFluidOutputs(tick);
+
+ } else {
+ stopMachine(false);
+ }
+
+ }
+
+ protected void pushItemOutputs(long tick) {
+ if (tick % AUTO_OUTPUT_FREQUENCY_TICK != 0) return;
+ final List<WeakTargetRef<IMultiBlockPart>> registeredItemOutputs = registeredTickableParts
+ .get(MultiTileCasingPurpose.ItemOutput.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> itemOutputIterator = registeredItemOutputs.listIterator();
+ while (itemOutputIterator.hasNext()) {
+ final IMultiBlockPart part = (itemOutputIterator.next()).get();
+ if (part == null) {
+ itemOutputIterator.remove();
+ continue;
+ }
+ if (!part.shouldTick(mTickTimer)) {
+ itemOutputIterator.remove();
+ continue;
+ }
+
+ final IInventory facingInventory = part.getIInventoryAtSide(part.getFrontFacing());
+ if (facingInventory == null) {
+ continue;
+ }
+
+ moveMultipleItemStacks(
+ part,
+ facingInventory,
+ part.getFrontFacing(),
+ part.getBackFacing(),
+ null,
+ false,
+ (byte) 64,
+ (byte) 1,
+ (byte) 64,
+ (byte) 1,
+ part.getSizeInventory());
+ for (int i = 0; i < part.getSizeInventory(); i++) {
+ final ItemStack stack = part.getStackInSlot(i);
+ if (stack != null && stack.stackSize <= 0) {
+ part.setInventorySlotContents(i, null);
+ }
+ }
+
+ }
+ }
+
+ protected void pushFluidOutputs(long tick) {
+ if (tick % AUTO_OUTPUT_FREQUENCY_TICK != 0) return;
+ final List<WeakTargetRef<IMultiBlockPart>> registeredFluidOutputs = registeredTickableParts
+ .get(MultiTileCasingPurpose.FluidOutput.ordinal());
+ final Iterator<WeakTargetRef<IMultiBlockPart>> fluidOutputIterator = registeredFluidOutputs.listIterator();
+ while (fluidOutputIterator.hasNext()) {
+ final IMultiBlockPart part = (fluidOutputIterator.next()).get();
+ if (part == null) {
+ fluidOutputIterator.remove();
+ continue;
+ }
+ if (!part.shouldTick(mTickTimer)) {
+ fluidOutputIterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public void setCleanroom(boolean cleanroom) {
+ isCleanroom = cleanroom;
+ }
+
+ protected void clearSpecialLists() {
+ upgradeCasings.clear();
+ functionalCasings.clear();
+ }
+
+ @Override
+ public final boolean isFacingValid(ForgeDirection facing) {
+ return canSetToDirectionAny(facing);
+ }
+
+ @Override
+ public void onFacingChange() {
+ toolSetDirection(getFrontFacing());
+ onStructureChange();
+ }
+
+ @Override
+ public boolean allowCoverOnSide(ForgeDirection side, GT_ItemStack aCoverID) {
+ return side != facing;
+ }
+
+ @Override
+ public String[] getStructureDescription(ItemStack stackSize) {
+ return getTooltip().getStructureHint();
+ }
+
+ @Override
+ public IAlignmentLimits getAlignmentLimits() {
+ return limits;
+ }
+
+ protected void setAlignmentLimits(IAlignmentLimits mLimits) {
+ this.limits = mLimits;
+ }
+
+ public boolean isSeparateInputs() {
+ return separateInputs;
+ }
+
+ public void setSeparateInputs(boolean aSeparateInputs) {
+ separateInputs = aSeparateInputs;
+ }
+
+ protected IAlignmentLimits getInitialAlignmentLimits() {
+ return (d, r, f) -> !f.isVerticallyFliped();
+ }
+
+ public static class BuildState {
+
+ /**
+ * Utility class to keep track of the build state of a multiblock
+ */
+ boolean building = false;
+
+ Vec3Impl currentOffset;
+
+ public void startBuilding(Vec3Impl structureOffset) {
+ if (building) throw new IllegalStateException("Already building!");
+ building = true;
+ setCurrentOffset(structureOffset);
+ }
+
+ public Vec3Impl setCurrentOffset(Vec3Impl structureOffset) {
+ verifyBuilding();
+ return (currentOffset = structureOffset);
+ }
+
+ private void verifyBuilding() {
+ if (!building) throw new IllegalStateException("Not building!");
+ }
+
+ public boolean failBuilding() {
+ building = false;
+ currentOffset = null;
+ return false;
+ }
+
+ public Vec3Impl stopBuilding() {
+ final Vec3Impl toReturn = getCurrentOffset();
+ building = false;
+ currentOffset = null;
+
+ return toReturn;
+ }
+
+ public Vec3Impl getCurrentOffset() {
+ verifyBuilding();
+ return currentOffset;
+ }
+
+ public Vec3Impl addOffset(Vec3Impl offset) {
+ verifyBuilding();
+ return setCurrentOffset(currentOffset.add(offset));
+ }
+ }
+
+ public void registerSpecialCasings(MultiBlockPart part) {
+ if (part instanceof UpgradeCasing upgradeCasing) {
+ upgradeCasings.add(new WeakTargetRef<>(upgradeCasing, true));
+ }
+ if (part instanceof FunctionalCasing functionalCasing) {
+ functionalCasings.add(new WeakTargetRef<>(functionalCasing, true));
+ }
+ }
+
+ // #region Fluid - MultiBlock related Fluid Tank behaviour.
+
+ @Override
+ @Nullable
+ public FluidInventoryLogic getFluidLogic(@Nonnull ForgeDirection side, @Nonnull InventoryType type) {
+ if (side == facing) return null;
+ return switch (type) {
+ case Input -> controllerFluidInput.getAllInventoryLogics();
+ case Output -> controllerFluidOutput.getAllInventoryLogics();
+ default -> null;
+ };
+ }
+
+ @Override
+ @Nonnull
+ public @NotNull FluidInventoryLogic getFluidLogic(@Nonnull InventoryType type, @Nullable UUID id) {
+ return switch (type) {
+ case Input -> controllerFluidInput.getInventoryLogic(id);
+ case Output -> controllerFluidOutput.getInventoryLogic(id);
+ default -> throw new IllegalStateException("Unexpected value: " + type);
+ };
+ }
+
+ @Override
+ @Nonnull
+ public UUID registerFluidInventory(int tanks, long capacity, int tier, @Nonnull InventoryType type,
+ boolean isUpgradeInventory) {
+ return switch (type) {
+ case Input -> controllerFluidInput
+ .addInventory(new FluidInventoryLogic(tanks, capacity, tier, isUpgradeInventory));
+ case Output -> controllerFluidOutput
+ .addInventory(new FluidInventoryLogic(tanks, capacity, tier, isUpgradeInventory));
+ case Both -> {
+ UUID id = controllerFluidInput
+ .addInventory(new FluidInventoryLogic(tanks, capacity, tier, isUpgradeInventory));
+ controllerFluidOutput
+ .addInventory(id, new FluidInventoryLogic(tanks, capacity, tier, isUpgradeInventory));
+ yield id;
+ }
+ };
+ }
+
+ @Override
+ @Nonnull
+ public FluidInventoryLogic unregisterFluidInventory(@Nonnull UUID id, @Nonnull InventoryType type) {
+ return switch (type) {
+ case Input -> controllerFluidInput.removeInventory(id);
+ case Output -> controllerFluidOutput.removeInventory(id);
+ case Both -> {
+ FluidInventoryLogic input = controllerFluidInput.removeInventory(id);
+ FluidInventoryLogic output = controllerFluidOutput.removeInventory(id);
+ yield new FluidInventoryLogic(
+ Stream.of(input, output)
+ .map(FluidInventoryLogic::getInventory)
+ .collect(Collectors.toList()));
+ }
+ };
+ }
+
+ @Override
+ public void changeFluidInventoryDisplayName(@Nullable UUID id, @Nullable String displayName,
+ @Nonnull InventoryType type) {
+ switch (type) {
+ case Input:
+ controllerFluidInput.setInventoryDisplayName(id, displayName);
+ break;
+ case Output:
+ controllerFluidOutput.setInventoryDisplayName(id, displayName);
+ break;
+ case Both:
+ controllerFluidInput.setInventoryDisplayName(id, displayName);
+ controllerFluidOutput.setInventoryDisplayName(id, displayName);
+ break;
+ }
+ }
+
+ // #endregion Fluid
+
+ // #region Item - MultiBlock related Item behaviour.
+
+ @Override
+ @Nullable
+ public ItemInventoryLogic getItemLogic(@Nonnull ForgeDirection side, @Nonnull InventoryType type) {
+ if (side == facing) return null;
+ return switch (type) {
+ case Input -> controllerItemInput.getAllInventoryLogics();
+ case Output -> controllerItemOutput.getAllInventoryLogics();
+ default -> null;
+ };
+ }
+
+ @Override
+ @Nonnull
+ public ItemInventoryLogic getItemLogic(@Nonnull InventoryType type, @Nullable UUID id) {
+ return switch (type) {
+ case Input -> controllerItemInput.getInventoryLogic(id);
+ case Output -> controllerItemOutput.getInventoryLogic(id);
+ default -> throw new IllegalStateException("Unexpected value: " + type);
+ };
+ }
+
+ @Override
+ @Nonnull
+ public UUID registerItemInventory(int slots, int tier, @Nonnull InventoryType type, boolean isUpgradeInventory) {
+ return switch (type) {
+ case Input -> controllerItemInput.addInventory(new ItemInventoryLogic(slots, tier, isUpgradeInventory));
+ case Output -> controllerItemOutput.addInventory(new ItemInventoryLogic(slots, tier, isUpgradeInventory));
+ case Both -> {
+ UUID id = controllerItemInput.addInventory(new ItemInventoryLogic(slots, tier, isUpgradeInventory));
+ controllerItemOutput.addInventory(id, new ItemInventoryLogic(slots, tier, isUpgradeInventory));
+ yield id;
+ }
+ };
+ }
+
+ @Override
+ public ItemInventoryLogic unregisterItemInventory(@Nonnull UUID id, @Nonnull InventoryType type) {
+ return switch (type) {
+ case Input -> controllerItemInput.removeInventory(id);
+ case Output -> controllerItemOutput.removeInventory(id);
+ case Both -> {
+ ItemInventoryLogic input = controllerItemInput.removeInventory(id);
+ ItemInventoryLogic output = controllerItemOutput.removeInventory(id);
+ yield new ItemInventoryLogic(
+ Arrays.asList(input, output)
+ .stream()
+ .map(inv -> inv.getInventory())
+ .collect(Collectors.toList()));
+ }
+ };
+ }
+
+ @Override
+ public void changeItemInventoryDisplayName(@Nullable UUID id, @Nullable String displayName,
+ @Nonnull InventoryType type) {
+ switch (type) {
+ case Input:
+ controllerItemInput.setInventoryDisplayName(id, displayName);
+ break;
+ case Output:
+ controllerItemOutput.setInventoryDisplayName(id, displayName);
+ break;
+ case Both:
+ controllerItemInput.setInventoryDisplayName(id, displayName);
+ controllerItemOutput.setInventoryDisplayName(id, displayName);
+ break;
+ }
+ }
+
+ // #endregion Item
+
+ // #region Energy
+
+ @Nonnull
+ @Override
+ public PowerLogic getPowerLogic() {
+ return getPowerLogic(ForgeDirection.UNKNOWN);
+ }
+
+ // #endregion Energy
+
+ @Override
+ protected void updateSlots() {
+ controllerItemInput.getAllInventoryLogics()
+ .update(shouldSort);
+ controllerItemOutput.getAllInventoryLogics()
+ .update(shouldSort);
+ controllerFluidInput.getAllInventoryLogics()
+ .update();
+ controllerFluidOutput.getAllInventoryLogics()
+ .update();
+ }
+
+ /*
+ * GUI Work - Multiblock GUI related methods
+ */
+ @Override
+ public boolean useModularUI() {
+ return true;
+ }
+
+ @Override
+ public boolean hasGui(ForgeDirection side) {
+ return true;
+ }
+
+ @Override
+ protected void addTitleTextStyle(ModularWindow.Builder builder, String title) {
+ // leave empty
+ }
+
+ @Override
+ public boolean supportsVoidProtection() {
+ return true;
+ }
+
+ @Override
+ public VoidingMode getVoidingMode() {
+ return voidingMode;
+ }
+
+ @Override
+ public void setVoidingMode(VoidingMode mode) {
+ this.voidingMode = mode;
+ }
+
+ @Override
+ public boolean canDumpItemToME() {
+ return false;
+ }
+
+ @Override
+ public boolean canDumpFluidToME() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsInputSeparation() {
+ return true;
+ }
+
+ @Override
+ public boolean isInputSeparated() {
+ return separateInputs;
+ }
+
+ @Override
+ public void setInputSeparation(Boolean enabled) {
+ this.separateInputs = enabled;
+ }
+
+ @Override
+ public boolean supportsBatchMode() {
+ return true;
+ }
+
+ @Override
+ public boolean isBatchModeEnabled() {
+ return batchMode;
+ }
+
+ @Override
+ public void setBatchMode(Boolean mode) {
+ this.batchMode = mode;
+ }
+
+ @Override
+ public boolean supportsSingleRecipeLocking() {
+ return false;
+ }
+
+ @Override
+ public boolean isRecipeLockingEnabled() {
+ return recipeLock;
+ }
+
+ @Override
+ public void setRecipeLocking(Boolean enabled) {
+ this.recipeLock = enabled;
+ }
+
+ @Override
+ public void getWailaNBTData(EntityPlayerMP player, TileEntity tile, NBTTagCompound tag, World world, int x, int y,
+ int z) {
+ super.getWailaNBTData(player, tile, tag, world, x, y, z);
+ P processing = getProcessingLogic();
+ tag.setInteger("progress", processing.getProgress());
+ tag.setInteger("maxProgress", processing.getDuration());
+ tag.setBoolean("structureOkay", structureOkay);
+ tag.setBoolean("isActive", isActive());
+ if (isActive()) {
+ tag.setLong("energyUsage", getProcessingLogic().getCalculatedEut());
+ tag.setLong("energyTier", tier);
+ }
+ }
+
+ @Override
+ public void getWailaBody(ItemStack itemStack, List<String> currentTip, IWailaDataAccessor accessor,
+ IWailaConfigHandler config) {
+ super.getWailaBody(itemStack, currentTip, accessor, config);
+ final NBTTagCompound tag = accessor.getNBTData();
+ if (!tag.getBoolean("structureOkay")) {
+ currentTip.add(RED + "** INCOMPLETE STRUCTURE **" + RESET);
+ } else {
+ currentTip.add((GREEN + "Running Fine") + RESET);
+ }
+ if (isSimpleMachine) {
+ boolean isActive = tag.getBoolean("isActive");
+ currentTip.add(
+ GT_Waila.getMachineProgressString(isActive, tag.getInteger("maxProgress"), tag.getInteger("progress")));
+ }
+ boolean isActive = tag.getBoolean("isActive");
+ if (isActive) {
+ long energyTier = tag.getLong("energyTier");
+ long actualEnergyUsage = tag.getLong("energyUsage");
+ if (actualEnergyUsage > 0) {
+ currentTip.add(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.waila.energy.use_with_amperage",
+ GT_Utility.formatNumbers(actualEnergyUsage),
+ GT_Utility.getAmperageForTier(actualEnergyUsage, (byte) energyTier),
+ GT_Utility.getColoredTierNameFromTier((byte) energyTier)));
+ } else if (actualEnergyUsage < 0) {
+ currentTip.add(
+ StatCollector.translateToLocalFormatted(
+ "GT5U.waila.energy.produce_with_amperage",
+ GT_Utility.formatNumbers(-actualEnergyUsage),
+ GT_Utility.getAmperageForTier(-actualEnergyUsage, (byte) energyTier),
+ GT_Utility.getColoredTierNameFromTier((byte) energyTier)));
+ }
+ }
+ }
+
+ @Override
+ public GT_Packet_MultiTileEntity getClientDataPacket() {
+ final GT_Packet_MultiTileEntity packet = super.getClientDataPacket();
+
+ return packet;
+
+ }
+
+ @Override
+ public void enableWorking() {
+ super.enableWorking();
+ if (!structureOkay) {
+ checkStructure(true);
+ }
+ }
+
+ @Override
+ public List<ItemStack> getItemOutputSlots(ItemStack[] toOutput) {
+ return new ArrayList<>(0);
+ }
+
+ @Override
+ public List<? extends IFluidStore> getFluidOutputSlots(FluidStack[] toOutput) {
+ return new ArrayList<>(0);
+ }
+
+ @Override
+ @Nonnull
+ public Set<Entry<UUID, FluidInventoryLogic>> getAllFluidInventoryLogics(@Nonnull InventoryType type) {
+ return switch (type) {
+ case Input -> controllerFluidInput.getAllInventoryLogicsAsEntrySet();
+ case Output -> controllerFluidOutput.getAllInventoryLogicsAsEntrySet();
+ default -> super.getAllFluidInventoryLogics(type);
+ };
+ }
+
+ @Override
+ @Nonnull
+ public Set<Entry<UUID, ItemInventoryLogic>> getAllItemInventoryLogics(@Nonnull InventoryType type) {
+ return switch (type) {
+ case Input -> controllerItemInput.getAllInventoryLogicsAsEntrySet();
+ case Output -> controllerItemOutput.getAllInventoryLogicsAsEntrySet();
+ default -> super.getAllItemInventoryLogics(type);
+ };
+ }
+
+ @Override
+ public void setWirelessSupport(boolean canUse) {
+ if (canUse) {
+ strongCheckOrAddUser(getOwnerUuid());
+ }
+ power.setCanUseWireless(canUse, getOwnerUuid());
+ }
+
+ @Override
+ public void setLaserSupport(boolean canUse) {
+ power.setCanUseLaser(canUse);
+ }
+
+ @Override
+ public void setMaxAmperage(long amperage) {
+ power.setMaxAmperage(amperage);
+ }
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/MultiBlockPart.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/MultiBlockPart.java
new file mode 100644
index 0000000000..5a16ed4b38
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/MultiBlockPart.java
@@ -0,0 +1,693 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import static com.google.common.math.LongMath.log2;
+import static gregtech.api.enums.GT_Values.B;
+import static gregtech.api.enums.Textures.BlockIcons.FLUID_IN_SIGN;
+import static gregtech.api.enums.Textures.BlockIcons.FLUID_OUT_SIGN;
+import static gregtech.api.enums.Textures.BlockIcons.ITEM_IN_SIGN;
+import static gregtech.api.enums.Textures.BlockIcons.ITEM_OUT_SIGN;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_ENERGY_IN_MULTI;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_ENERGY_OUT_MULTI;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_PIPE_IN;
+import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_PIPE_OUT;
+
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.ChunkCoordinates;
+import net.minecraft.util.StatCollector;
+import net.minecraftforge.common.util.ForgeDirection;
+import net.minecraftforge.fluids.Fluid;
+
+import com.gtnewhorizons.modularui.api.screen.ModularWindow.Builder;
+import com.gtnewhorizons.modularui.api.screen.UIBuildContext;
+import com.gtnewhorizons.modularui.common.widget.DrawableWidget;
+
+import gregtech.api.enums.GT_Values.NBT;
+import gregtech.api.enums.InventoryType;
+import gregtech.api.fluid.FluidTankGT;
+import gregtech.api.gui.GUIHost;
+import gregtech.api.gui.GUIProvider;
+import gregtech.api.interfaces.ITexture;
+import gregtech.api.logic.FluidInventoryLogic;
+import gregtech.api.logic.ItemInventoryLogic;
+import gregtech.api.logic.NullPowerLogic;
+import gregtech.api.logic.PowerLogic;
+import gregtech.api.logic.interfaces.PowerLogicHost;
+import gregtech.api.multitileentity.MultiTileEntityRegistry;
+import gregtech.api.multitileentity.WeakTargetRef;
+import gregtech.api.multitileentity.base.NonTickableMultiTileEntity;
+import gregtech.api.multitileentity.enums.MultiTileCasingPurpose;
+import gregtech.api.multitileentity.interfaces.IMultiBlockController;
+import gregtech.api.multitileentity.interfaces.IMultiBlockPart;
+import gregtech.api.render.TextureFactory;
+import gregtech.api.util.GT_Utility;
+import gregtech.common.covers.CoverInfo;
+import gregtech.common.gui.PartGUIProvider;
+import mcp.mobius.waila.api.IWailaConfigHandler;
+import mcp.mobius.waila.api.IWailaDataAccessor;
+
+public abstract class MultiBlockPart extends NonTickableMultiTileEntity
+ implements IMultiBlockPart, PowerLogicHost, GUIHost {
+
+ public static final int NOTHING = 0, ENERGY_IN = B[0], ENERGY_OUT = B[1], FLUID_IN = B[2], FLUID_OUT = B[3],
+ ITEM_IN = B[4], ITEM_OUT = B[5];
+
+ protected final List<Integer> BASIC_MODES = new ArrayList<>(
+ Arrays.asList(NOTHING, ENERGY_IN, ENERGY_OUT, FLUID_IN, FLUID_OUT, ITEM_IN, ITEM_OUT));
+
+ protected Set<MultiTileCasingPurpose> registeredPurposes = new HashSet<>();
+
+ protected final WeakTargetRef<IMultiBlockController> controller = new WeakTargetRef<>(
+ IMultiBlockController.class,
+ false);
+
+ protected int allowedModes = NOTHING; // BITMASK - Modes allowed for this part
+ protected int mode = 0; // Mode selected for this part
+
+ protected UUID lockedInventory;
+ protected int mLockedInventoryIndex = 0;
+ protected FluidTankGT configurationTank = new FluidTankGT();
+
+ @Nonnull
+ protected final GUIProvider<?> guiProvider = createGUIProvider();
+
+ /**
+ * What Part Tier is this part? All Basic Casings are Tier 1, and will allow: Energy, Item, Fluid input/output. Some
+ * of the more advanced modes can be set to require a higher tier part.
+ */
+ public int getPartTier() {
+ return 1;
+ }
+
+ @Override
+ public UUID getLockedInventory() {
+ return lockedInventory;
+ }
+
+ public void setTarget(IMultiBlockController newController, int aAllowedModes) {
+ final IMultiBlockController currentController = getTarget(false);
+ if (currentController != null && currentController != newController) {
+ for (MultiTileCasingPurpose purpose : registeredPurposes) {
+ unregisterPurpose(purpose);
+ }
+ }
+
+ allowedModes = aAllowedModes;
+ if (newController != currentController) {
+ registerCovers(newController);
+ registerPurposes();
+ }
+ }
+
+ protected void registerPurpose(MultiTileCasingPurpose purpose) {
+ IMultiBlockController target = getTarget(false);
+ if (target != null) {
+ target.registerCaseWithPurpose(purpose, this);
+ registeredPurposes.add(purpose);
+ }
+ }
+
+ protected void unregisterPurpose(MultiTileCasingPurpose purpose) {
+ IMultiBlockController target = getTarget(false);
+ if (target != null) {
+ target.unregisterCaseWithPurpose(purpose, this);
+ }
+ registeredPurposes.remove(purpose);
+ }
+
+ @Override
+ protected void addDebugInfo(EntityPlayer aPlayer, int aLogLevel, ArrayList<String> tList) {
+ final IMultiBlockController controller = getTarget(false);
+ if (controller != null) {
+ tList.add("Has controller");
+ } else {
+ tList.add("No Controller");
+ }
+ tList.add("Casing Mode: " + getModeName(mode));
+ }
+
+ @Override
+ public void getWailaBody(ItemStack itemStack, List<String> currentTip, IWailaDataAccessor accessor,
+ IWailaConfigHandler config) {
+ super.getWailaBody(itemStack, currentTip, accessor, config);
+ currentTip.add(String.format("Mode: %s", getModeName(mode)));
+ if (modeSelected(FLUID_OUT)) {
+ if (configurationTank != null && configurationTank.get() != null) {
+ currentTip.add(
+ String.format(
+ "Locked to: %s",
+ configurationTank.get()
+ .getLocalizedName()));
+ } else {
+ currentTip.add("Locked to: Nothing");
+ }
+ }
+ }
+
+ public IMultiBlockController getTarget(boolean aCheckValidity) {
+ final IMultiBlockController res = controller.get();
+ if (res != null && aCheckValidity) {
+ return res.checkStructure(false) ? res : null;
+ }
+ return res;
+ }
+
+ public void registerCovers(IMultiBlockController controller) {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ final CoverInfo coverInfo = getCoverInfoAtSide(side);
+ if (coverInfo.isValid() && coverInfo.getTickRate() > 0) {
+ controller.registerCoveredPartOnSide(side, this);
+ }
+ }
+ }
+
+ protected void registerPurposes() {
+ for (MultiTileCasingPurpose purpose : registeredPurposes) {
+ registerPurpose(purpose);
+ }
+ }
+
+ @Override
+ public void setCoverItemAtSide(ForgeDirection side, ItemStack aCover) {
+ super.setCoverItemAtSide(side, aCover);
+ // TODO: Filter on tickable covers
+ final IMultiBlockController tTarget = getTarget(true);
+ if (tTarget == null) {
+ return;
+ }
+
+ final CoverInfo coverInfo = getCoverInfoAtSide(side);
+ if (coverInfo.isValid() && coverInfo.getTickRate() > 0) {
+ tTarget.registerCoveredPartOnSide(side, this);
+ }
+
+ }
+
+ public void unregisterCovers(IMultiBlockController controller) {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ if (getCoverInfoAtSide(side).isValid()) {
+ controller.unregisterCoveredPartOnSide(side, this);
+ }
+ }
+ }
+
+ @Override
+ public boolean dropCover(ForgeDirection side, ForgeDirection droppedSide, boolean aForced) {
+ final boolean res = super.dropCover(side, droppedSide, aForced);
+ final IMultiBlockController tTarget = getTarget(true);
+ if (tTarget != null) {
+ tTarget.unregisterCoveredPartOnSide(side, this);
+ }
+ return res;
+ }
+
+ @Override
+ public void readMultiTileNBT(NBTTagCompound aNBT) {
+ if (aNBT.hasKey(NBT.ALLOWED_MODES)) allowedModes = aNBT.getInteger(NBT.ALLOWED_MODES);
+ if (aNBT.hasKey(NBT.MODE)) setMode(aNBT.getByte(NBT.MODE));
+ if (aNBT.hasKey(NBT.TARGET)) {
+ controller
+ .setPosition(aNBT.getInteger(NBT.TARGET_X), aNBT.getShort(NBT.TARGET_Y), aNBT.getInteger(NBT.TARGET_Z));
+ controller.setWorld(worldObj);
+ }
+ if (aNBT.hasKey(NBT.LOCKED_INVENTORY)) {
+ lockedInventory = UUID.fromString(aNBT.getString(NBT.LOCKED_INVENTORY));
+ }
+ if (aNBT.hasKey(NBT.LOCKED_INVENTORY_INDEX)) {
+ mLockedInventoryIndex = aNBT.getInteger(NBT.LOCKED_INVENTORY_INDEX);
+ }
+ if (aNBT.hasKey(NBT.LOCKED_FLUID)) {
+ configurationTank.readFromNBT(aNBT, NBT.LOCKED_FLUID);
+ }
+ if (modeSelected(ITEM_OUT)) {
+ registeredPurposes.add(MultiTileCasingPurpose.ItemOutput);
+ }
+ if (modeSelected(FLUID_OUT)) {
+ registeredPurposes.add(MultiTileCasingPurpose.FluidOutput);
+ }
+ }
+
+ @Override
+ public void writeMultiTileNBT(NBTTagCompound nbt) {
+ if (allowedModes != NOTHING) nbt.setInteger(NBT.ALLOWED_MODES, allowedModes);
+ if (mode != 0) nbt.setInteger(NBT.MODE, mode);
+
+ final ChunkCoordinates pos = controller.getPosition();
+ if (pos.posY >= 0) {
+ // Valid position
+ nbt.setBoolean(NBT.TARGET, true);
+ nbt.setInteger(NBT.TARGET_X, pos.posX);
+ nbt.setShort(NBT.TARGET_Y, (short) pos.posY);
+ nbt.setInteger(NBT.TARGET_Z, pos.posZ);
+ }
+
+ if (lockedInventory != null) {
+ nbt.setString(NBT.LOCKED_INVENTORY, lockedInventory.toString());
+ }
+ if (mLockedInventoryIndex != 0) {
+ nbt.setInteger(NBT.LOCKED_INVENTORY_INDEX, mLockedInventoryIndex);
+ }
+ configurationTank.writeToNBT(nbt, NBT.LOCKED_FLUID);
+ }
+
+ @Override
+ public void setLockedInventoryIndex(int index) {
+ mLockedInventoryIndex = index;
+ }
+
+ @Override
+ public int getLockedInventoryIndex() {
+ return mLockedInventoryIndex;
+ }
+
+ @Override
+ public ChunkCoordinates getTargetPos() {
+ return controller.getPosition();
+ }
+
+ @Override
+ public void setMode(int mode) {
+ if (this.mode == mode) return;
+ if (modeSelected(FLUID_OUT)) {
+ unregisterPurpose(MultiTileCasingPurpose.FluidOutput);
+ }
+ if (modeSelected(ITEM_OUT)) {
+ unregisterPurpose(MultiTileCasingPurpose.ItemOutput);
+ }
+ this.mode = mode;
+ if (modeSelected(FLUID_OUT)) {
+ registerPurpose(MultiTileCasingPurpose.FluidOutput);
+ }
+ if (modeSelected(ITEM_OUT)) {
+ registerPurpose(MultiTileCasingPurpose.ItemOutput);
+ }
+ }
+
+ @Override
+ public int getMode() {
+ return mode;
+ }
+
+ @Override
+ public int getAllowedModes() {
+ return allowedModes;
+ }
+
+ @Override
+ public void setAllowedModes(int aAllowedModes) {
+ allowedModes = aAllowedModes;
+ }
+
+ /**
+ * True if `mode` is one of the allowed modes
+ */
+ public boolean hasMode(int mode) {
+ // This is not sent to the client
+ return (allowedModes & mode) != 0;
+ }
+
+ /**
+ * Returns true if the part has any of the modes provided, and that mode is the currently selected mode
+ */
+ public boolean modeSelected(int... modes) {
+ for (int mode : modes) {
+ if (hasMode(mode) && this.mode == getModeOrdinal(mode)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onBlockBroken() {
+ final IMultiBlockController tTarget = getTarget(false);
+ if (tTarget != null) {
+ unregisterCovers(tTarget);
+ tTarget.onStructureChange();
+ }
+ return false;
+ }
+
+ @Override
+ public void onBlockAdded() {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ final TileEntity te = getTileEntityAtSide(side);
+ if (te instanceof MultiBlockPart part) {
+ final IMultiBlockController tController = part.getTarget(false);
+ if (tController != null) tController.onStructureChange();
+ } else if (te instanceof IMultiBlockController controller) {
+ controller.onStructureChange();
+ }
+ }
+ }
+
+ @Override
+ public ITexture getTexture(ForgeDirection side) {
+ ITexture texture = super.getTexture(side);
+ if (mode != 0 && side == facing) {
+ if (mode == getModeOrdinal(ITEM_IN)) {
+ return TextureFactory.of(
+ texture,
+ TextureFactory.of(OVERLAY_PIPE_IN),
+ TextureFactory.of(ITEM_IN_SIGN),
+ getCoverTexture(side));
+ }
+ if (mode == getModeOrdinal(ITEM_OUT)) {
+ return TextureFactory.of(
+ texture,
+ TextureFactory.of(OVERLAY_PIPE_OUT),
+ TextureFactory.of(ITEM_OUT_SIGN),
+ getCoverTexture(side));
+ }
+ if (mode == getModeOrdinal(FLUID_IN)) {
+ return TextureFactory.of(
+ texture,
+ TextureFactory.of(OVERLAY_PIPE_IN),
+ TextureFactory.of(FLUID_IN_SIGN),
+ getCoverTexture(side));
+ }
+ if (mode == getModeOrdinal(FLUID_OUT)) {
+ return TextureFactory.of(
+ texture,
+ TextureFactory.of(OVERLAY_PIPE_OUT),
+ TextureFactory.of(FLUID_OUT_SIGN),
+ getCoverTexture(side));
+ }
+ if (mode == getModeOrdinal(ENERGY_IN)) {
+ return TextureFactory.of(texture, TextureFactory.of(OVERLAY_ENERGY_IN_MULTI), getCoverTexture(side));
+ }
+ if (mode == getModeOrdinal(ENERGY_OUT)) {
+ return TextureFactory.of(texture, TextureFactory.of(OVERLAY_ENERGY_OUT_MULTI), getCoverTexture(side));
+ }
+ }
+
+ return TextureFactory.of(texture, getCoverTexture(side));
+ }
+
+ protected String getModeName(int aMode) {
+ if (aMode == NOTHING) return "Nothing";
+ if (aMode == getModeOrdinal(ITEM_IN)) return "Item Input";
+ if (aMode == getModeOrdinal(ITEM_OUT)) return "Item Output";
+ if (aMode == getModeOrdinal(FLUID_IN)) return "Fluid Input";
+ if (aMode == getModeOrdinal(FLUID_OUT)) return "Fluid Output";
+ if (aMode == getModeOrdinal(ENERGY_IN)) return "Energy Input";
+ if (aMode == getModeOrdinal(ENERGY_OUT)) return "Energy Output";
+ return "Unknown";
+ }
+
+ protected byte getModeOrdinal(int aMode) {
+ // log2 returns the bit position of the only bit set, add 1 to account for 0 being NOTHING
+ // NOTE: Must be a power of 2 (single bit)
+ return (byte) (log2(aMode, RoundingMode.UNNECESSARY) + 1);
+ }
+
+ protected byte getNextAllowedMode(List<Integer> allowedModes) {
+ if (this.allowedModes == NOTHING) return NOTHING;
+
+ final int numModes = allowedModes.size();
+ for (byte i = 1; i <= numModes; i++) {
+ final byte curMode = (byte) ((mode + i) % numModes);
+ if (curMode == NOTHING || hasMode(1 << (curMode - 1))) return curMode;
+ }
+ // Nothing valid found
+ return 0;
+ }
+
+ @Override
+ public boolean onMalletRightClick(EntityPlayer aPlayer, ItemStack tCurrentItem, ForgeDirection wrenchSide, float aX,
+ float aY, float aZ) {
+ if (allowedModes == NOTHING) return true;
+ if (mode == NOTHING) {
+ facing = wrenchSide;
+ }
+ setMode(getNextAllowedMode(BASIC_MODES));
+ if (aPlayer.isSneaking()) {
+ facing = wrenchSide;
+ }
+ GT_Utility.sendChatToPlayer(aPlayer, "Mode set to `" + getModeName(mode) + "' (" + mode + ")");
+ sendClientData((EntityPlayerMP) aPlayer);
+ return true;
+ }
+
+ @Override
+ public void setLightValue(byte aLightValue) {}
+
+ @Override
+ public byte getComparatorValue(ForgeDirection side) {
+ return 0;
+ }
+
+ @Override
+ public String getTileEntityName() {
+ return "gt.multitileentity.multiblock.part";
+ }
+
+ @Override
+ public boolean shouldTick(long tickTimer) {
+ return modeSelected(ITEM_OUT, FLUID_OUT);
+ }
+
+ /**
+ * TODO: Make sure the energy/item/fluid hatch is facing that way! or has that mode enabled on that side Check
+ * SIDE_UNKNOWN for or coverbehavior
+ */
+
+ // #region Fluid - Depending on the part type - proxy it to the multiblock controller, if we have one
+ @Override
+ @Nullable
+ public FluidInventoryLogic getFluidLogic(@Nonnull ForgeDirection side, @Nonnull InventoryType type) {
+ if (side != facing && side != ForgeDirection.UNKNOWN) return null;
+
+ if (!modeSelected(FLUID_IN, FLUID_OUT)) return null;
+
+ IMultiBlockController controller = getTarget(false);
+ if (controller == null) return null;
+ return controller
+ .getFluidLogic(modeSelected(FLUID_IN) ? InventoryType.Input : InventoryType.Output, lockedInventory);
+ }
+
+ // #endregion Fluid
+
+ // #region Energy - Depending on the part type - proxy to the multiblock controller, if we have one
+
+ @Override
+ @Nonnull
+ public PowerLogic getPowerLogic(@Nonnull ForgeDirection side) {
+ if (side != facing && side != ForgeDirection.UNKNOWN) {
+ return new NullPowerLogic();
+ }
+
+ if (!modeSelected(ENERGY_IN, ENERGY_OUT)) {
+ return new NullPowerLogic();
+ }
+
+ final IMultiBlockController controller = getTarget(true);
+ if (controller == null) {
+ return new NullPowerLogic();
+ }
+ return controller.getPowerLogic();
+ }
+
+ @Override
+ public boolean isEnetInput() {
+ return modeSelected(ENERGY_IN);
+ }
+
+ @Override
+ public boolean isEnetOutput() {
+ return modeSelected(ENERGY_OUT);
+ }
+
+ // #endregion Energy
+
+ // #region Item - Depending on the part type - proxy to the multiblock controller, if we have one
+
+ @Override
+ @Nullable
+ public ItemInventoryLogic getItemLogic(@Nonnull ForgeDirection side, @Nonnull InventoryType unused) {
+ if (side != facing && side != ForgeDirection.UNKNOWN) return null;
+
+ if (!modeSelected(ITEM_IN, ITEM_OUT)) return null;
+
+ final IMultiBlockController controller = getTarget(false);
+ if (controller == null) return null;
+
+ return controller
+ .getItemLogic(modeSelected(ITEM_IN) ? InventoryType.Input : InventoryType.Output, lockedInventory);
+ }
+
+ @Override
+ @Nullable
+ public InventoryType getItemInventoryType() {
+ if (!modeSelected(ITEM_IN, ITEM_OUT)) return InventoryType.Both;
+ return modeSelected(ITEM_IN) ? InventoryType.Input : InventoryType.Output;
+ }
+
+ // #endregion Item
+
+ // === Modular UI ===
+ @Override
+ public boolean useModularUI() {
+ return true;
+ }
+
+ @Override
+ public String getLocalName() {
+ if (modeSelected(ITEM_IN)) return "Input Inventory";
+ if (modeSelected(ITEM_OUT)) return "Output Inventory";
+ if (modeSelected(FLUID_IN)) return "Fluid Input Hatch";
+ if (modeSelected(FLUID_OUT)) return "Fluid Output Hatch";
+
+ return "Unknown";
+ }
+
+ @Override
+ public boolean hasGui(ForgeDirection side) {
+ if (modeSelected(ENERGY_IN, ENERGY_OUT) && facing == side) {
+ return false;
+ }
+ return getTarget(true) != null;
+ }
+
+ protected boolean isWrongFluid(Fluid fluid) {
+ if (fluid == null) {
+ return true;
+ }
+ Fluid lockedFluid = getLockedFluid();
+ if (lockedFluid != null) {
+ return !fluid.equals(lockedFluid);
+ }
+ return false;
+ }
+
+ protected Fluid getLockedFluid() {
+ if (configurationTank.get() != null && configurationTank.get()
+ .getFluid() != null) {
+ return configurationTank.get()
+ .getFluid();
+ }
+ return null;
+ }
+
+ @Override
+ public void addUIWidgets(Builder builder, UIBuildContext buildContext) {
+ super.addUIWidgets(builder, buildContext);
+ IMultiBlockController controller = getTarget(false);
+ if (controller == null) {
+ return;
+ }
+ if ((modeSelected(ITEM_IN, ITEM_OUT))) {
+ builder.widget(
+ controller
+ .getItemLogic(modeSelected(ITEM_IN) ? InventoryType.Input : InventoryType.Output, lockedInventory)
+ .getGuiPart()
+ .setSize(18 * 4 + 4, 18 * 5)
+ .setPos(52, 7));
+ }
+
+ if ((modeSelected(FLUID_IN, FLUID_OUT))) {
+ builder.widget(
+ controller
+ .getFluidLogic(modeSelected(FLUID_IN) ? InventoryType.Input : InventoryType.Output, lockedInventory)
+ .getGuiPart()
+ .setSize(18 * 4 + 4, 18 * 5)
+ .setPos(52, 7));
+ }
+ }
+
+ protected boolean canOpenControllerGui() {
+ return true;
+ }
+
+ @Override
+ protected int getGUIHeight() {
+ return super.getGUIHeight() + 20;
+ }
+
+ @Override
+ public void addGregTechLogo(Builder builder) {
+ if (modeSelected(ITEM_IN, ITEM_OUT)) {
+ builder.widget(
+ new DrawableWidget().setDrawable(getGUITextureSet().getGregTechLogo())
+ .setSize(17, 17)
+ .setPos(152, 74));
+ } else if (modeSelected(FLUID_IN, FLUID_OUT)) {
+ builder.widget(
+ new DrawableWidget().setDrawable(getGUITextureSet().getGregTechLogo())
+ .setSize(17, 17)
+ .setPos(152, 82));
+ } else {
+ super.addGregTechLogo(builder);
+ }
+ }
+
+ @Override
+ public void addToolTips(List<String> list, ItemStack stack, boolean f3_h) {
+ list.add("A MultiTileEntity Casing");
+ }
+
+ public String getInventoryName() {
+ IMultiBlockController controller = getTarget(false);
+ if (controller == null) return "";
+ if (modeSelected(ITEM_IN, ITEM_OUT)) {
+ InventoryType type = modeSelected(ITEM_IN) ? InventoryType.Input : InventoryType.Output;
+ ItemInventoryLogic itemLogic = controller.getItemLogic(type, lockedInventory);
+ return itemLogic.getDisplayName();
+ }
+ if (modeSelected(FLUID_IN, FLUID_OUT)) {
+ InventoryType type = modeSelected(FLUID_IN) ? InventoryType.Input : InventoryType.Output;
+ FluidInventoryLogic fluidLogic = controller.getFluidLogic(type, lockedInventory);
+ return fluidLogic.getDisplayName();
+ }
+ return "";
+ }
+
+ @Override
+ @Nonnull
+ public ForgeDirection getPowerOutputSide() {
+ if (!modeSelected(ENERGY_OUT)) return ForgeDirection.UNKNOWN;
+ return facing;
+ }
+
+ @Nonnull
+ protected GUIProvider<?> createGUIProvider() {
+ return new PartGUIProvider<>(this);
+ }
+
+ @Override
+ @Nonnull
+ public GUIProvider<?> getGUI(@Nonnull UIBuildContext uiContext) {
+ IMultiBlockController controller = getTarget(false);
+ if (controller == null) return guiProvider;
+ if (!modeSelected(NOTHING, ENERGY_IN, ENERGY_OUT)) return guiProvider;
+ if (!canOpenControllerGui()) return guiProvider;
+ if (uiContext.getPlayer()
+ .isSneaking()) return guiProvider;
+ GUIProvider<?> controllerGUI = controller.getGUI(uiContext);
+ return controllerGUI;
+ }
+
+ @Override
+ public ItemStack getAsItem() {
+ return MultiTileEntityRegistry.getRegistry(getMultiTileEntityRegistryID())
+ .getItem(getMultiTileEntityID());
+ }
+
+ @Override
+ public String getMachineName() {
+ return StatCollector.translateToLocal(getAsItem().getUnlocalizedName());
+ }
+
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableController.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableController.java
new file mode 100644
index 0000000000..51feb363dd
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableController.java
@@ -0,0 +1,128 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import net.minecraft.item.ItemStack;
+
+import com.gtnewhorizon.structurelib.util.Vec3Impl;
+
+import gregtech.api.logic.MuTEProcessingLogic;
+
+public abstract class StackableController<C extends StackableController<C, P>, P extends MuTEProcessingLogic<P>>
+ extends Controller<C, P> {
+
+ protected static String STACKABLE_STOP = "STACKABLE_STOP";
+ protected static String STACKABLE_MIDDLE = "STACKABLE_MIDDLE";
+ protected static String STACKABLE_START = "STACKABLE_START";
+ protected int stackCount = 0;
+
+ /**
+ * construct implementation for stackable multi-blocks
+ */
+ @Override
+ public void construct(ItemStack trigger, boolean hintsOnly) {
+ final int blueprintCount = (trigger.stackSize - 1) + getMinStacks();
+ final int stackCount = Math.min(blueprintCount, getMaxStacks());
+
+ buildState.startBuilding(getStartingStructureOffset());
+ buildPiece(getStackableStart(), trigger, hintsOnly, buildState.getCurrentOffset());
+ buildState.addOffset(getStartingStackOffset());
+
+ for (int i = 0; i < stackCount; i++) {
+ buildPiece(getStackableMiddle(i), trigger, hintsOnly, buildState.getCurrentOffset());
+ buildState.addOffset(getPerStackOffset());
+ }
+ if (hasTop()) {
+ buildState.addOffset(getAfterLastStackOffset());
+ buildPiece(getStackableStop(), trigger, hintsOnly, buildState.stopBuilding());
+ } else {
+ buildState.stopBuilding();
+ }
+ }
+
+ /**
+ * Stackable
+ *
+ * @return The minimum number of stacks required for this multi-block to form
+ */
+ public abstract int getMinStacks();
+
+ /**
+ * Stackable
+ *
+ * @return The maximum number of stacks allowed for this multi-block to form
+ */
+ public abstract int getMaxStacks();
+
+ /**
+ * Stackable
+ *
+ * @return The starting offset for the first stack
+ */
+ public abstract Vec3Impl getStartingStackOffset();
+
+ /**
+ * Stackable
+ *
+ * @return The per stack offset
+ */
+ public abstract Vec3Impl getPerStackOffset();
+
+ /**
+ * Stackable
+ *
+ * @return Whether this structure has a Top/Cap. Defaults to true.
+ */
+ public boolean hasTop() {
+ return true;
+ }
+
+ /**
+ * Stackable
+ *
+ * @return Any offset needed after the last stack
+ */
+ public Vec3Impl getAfterLastStackOffset() {
+ return new Vec3Impl(0, 0, 0);
+ }
+
+ /**
+ * checkMachine implementation for stackable multi-blocks
+ */
+ @Override
+ public boolean checkMachine() {
+ stackCount = 0;
+
+ buildState.startBuilding(getStartingStructureOffset());
+ if (!checkPiece(getStackableStart(), buildState.getCurrentOffset())) return buildState.failBuilding();
+
+ buildState.addOffset(getStartingStackOffset());
+
+ for (int i = 0; i < getMaxStacks(); i++) {
+ if (!checkPiece(getStackableMiddle(i), buildState.getCurrentOffset())) {
+ break;
+ }
+
+ buildState.addOffset(getPerStackOffset());
+ stackCount++;
+
+ }
+ if (stackCount < getMinStacks()) return buildState.failBuilding();
+
+ buildState.addOffset(getAfterLastStackOffset());
+ if (!checkPiece(getStackableStop(), buildState.stopBuilding())) {
+ return buildState.failBuilding();
+ }
+ return super.checkMachine();
+ }
+
+ protected String getStackableStop() {
+ return STACKABLE_STOP;
+ }
+
+ protected String getStackableMiddle(int stackIndex) {
+ return STACKABLE_MIDDLE;
+ }
+
+ protected String getStackableStart() {
+ return STACKABLE_START;
+ }
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableModularController.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableModularController.java
new file mode 100644
index 0000000000..1dfd497151
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/StackableModularController.java
@@ -0,0 +1,77 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jetbrains.annotations.NotNull;
+
+import gregtech.api.logic.MuTEProcessingLogic;
+import gregtech.api.multitileentity.interfaces.UpgradableModularMuTE;
+import gregtech.api.util.GT_StructureUtilityMuTE.UpgradeCasings;
+
+public abstract class StackableModularController<C extends StackableModularController<C, P>, P extends MuTEProcessingLogic<P>>
+ extends StackableController<C, P> implements UpgradableModularMuTE {
+
+ protected double durationMultiplier = 1;
+ protected double euTickMultiplier = 1;
+
+ private Map<UpgradeCasings, int[]> mucMap;
+
+ protected @NotNull Map<UpgradeCasings, int[]> getMucMap() {
+ if (mucMap == null) {
+ mucMap = createMucMap();
+ }
+ return mucMap;
+ }
+
+ protected static @NotNull Map<UpgradeCasings, int[]> createMucMap() {
+ Map<UpgradeCasings, int[]> mucCount = new HashMap<>();
+ mucCount.put(UpgradeCasings.Heater, new int[] { 0, 0, 0, 0, 0 });
+ mucCount.put(UpgradeCasings.Insulator, new int[] { 0, 0, 0, 0, 0 });
+ return mucCount;
+ }
+
+ @Override
+ public void increaseMucCount(UpgradeCasings casingType, int tier) {
+ Map<UpgradeCasings, int[]> mucCounters = getMucMap();
+ int[] casingCount = mucCounters.get(casingType);
+
+ switch (tier) {
+ case 0, 1, 2 -> casingCount[0] += 1;
+ case 3, 4, 5 -> casingCount[1] += 1;
+ case 6, 7, 8 -> casingCount[2] += 1;
+ case 9, 10, 11 -> casingCount[3] += 1;
+ default -> casingCount[4] += 1;
+ }
+ }
+
+ @Override
+ public void resetMucCount() {
+ Map<UpgradeCasings, int[]> mucCounters = getMucMap();
+ mucCounters.forEach((type, casingCount) -> { Arrays.fill(casingCount, 0); });
+ }
+
+ // Returns the cheapest MUC that is possible for the multi, which gets the minimum bonuses.
+ protected abstract UpgradeCasings getBaseMucType();
+
+ // Minimum parallel bonus per MUC. Higher tier MUCs multiply with this value for even more parallels.
+ protected abstract int getParallelFactor();
+
+ protected void calculateParallels() {
+ int parallelCount = 0;
+ int parallelFactor = getParallelFactor();
+ int[] parallelCasingList = mucMap.get(getBaseMucType());
+
+ for (int i = 0; i < 5; i++) {
+ // (i * 3 + 1) -> Convert MUC tier into minimum GT tier, in groups of 3 (LV, EV, LuV, UHV, UMV)
+ // If higher than multi tier, upgrade casing has no effect
+ if (i * 3 + 1 <= tier) {
+ parallelCount += parallelCasingList[i] * (i + 1) * parallelFactor;
+ }
+ }
+ maxParallel = parallelCount == 0 ? 1 : parallelCount;
+ }
+
+ protected abstract boolean calculateMucMultipliers();
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/base/WallShareablePart.java b/src/main/java/gregtech/api/multitileentity/multiblock/base/WallShareablePart.java
new file mode 100644
index 0000000000..238ce1eb2d
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/base/WallShareablePart.java
@@ -0,0 +1,100 @@
+package gregtech.api.multitileentity.multiblock.base;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ChunkCoordinates;
+import net.minecraftforge.common.util.ForgeDirection;
+
+import gregtech.api.multitileentity.WeakTargetRef;
+import gregtech.api.multitileentity.interfaces.IMultiBlockController;
+
+public class WallShareablePart extends MultiBlockPart {
+
+ protected List<WeakTargetRef<IMultiBlockController>> targets = new ArrayList<>();
+
+ @Override
+ public void setTarget(IMultiBlockController newController, int allowedModes) {
+ if (targets.size() >= 1) {
+ this.allowedModes = 0;
+ setMode((byte) 0);
+ controller.invalidate();
+ } else {
+ this.allowedModes = allowedModes;
+ controller.setTarget(newController);
+ }
+
+ if (newController == null) {
+ return;
+ }
+
+ targets.add(new WeakTargetRef<IMultiBlockController>(IMultiBlockController.class, true));
+ }
+
+ @Override
+ public UUID getLockedInventory() {
+ if (targets.size() > 1) {
+ return null;
+ }
+ return super.getLockedInventory();
+ }
+
+ @Override
+ public IMultiBlockController getTarget(boolean aCheckValidity) {
+ if (targets.size() != 1) {
+ return null;
+ }
+
+ controller.setTarget(
+ targets.get(0)
+ .get());
+ return super.getTarget(aCheckValidity);
+ }
+
+ @Override
+ public String getTileEntityName() {
+ return "gt.multiTileEntity.casing.wallSharable";
+ }
+
+ @Override
+ public boolean onBlockBroken() {
+ for (final WeakTargetRef<IMultiBlockController> tar : targets) {
+ IMultiBlockController target = getTarget(tar.getPosition(), false);
+ if (target == null) {
+ continue;
+ }
+ target.onStructureChange();
+ }
+ return false;
+ }
+
+ @Override
+ public void onBlockAdded() {
+ for (final ForgeDirection side : ForgeDirection.VALID_DIRECTIONS) {
+ final TileEntity te = getTileEntityAtSide(side);
+ if (te instanceof MultiBlockPart part) {
+ final IMultiBlockController tController = part.getTarget(false);
+ if (tController != null) tController.onStructureChange();
+ } else if (te instanceof IMultiBlockController tController) {
+ tController.onStructureChange();
+ }
+ }
+ }
+
+ public IMultiBlockController getTarget(ChunkCoordinates coordinates, boolean aCheckValidity) {
+ IMultiBlockController target = null;
+ if (coordinates == null) return null;
+ if (worldObj.blockExists(coordinates.posX, coordinates.posY, coordinates.posZ)) {
+ final TileEntity te = worldObj.getTileEntity(coordinates.posX, coordinates.posY, coordinates.posZ);
+ if (te instanceof IMultiBlockController) {
+ target = (IMultiBlockController) te;
+ }
+ }
+ if (aCheckValidity) {
+ return target != null && target.checkStructure(false) ? target : null;
+ }
+ return target;
+ }
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/casing/BasicCasing.java b/src/main/java/gregtech/api/multitileentity/multiblock/casing/BasicCasing.java
new file mode 100644
index 0000000000..84f1442a88
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/casing/BasicCasing.java
@@ -0,0 +1,7 @@
+package gregtech.api.multitileentity.multiblock.casing;
+
+import gregtech.api.multitileentity.multiblock.base.MultiBlockPart;
+
+public class BasicCasing extends MultiBlockPart {
+ /* Nothing */
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/casing/CasingBehaviorBase.java b/src/main/java/gregtech/api/multitileentity/multiblock/casing/CasingBehaviorBase.java
new file mode 100644
index 0000000000..9f0d9bd2d1
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/casing/CasingBehaviorBase.java
@@ -0,0 +1,10 @@
+package gregtech.api.multitileentity.multiblock.casing;
+
+/**
+ * Allows for functional casings that influence the multiblock structure's behavior Examples include: - Extra Byproducts
+ * - Extra Speed - More parallels - Upgraded internal item/energy/fluid storage - Faster output - Voiding/Anti-Voiding
+ * upgrade - Ender Upgrades - etc, etc.
+ *
+ */
+public class CasingBehaviorBase {
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/casing/FunctionalCasing.java b/src/main/java/gregtech/api/multitileentity/multiblock/casing/FunctionalCasing.java
new file mode 100644
index 0000000000..bc3c857fd6
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/casing/FunctionalCasing.java
@@ -0,0 +1,29 @@
+package gregtech.api.multitileentity.multiblock.casing;
+
+import net.minecraft.nbt.NBTTagCompound;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.multitileentity.multiblock.base.MultiBlockPart;
+
+public abstract class FunctionalCasing extends MultiBlockPart {
+
+ private int tier = 0;
+
+ @Override
+ public int getPartTier() {
+ return tier;
+ }
+
+ public abstract float getPartModifier();
+
+ @Override
+ public void readMultiTileNBT(NBTTagCompound nbt) {
+ super.readMultiTileNBT(nbt);
+ tier = nbt.getInteger(GT_Values.NBT.TIER);
+ }
+
+ @Override
+ public void writeMultiTileNBT(NBTTagCompound nbt) {
+ super.writeMultiTileNBT(nbt);
+ }
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/casing/Glasses.java b/src/main/java/gregtech/api/multitileentity/multiblock/casing/Glasses.java
new file mode 100644
index 0000000000..edc1bd0e5b
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/casing/Glasses.java
@@ -0,0 +1,32 @@
+package gregtech.api.multitileentity.multiblock.casing;
+
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofBlockUnlocalizedName;
+import static com.gtnewhorizon.structurelib.structure.StructureUtility.ofChain;
+import static gregtech.api.enums.Mods.BartWorks;
+import static gregtech.api.enums.Mods.Botania;
+import static gregtech.api.enums.Mods.IndustrialCraft2;
+import static gregtech.api.enums.Mods.Thaumcraft;
+
+import com.gtnewhorizon.structurelib.structure.IStructureElementChain;
+
+public class Glasses {
+
+ /** support all Bart, Botania, Ic2, Thaumcraft glasses for multiblock structure **/
+ public static <T> IStructureElementChain<T> chainAllGlasses() {
+ return ofChain(
+ // IndustrialCraft2 glass
+ ofBlockUnlocalizedName(IndustrialCraft2.ID, "blockAlloyGlass", 0, true),
+
+ // Botania glass
+ ofBlockUnlocalizedName(Botania.ID, "manaGlass", 0, false),
+ ofBlockUnlocalizedName(Botania.ID, "elfGlass", 0, false),
+
+ // BartWorks glass
+ ofBlockUnlocalizedName(BartWorks.ID, "BW_GlasBlocks", 0, true),
+ ofBlockUnlocalizedName(BartWorks.ID, "BW_GlasBlocks2", 0, true),
+
+ // warded glass
+ ofBlockUnlocalizedName(Thaumcraft.ID, "blockCosmeticOpaque", 2, false));
+ }
+
+}
diff --git a/src/main/java/gregtech/api/multitileentity/multiblock/casing/UpgradeCasing.java b/src/main/java/gregtech/api/multitileentity/multiblock/casing/UpgradeCasing.java
new file mode 100644
index 0000000000..566afcd770
--- /dev/null
+++ b/src/main/java/gregtech/api/multitileentity/multiblock/casing/UpgradeCasing.java
@@ -0,0 +1,35 @@
+package gregtech.api.multitileentity.multiblock.casing;
+
+import net.minecraft.nbt.NBTTagCompound;
+
+import gregtech.api.enums.GT_Values;
+import gregtech.api.multitileentity.interfaces.IMultiBlockController;
+import gregtech.api.multitileentity.multiblock.base.MultiBlockPart;
+
+public abstract class UpgradeCasing extends MultiBlockPart {
+
+ protected int tier = 0;
+
+ @Override
+ public int getPartTier() {
+ return tier;
+ }
+
+ @Override
+ public void setTarget(IMultiBlockController newController, int aAllowedModes) {
+ super.setTarget(newController, aAllowedModes);
+
+ if (getTarget(false) != null) {
+ customWork(getTarget(false));
+ }
+ }
+
+ @Override
+ public void readMultiTileNBT(NBTTagCompound aNBT) {
+ super.readMultiTileNBT(aNBT);
+ tier = aNBT.getInteger(GT_Values.NBT.TIER);
+ }
+
+ protected abstract void customWork(IMultiBlockController aTarget);
+
+}