From 73b30c9421f189ab64569c24d2fc594afd665d11 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 6 Jul 2025 01:48:04 +0300 Subject: [PATCH] Add Chemical Vat multiplace - ability to place entire vat levels in 1 click --- .../content/machinery/vat/base/VatItem.java | 197 ++++++++++++++++++ .../content/machinery/vat/base/VatModel.java | 94 +++++++++ .../drmangotea/tfmg/registry/TFMGBlocks.java | 18 +- 3 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatItem.java create mode 100644 src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatModel.java diff --git a/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatItem.java b/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatItem.java new file mode 100644 index 00000000..4d757440 --- /dev/null +++ b/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatItem.java @@ -0,0 +1,197 @@ +package com.drmangotea.tfmg.content.machinery.vat.base; + +import com.drmangotea.tfmg.registry.TFMGBlockEntities; +import com.simibubi.create.api.connectivity.ConnectivityHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.items.ItemStackHandler; + +public class VatItem extends BlockItem { + + String vatType; + + public VatItem(Block block, Properties properties, String vatType) { + super(block, properties); + this.vatType = vatType; + } + + public static class SteelVatItem extends VatItem { + + public SteelVatItem(Block block, Properties properties) { + super(block, properties,"tfmg:steel_vat"); + } + } + + public static class CastIronVatItem extends VatItem { + + public CastIronVatItem(Block block, Properties properties) { + super(block, properties,"tfmg:cast_iron_vat"); + } + } + + public static class FireproofVatItem extends VatItem { + + public FireproofVatItem(Block block, Properties properties) { + super(block, properties,"tfmg:firebrick_lined_vat"); + } + } + + @Override + public InteractionResult place(BlockPlaceContext ctx) { + InteractionResult initialResult = super.place(ctx); + if (!initialResult.consumesAction()) + return initialResult; + tryMultiPlace(ctx); + return initialResult; + } + + @Override + protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player, + ItemStack stack, BlockState state) { + CompoundTag nbt = stack.getTagElement("BlockEntityTag"); + if (nbt == null) return false; + + // 1. Preserve multi-block structure data + nbt.remove("Controller"); // Let the vat rebuild connectivity + nbt.remove("LastKnownPos"); + + // 2. Process inventories (input/output slots) + sanitizeInventory(nbt, "InputItems", 4); + sanitizeInventory(nbt, "OutputItems", 4); + + // 3. Handle fluids via Create's SmartFluidTankBehaviour + if (nbt.contains("CreateData")) { + CompoundTag createData = nbt.getCompound("CreateData"); + int capacity = VatBlockEntity.getCapacityMultiplier(); // Single-block capacity + + // Clamp all tanks (both input and output) + clampTankGroup(createData, "InputTanks", capacity); + clampTankGroup(createData, "OutputTanks", capacity); + } + + // 4. Clean transient and vat-specific data + nbt.remove("Luminosity"); + nbt.remove("Machines"); // Machines must re-attach after placement + nbt.remove("Timer"); // Reset recipe progress + nbt.remove("RecipeId"); + + return super.updateCustomBlockEntityTag(pos, level, player, stack, state); + } + + private void sanitizeInventory(CompoundTag nbt, String key, int slots) { + if (nbt.contains(key)) { + ItemStackHandler handler = new ItemStackHandler(slots); + handler.deserializeNBT(nbt.getCompound(key)); + + // Validate items (prevent overstacked/illegal items) + for (int i = 0; i < slots; i++) { + ItemStack stack = handler.getStackInSlot(i); + if (stack.getCount() > stack.getMaxStackSize()) { + stack.setCount(stack.getMaxStackSize()); + } + } + nbt.put(key, handler.serializeNBT()); + } + } + + private void clampTankGroup(CompoundTag createData, String tankGroup, int capacity) { + if (!createData.contains(tankGroup)) return; + + ListTag tanks = createData.getList(tankGroup, Tag.TAG_COMPOUND); + for (int i = 0; i < tanks.size(); i++) { + CompoundTag tankTag = tanks.getCompound(i); + if (tankTag.contains("TankContent")) { + FluidStack fluid = FluidStack.loadFluidStackFromNBT(tankTag.getCompound("TankContent")); + if (!fluid.isEmpty()) { + fluid.setAmount(Math.min(capacity, fluid.getAmount())); + tankTag.put("TankContent", fluid.writeToNBT(new CompoundTag())); + } + } + } + } + + private void tryMultiPlace(BlockPlaceContext ctx) { + + Player player = ctx.getPlayer(); + if (player == null) + return; + if (player.isShiftKeyDown()) + return; + Direction face = ctx.getClickedFace(); + if (!face.getAxis() + .isVertical()) + return; + ItemStack stack = ctx.getItemInHand(); + Level world = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + BlockPos placedOnPos = pos.relative(face.getOpposite()); + BlockState placedOnState = world.getBlockState(placedOnPos); + + if (!(placedOnState.getBlock() instanceof VatBlock placedOnVat)) return; + if (!placedOnVat.vatType.equals(this.vatType)) return; + + + VatBlockEntity vatAt = ConnectivityHandler.partAt( + TFMGBlockEntities.CHEMICAL_VAT.get(), world, placedOnPos); + if (vatAt == null) + return; + VatBlockEntity controllerTE = vatAt.getControllerBE(); + if (controllerTE == null) + return; + + int width = controllerTE.getWidth(); + if (width == 1) + return; + + int vatsToPlace = 0; + BlockPos startPos = face == Direction.DOWN ? controllerTE.getBlockPos() + .below() + : controllerTE.getBlockPos() + .above(controllerTE.getHeight()); + + if (startPos.getY() != pos.getY()) + return; + + for (int xOffset = 0; xOffset < width; xOffset++) { + for (int zOffset = 0; zOffset < width; zOffset++) { + BlockPos offsetPos = startPos.offset(xOffset, 0, zOffset); + BlockState blockState = world.getBlockState(offsetPos); + if (VatBlock.isVat(blockState)) + continue; + if (!blockState.canBeReplaced()) + return; + vatsToPlace++; + } + } + + if (!player.isCreative() && stack.getCount() < vatsToPlace) + return; + + for (int xOffset = 0; xOffset < width; xOffset++) { + for (int zOffset = 0; zOffset < width; zOffset++) { + BlockPos offsetPos = startPos.offset(xOffset, 0, zOffset); + BlockState blockState = world.getBlockState(offsetPos); + if (VatBlock.isVat(blockState)) + continue; + BlockPlaceContext context = BlockPlaceContext.at(ctx, offsetPos, face); + player.getPersistentData() + .putBoolean("SilenceTankSound", true); + super.place(context); + player.getPersistentData() + .remove("SilenceTankSound"); + } + } + } +} diff --git a/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatModel.java b/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatModel.java new file mode 100644 index 00000000..3c51e107 --- /dev/null +++ b/src/main/java/com/drmangotea/tfmg/content/machinery/vat/base/VatModel.java @@ -0,0 +1,94 @@ +package com.drmangotea.tfmg.content.machinery.vat.base; + +import com.drmangotea.tfmg.base.TFMGSpriteShifts; +import com.simibubi.create.api.connectivity.ConnectivityHandler; +import com.simibubi.create.content.fluids.tank.FluidTankCTBehaviour; +import com.simibubi.create.foundation.block.connected.CTModel; +import com.simibubi.create.foundation.block.connected.CTSpriteShiftEntry; +import net.createmod.catnip.data.Iterate; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.client.model.data.ModelData; +import net.minecraftforge.client.model.data.ModelProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class VatModel extends CTModel { + protected static final ModelProperty CULL_PROPERTY = new ModelProperty<>(); + + public static VatModel steelVat(BakedModel originalModel) { + return new VatModel(originalModel, TFMGSpriteShifts.STEEL_VAT, TFMGSpriteShifts.STEEL_VAT_TOP, + TFMGSpriteShifts.STEEL_VAT_INNER); + } + public static VatModel castIronVat(BakedModel originalModel) { + return new VatModel(originalModel, TFMGSpriteShifts.CAST_IRON_VAT, TFMGSpriteShifts.CAST_IRON_VAT_TOP, + TFMGSpriteShifts.CAST_IRON_VAT_INNER); + } + public static VatModel fireproofVat(BakedModel originalModel) { + return new VatModel(originalModel, TFMGSpriteShifts.FIREPROOF_VAT, TFMGSpriteShifts.STEEL_VAT_TOP, + TFMGSpriteShifts.STEEL_VAT_INNER); + } + + + private VatModel(BakedModel originalModel, CTSpriteShiftEntry side, CTSpriteShiftEntry top, + CTSpriteShiftEntry inner) { + super(originalModel, new FluidTankCTBehaviour(side, top, inner)); + } + + @Override + protected ModelData.Builder gatherModelData(ModelData.Builder builder, BlockAndTintGetter world, BlockPos pos, BlockState state, + ModelData blockEntityData) { + super.gatherModelData(builder, world, pos, state, blockEntityData); + CullData cullData = new CullData(); + for (Direction d : Iterate.horizontalDirections) + cullData.setCulled(d, ConnectivityHandler.isConnected(world, pos, pos.relative(d))); + return builder.with(CULL_PROPERTY, cullData); + } + + @Override + public List getQuads(BlockState state, Direction side, RandomSource rand, ModelData extraData, RenderType renderType) { + if (side != null) + return Collections.emptyList(); + + List quads = new ArrayList<>(); + for (Direction d : Iterate.directions) { + if (extraData.has(CULL_PROPERTY) && extraData.get(CULL_PROPERTY) + .isCulled(d)) + continue; + quads.addAll(super.getQuads(state, d, rand, extraData, renderType)); + } + quads.addAll(super.getQuads(state, null, rand, extraData, renderType)); + return quads; + } + private class CullData { + boolean[] culledFaces; + + public CullData() { + culledFaces = new boolean[4]; + Arrays.fill(culledFaces, false); + } + + void setCulled(Direction face, boolean cull) { + if (face.getAxis() + .isVertical()) + return; + culledFaces[face.get2DDataValue()] = cull; + } + + boolean isCulled(Direction face) { + if (face.getAxis() + .isVertical()) + return false; + return culledFaces[face.get2DDataValue()]; + } + } +} diff --git a/src/main/java/com/drmangotea/tfmg/registry/TFMGBlocks.java b/src/main/java/com/drmangotea/tfmg/registry/TFMGBlocks.java index 6b5b674c..8a659347 100644 --- a/src/main/java/com/drmangotea/tfmg/registry/TFMGBlocks.java +++ b/src/main/java/com/drmangotea/tfmg/registry/TFMGBlocks.java @@ -110,6 +110,8 @@ import com.drmangotea.tfmg.content.machinery.oil_processing.pumpjack.pumpjack.ha import com.drmangotea.tfmg.content.machinery.oil_processing.surface_scanner.SurfaceScannerBlock; import com.drmangotea.tfmg.content.machinery.vat.base.VatBlock; import com.drmangotea.tfmg.content.machinery.vat.base.VatGenerator; +import com.drmangotea.tfmg.content.machinery.vat.base.VatItem; +import com.drmangotea.tfmg.content.machinery.vat.base.VatModel; import com.drmangotea.tfmg.content.machinery.vat.electrode_holder.ElectrodeHolderBlock; import com.drmangotea.tfmg.content.machinery.vat.industrial_mixer.IndustrialMixerBlock; import com.simibubi.create.AllMountedStorageTypes; @@ -457,10 +459,10 @@ public class TFMGBlocks { .properties(p -> p.isRedstoneConductor((p1, p2, p3) -> true)) .transform(pickaxeOnly()) .blockstate(new VatGenerator()::generate) - .onRegister(CreateRegistrate.blockModel(() -> SteelFluidTankModel::steelVat)) + .onRegister(CreateRegistrate.blockModel(() -> VatModel::steelVat)) .addLayer(() -> RenderType::cutoutMipped) - .item(SteelTankItem::new) - .model(AssetLookup.customBlockItemModel("_", "block_single_window")) + .item(VatItem.SteelVatItem::new) + .model(AssetLookup.customBlockItemModel("_", "block_single")) .build() .register(); public static final BlockEntry CAST_IRON_CHEMICAL_VAT = @@ -471,10 +473,10 @@ public class TFMGBlocks { .properties(p -> p.isRedstoneConductor((p1, p2, p3) -> true)) .transform(pickaxeOnly()) .blockstate(new VatGenerator()::generate) - .onRegister(CreateRegistrate.blockModel(() -> SteelFluidTankModel::castIronVat)) + .onRegister(CreateRegistrate.blockModel(() -> VatModel::castIronVat)) .addLayer(() -> RenderType::cutoutMipped) - .item(SteelTankItem::new) - .model(AssetLookup.customBlockItemModel("_", "block_single_window")) + .item(VatItem.CastIronVatItem::new) + .model(AssetLookup.customBlockItemModel("_", "block_single")) .build() .register(); public static final BlockEntry FIREPROOF_CHEMICAL_VAT = @@ -485,9 +487,9 @@ public class TFMGBlocks { .properties(p -> p.isRedstoneConductor((p1, p2, p3) -> true)) .transform(pickaxeOnly()) .blockstate(new VatGenerator()::generate) - .onRegister(CreateRegistrate.blockModel(() -> SteelFluidTankModel::fireproofVat)) + .onRegister(CreateRegistrate.blockModel(() -> VatModel::fireproofVat)) .addLayer(() -> RenderType::cutoutMipped) - .item(SteelTankItem::new) + .item(VatItem.FireproofVatItem::new) .model(AssetLookup.customBlockItemModel("_", "block_single")) .build() .register();