Basic framework for vacuum arc furnace multiblock. Moved multiblock methods to MultiblockHelper.

This commit is contained in:
XevianLight
2026-01-18 21:06:14 -07:00
parent 557c2761a7
commit ea998165be
32 changed files with 1335 additions and 245 deletions

View File

@@ -2,6 +2,11 @@ package net.xevianlight.aphelion.block.custom;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
@@ -13,13 +18,17 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.xevianlight.aphelion.block.entity.custom.EAFPartEntity;
import net.xevianlight.aphelion.block.entity.custom.ElectricArcFurnaceEntity;
import net.xevianlight.aphelion.block.entity.custom.VacuumArcFurnaceControllerEntity;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.MultiblockHelper;
import org.jetbrains.annotations.Nullable;
public class ArcFurnaceCasingBlock extends BaseEntityBlock {
public static final BooleanProperty FORMED = BooleanProperty.create("formed");
public static final BooleanProperty FORMED = AphelionBlockStateProperties.FORMED;
public ArcFurnaceCasingBlock(Properties properties) {
super(properties);
@@ -27,6 +36,11 @@ public class ArcFurnaceCasingBlock extends BaseEntityBlock {
.setValue(FORMED, false));
}
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
public static Properties getProperties() {
return Properties
.of()
@@ -42,18 +56,18 @@ public class ArcFurnaceCasingBlock extends BaseEntityBlock {
public static final MapCodec<ArcFurnaceCasingBlock> CODEC = simpleCodec(ArcFurnaceCasingBlock::new);
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
// @Override
// protected MapCodec<? extends BaseEntityBlock> codec() {
// return CODEC;
// }
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new EAFPartEntity(blockPos, blockState);
}
// @Override
// public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
// return new EAFPartEntity(blockPos, blockState);
// }
private void pingNearbyController(Level level, BlockPos pos) {
int r = 3;
int r = 5;
BlockPos.MutableBlockPos mp = new BlockPos.MutableBlockPos();
for (int dx=-r; dx<=r; dx++)
@@ -61,14 +75,28 @@ public class ArcFurnaceCasingBlock extends BaseEntityBlock {
for (int dz=-r; dz<=r; dz++) {
mp.set(pos.getX()+dx, pos.getY()+dy, pos.getZ()+dz);
BlockEntity be = level.getBlockEntity(mp);
if (be instanceof ElectricArcFurnaceEntity eaf) {
if (level.getBlockState(eaf.getBlockPos()).getBlock() instanceof ElectricArcFurnace) {
eaf.tryForm();
if (be instanceof VacuumArcFurnaceControllerEntity vaf) {
if (level.getBlockState(vaf.getBlockPos()).getBlock() instanceof VacuumArcFurnaceController) {
MultiblockHelper.tryForm(level, vaf.getBlockState(), vaf.getBlockPos(), VacuumArcFurnaceControllerEntity.SHAPE, AphelionBlockStateProperties.FORMED);
}
}
}
}
@Override
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult result) {
if (state.getValue(AphelionBlockStateProperties.FORMED)) {
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof EAFPartEntity eafPartEntity) {
if (eafPartEntity.getControllerPos() != null)
if (level.getBlockEntity(eafPartEntity.getControllerPos()) instanceof VacuumArcFurnaceControllerEntity)
serverPlayer.openMenu(new SimpleMenuProvider((VacuumArcFurnaceControllerEntity) level.getBlockEntity(eafPartEntity.getControllerPos()), Component.literal("Vacuum Arc Furnace")), eafPartEntity.getControllerPos());
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
return InteractionResult.FAIL;
}
@Override
public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
super.onPlace(state, level, pos, oldState, movedByPiston);
@@ -97,4 +125,9 @@ public class ArcFurnaceCasingBlock extends BaseEntityBlock {
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FORMED);
}
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new EAFPartEntity(blockPos, blockState);
}
}

View File

@@ -27,6 +27,8 @@ import net.minecraft.world.phys.BlockHitResult;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.xevianlight.aphelion.block.entity.custom.ElectricArcFurnaceEntity;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.MultiblockHelper;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
@@ -36,7 +38,7 @@ public class ElectricArcFurnace extends BaseEntityBlock {
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final BooleanProperty LIT = BlockStateProperties.LIT;
public static final BooleanProperty FORMED = BooleanProperty.create("formed");
public static final BooleanProperty FORMED = AphelionBlockStateProperties.FORMED;
public ElectricArcFurnace(Properties properties) {
super(properties);
@@ -72,8 +74,8 @@ public class ElectricArcFurnace extends BaseEntityBlock {
@Override
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult result) {
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof ElectricArcFurnaceEntity electricArcFurnaceEntity && state.getValue(FORMED)) {
serverPlayer.openMenu(new SimpleMenuProvider(electricArcFurnaceEntity, Component.literal("Electric Arc Furnace")), pos);
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof ElectricArcFurnaceEntity electricArcFurnaceEntity) {
serverPlayer.openMenu(new SimpleMenuProvider(electricArcFurnaceEntity, Component.literal("Electric Arc Furnace")), pos);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@@ -95,7 +97,7 @@ public class ElectricArcFurnace extends BaseEntityBlock {
if (!level.isClientSide && state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity= level.getBlockEntity(pos);
if (blockEntity instanceof ElectricArcFurnaceEntity electricArcFurnaceEntity) {
if(state.getValue(FORMED)) electricArcFurnaceEntity.unformForRemoval();
// if(state.getValue(FORMED)) MultiblockHelper.unformForRemoval(level, state, pos, ElectricArcFurnaceEntity.SHAPE, AphelionBlockStateProperties.FORMED);
electricArcFurnaceEntity.drops();
}
}
@@ -107,9 +109,9 @@ public class ElectricArcFurnace extends BaseEntityBlock {
super.onPlace(state, level, pos, oldState, movedByPiston);
if (!level.isClientSide() && oldState.getBlock() != state.getBlock()) {
BlockEntity blockEntity= level.getBlockEntity(pos);
if (blockEntity instanceof ElectricArcFurnaceEntity electricArcFurnaceEntity) {
electricArcFurnaceEntity.tryForm();
}
// if (blockEntity instanceof ElectricArcFurnaceEntity electricArcFurnaceEntity) {
// MultiblockHelper.tryForm(level, state, pos, ElectricArcFurnaceEntity.SHAPE, AphelionBlockStateProperties.FORMED);
// }
}
}

View File

@@ -0,0 +1,196 @@
package net.xevianlight.aphelion.block.custom;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.xevianlight.aphelion.block.entity.custom.VacuumArcFurnaceControllerEntity;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.MultiblockHelper;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class VacuumArcFurnaceController extends BaseEntityBlock {
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final BooleanProperty LIT = BlockStateProperties.LIT;
public static final BooleanProperty FORMED = AphelionBlockStateProperties.FORMED;
public VacuumArcFurnaceController(Properties properties) {
super(properties);
this.registerDefaultState(this.getStateDefinition().any()
.setValue(FORMED, false));
}
public static final MapCodec<VacuumArcFurnaceController> CODEC = simpleCodec(VacuumArcFurnaceController::new);
private final int INPUT_SLOT = 0;
private final int SECONDARY_INPUT_SLOT = 1;
private final int OUTPUT_SLOT = 2;
private final int ENERGY_SLOT = 3;
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
public static Properties getProperties() {
return Properties
.of()
.sound(SoundType.METAL)
.destroyTime(2f)
.explosionResistance(10f)
.requiresCorrectToolForDrops();
}
public static Item.Properties getItemProperties() {
return new Item.Properties();
}
@Override
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult result) {
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof VacuumArcFurnaceControllerEntity vacuumArcFurnaceEntity) {
if (vacuumArcFurnaceEntity.isFormed())
serverPlayer.openMenu(new SimpleMenuProvider(vacuumArcFurnaceEntity, Component.literal("Vacuum Arc Furnace")), pos);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new VacuumArcFurnaceControllerEntity(blockPos, blockState);
}
@Override
public RenderShape getRenderShape(BlockState pState) {
return RenderShape.MODEL;
}
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
if (!level.isClientSide && state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity= level.getBlockEntity(pos);
if (blockEntity instanceof VacuumArcFurnaceControllerEntity vacuumArcFurnaceEntity) {
if(state.getValue(FORMED)) MultiblockHelper.unformForRemoval(level, state, pos, vacuumArcFurnaceEntity.SHAPE, AphelionBlockStateProperties.FORMED);
vacuumArcFurnaceEntity.drops();
}
}
super.onRemove(state, level, pos, newState, movedByPiston);
}
@Override
protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
super.onPlace(state, level, pos, oldState, movedByPiston);
if (!level.isClientSide() && oldState.getBlock() != state.getBlock()) {
BlockEntity blockEntity= level.getBlockEntity(pos);
if (blockEntity instanceof VacuumArcFurnaceControllerEntity vacuumArcFurnaceEntity) {
MultiblockHelper.tryForm(level, state, pos, vacuumArcFurnaceEntity.SHAPE, AphelionBlockStateProperties.FORMED);
}
}
}
public static int getRedstoneSignalFromItemHandler(@javax.annotation.Nullable ItemStackHandler itemStackHandler, List<Integer> slots) {
if (itemStackHandler == null) {
return 0;
} else {
int i = 0;
float f = 0.0F;
for(int slot : slots) {
ItemStack itemstack = itemStackHandler.getStackInSlot(slot);
if (!itemstack.isEmpty()) {
f += (float)itemstack.getCount() / (float)Math.min(itemStackHandler.getSlotLimit(slot), itemstack.getMaxStackSize());
++i;
}
}
f /= (float)slots.size();
return Mth.floor(f * 14.0F) + (i > 0 ? 1 : 0);
}
}
@Override
protected boolean isSignalSource(BlockState state) {
return true;
}
@Override
protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) {
return super.getSignal(state, level, pos, direction);
}
@Override
protected boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
protected int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos) {
List<Integer> slots = new ArrayList<>();
slots.add(VacuumArcFurnaceControllerEntity.INPUT_SLOT);
slots.add(VacuumArcFurnaceControllerEntity.SECONDARY_INPUT_SLOT);
slots.add(VacuumArcFurnaceControllerEntity.OUTPUT_SLOT);
VacuumArcFurnaceControllerEntity vacuumArcFurnaceEntity = ((VacuumArcFurnaceControllerEntity) level.getBlockEntity(pos));
if (vacuumArcFurnaceEntity != null)
return getRedstoneSignalFromItemHandler(vacuumArcFurnaceEntity.inventory, slots);
return 0;
}
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
if (level.isClientSide) {
return null;
}
return createTickerHelper(blockEntityType, ModBlockEntities.VACUUM_ARC_FURNACE_ENTITY.get(), (level1, blockPos, blockState, vacuumArcFurnaceEntity) -> vacuumArcFurnaceEntity.tick(level1, blockPos, blockState));
}
@Override
protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}
@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}
@Override
public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()).setValue(LIT, false).setValue(FORMED, false);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, LIT, FORMED);
}
}

View File

@@ -7,9 +7,10 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.util.IMultiblockPart;
import org.jetbrains.annotations.Nullable;
public class EAFPartEntity extends BlockEntity {
public class EAFPartEntity extends BlockEntity implements IMultiblockPart {
@Nullable private BlockPos controllerPos;
public EAFPartEntity(BlockPos pos, BlockState blockState) {

View File

@@ -8,12 +8,9 @@ import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Containers;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -23,26 +20,24 @@ import net.minecraft.world.item.crafting.*;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.energy.IEnergyStorage;
import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.xevianlight.aphelion.block.custom.ArcFurnaceCasingBlock;
import net.xevianlight.aphelion.block.custom.ElectricArcFurnace;
import net.xevianlight.aphelion.block.entity.energy.ModEnergyStorage;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.core.init.ModBlocks;
import net.xevianlight.aphelion.recipe.ElectricArcFurnaceRecipe;
import net.xevianlight.aphelion.recipe.ElectricArcFurnaceRecipeInput;
import net.xevianlight.aphelion.recipe.ModRecipes;
import net.xevianlight.aphelion.screen.ElectricArcFurnaceMenu;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.MultiblockHelper;
import net.xevianlight.aphelion.util.SidedSlotHandler;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvider {
@@ -108,7 +103,7 @@ public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvide
public void tick(Level level, BlockPos pos, BlockState blockState) {
if (!blockState.getValue(ElectricArcFurnace.FORMED))
if (!blockState.getValue(AphelionBlockStateProperties.FORMED))
return;
chargeFromItem();
@@ -316,6 +311,7 @@ public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvide
}
private final IItemHandler fullHandler = new SidedSlotHandler(inventory, new int[]{INPUT_SLOT, SECONDARY_INPUT_SLOT, OUTPUT_SLOT, ENERGY_SLOT}, true, true);
private final IItemHandler emptyJeiHandler = new SidedSlotHandler(inventory, new int[]{}, false, false);
private final IItemHandler inputHandler = new SidedSlotHandler(inventory, new int[]{0,1}, true, true);
private final IItemHandler outputHandler = new SidedSlotHandler(inventory, new int[]{2,3}, false, true);
private final IItemHandler jadeHandler = new SidedSlotHandler(inventory, new int[]{0}, false, false);
@@ -325,13 +321,18 @@ public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvide
}
public IEnergyStorage getEnergyStorage(@Nullable Direction direction) {
return ENERGY_STORAGE;
}
public IEnergyStorage getTrueEnergyStorage(@Nullable Direction direction) {
return this.ENERGY_STORAGE;
}
private final ModEnergyStorage ENERGY_STORAGE = createEnergyStorage();
private final ModEnergyStorage ENERGY_STORAGE = createEnergyStorage(ENERGY_CAPACITY, MAX_TRANSFER);
private final ModEnergyStorage NULL_ENERGY_STORAGE = createEnergyStorage(0, 0);
private ModEnergyStorage createEnergyStorage() {
return new ModEnergyStorage(ENERGY_CAPACITY, MAX_TRANSFER) {
private ModEnergyStorage createEnergyStorage(int capacity, int transfer) {
return new ModEnergyStorage(capacity, transfer) {
@Override
public void onEnergyChanged() {
setChanged();
@@ -378,7 +379,7 @@ public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvide
@Override
public @Nullable AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) {
return new ElectricArcFurnaceMenu(i, inventory, this, this.data);
return new ElectricArcFurnaceMenu(i, inventory, this, this.data);
}
@Nullable
@@ -403,132 +404,8 @@ public class ElectricArcFurnaceEntity extends BlockEntity implements MenuProvide
};
}
/// MULTIBLOCK
/// LOGIC
/// BELOW
private boolean formed = false; // cached runtime state
private static final BlockPos[] SHAPE = new BlockPos[] {
new BlockPos(1, 0, 0),
new BlockPos(0, 0, 1),
new BlockPos(1, 0, 1),
new BlockPos(0, 1, 0),
new BlockPos(1, 1, 0),
new BlockPos(0, 1, 1),
new BlockPos(1, 1, 1),
public static final MultiblockHelper.ShapePart[] SHAPE = new MultiblockHelper.ShapePart[] {
new MultiblockHelper.ShapePart(new BlockPos(3,0,3), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
// new MultiblockHelper.ShapePart(new BlockPos(-1,0,0), s -> s.is(Blocks.AMETHYST_BLOCK))
};
private List<BlockPos> getPartPositions() {
Direction facing = getBlockState().getValue(BlockStateProperties.HORIZONTAL_FACING);
BlockPos origin = getBlockPos();
List<BlockPos> out = new ArrayList<>(SHAPE.length);
for (BlockPos off : SHAPE) {
BlockPos ro = rotateY(off, facing);
out.add(origin.offset(ro));
}
return out;
}
private boolean structureMatches() {
if (level == null) return false;
for (BlockPos p : getPartPositions()) {
BlockState st = level.getBlockState(p);
// Accept only your casing/part blocks
if (!(st.getBlock() instanceof ArcFurnaceCasingBlock)) {
return false;
}
}
return true;
}
public boolean isFormed() {
return formed;
}
public void tryForm() {
if (level == null || level.isClientSide()) return;
if (!(getBlockState().getBlock() instanceof ElectricArcFurnace)) return; // don't form if not actually present
boolean valid = structureMatches();
if (valid && !formed) {
formed = true;
setFormedState(true);
linkParts();
} else if (!valid && formed) {
unform();
}
}
public void unform() {
if (level == null || level.isClientSide()) return;
formed = false;
setFormedState(false);
unlinkParts();
}
public void unformForRemoval() {
if (level == null || level.isClientSide()) return;
formed = false;
unlinkParts();
}
private void setFormedState(boolean value) {
BlockState state = getBlockState();
if (state.hasProperty(ElectricArcFurnace.FORMED) && state.getValue(ElectricArcFurnace.FORMED) != value) {
level.setBlock(worldPosition, state.setValue(ElectricArcFurnace.FORMED, value), 3);
}
}
private void linkParts() {
if (level == null || level.isClientSide()) return;
for (BlockPos p : getPartPositions()) {
BlockState st = level.getBlockState(p);
if (st.getBlock() instanceof ArcFurnaceCasingBlock && st.hasProperty(ArcFurnaceCasingBlock.FORMED)) {
if (!st.getValue(ArcFurnaceCasingBlock.FORMED)) {
level.setBlock(p, st.setValue(ArcFurnaceCasingBlock.FORMED, true), 3);
}
}
BlockEntity be = level.getBlockEntity(p);
if (be instanceof EAFPartEntity part) {
part.setControllerPos(worldPosition);
part.setChanged();
}
}
}
private void unlinkParts() {
if (level == null || level.isClientSide()) return;
for (BlockPos p : getPartPositions()) {
BlockState st = level.getBlockState(p);
if (st.getBlock() instanceof ArcFurnaceCasingBlock && st.hasProperty(ArcFurnaceCasingBlock.FORMED)) {
if (st.getValue(ArcFurnaceCasingBlock.FORMED)) {
level.setBlock(p, st.setValue(ArcFurnaceCasingBlock.FORMED, false), 3);
}
}
BlockEntity be = level.getBlockEntity(p);
if (be instanceof EAFPartEntity part && worldPosition.equals(part.getControllerPos())) {
part.setControllerPos(null);
part.setChanged();
}
}
}
}

View File

@@ -0,0 +1,507 @@
package net.xevianlight.aphelion.block.entity.custom;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.Containers;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.BlastingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.item.crafting.SingleRecipeInput;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.energy.IEnergyStorage;
import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.xevianlight.aphelion.block.custom.ElectricArcFurnace;
import net.xevianlight.aphelion.block.entity.energy.ModEnergyStorage;
import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.core.init.ModBlocks;
import net.xevianlight.aphelion.recipe.ElectricArcFurnaceRecipe;
import net.xevianlight.aphelion.recipe.ElectricArcFurnaceRecipeInput;
import net.xevianlight.aphelion.recipe.ModRecipes;
import net.xevianlight.aphelion.screen.VacuumArcFurnaceMenu;
import net.xevianlight.aphelion.screen.ElectricArcFurnaceMenu;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.IMultiblockController;
import net.xevianlight.aphelion.util.MultiblockHelper;
import net.xevianlight.aphelion.util.SidedSlotHandler;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public class VacuumArcFurnaceControllerEntity extends BlockEntity implements MenuProvider, IMultiblockController {
private final int SIZE = 4;
private int ENERGY_CAPACITY = 64000;
private int MAX_TRANSFER = 320;
private int progress = 0;
private int maxProgress = 100;
private final int DEFAULT_MAX_PROGRESS = 100;
private final ContainerData data;
private int MACHINE_ENERGY_COST = 20;
public static final int INPUT_SLOT = 0;
public static final int SECONDARY_INPUT_SLOT = 1;
public static final int OUTPUT_SLOT = 2;
public static final int ENERGY_SLOT = 3;
public VacuumArcFurnaceControllerEntity(BlockPos pos, BlockState blockState) {
super(ModBlockEntities.VACUUM_ARC_FURNACE_ENTITY.get(), pos, blockState);
this.data = new ContainerData() {
@Override
public int get(int index) {
return switch (index) {
case 0 -> VacuumArcFurnaceControllerEntity.this.progress;
case 1 -> VacuumArcFurnaceControllerEntity.this.maxProgress;
default -> 0;
};
}
@Override
public void set(int index, int pValue) {
switch (index) {
case 0: VacuumArcFurnaceControllerEntity.this.progress = pValue;
case 1: VacuumArcFurnaceControllerEntity.this.maxProgress = pValue;
}
}
@Override
public int getCount() {
return 2;
}
};
}
public final ItemStackHandler inventory = new ItemStackHandler(SIZE) {
@Override
protected void onContentsChanged(int slot) {
setChanged();
if(!level.isClientSide()) {
level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3);
}
}
// @Override
// public boolean isItemValid(int slot, ItemStack stack) {
// if (slot == ENERGY_SLOT) {
// var capability = stack.getCapability(Capabilities.EnergyStorage.ITEM);
// return capability != null;
// }
// return super.isItemValid(slot, stack);
// }
};
public void tick(Level level, BlockPos pos, BlockState blockState) {
if (!blockState.getValue(AphelionBlockStateProperties.FORMED))
return;
chargeFromItem();
if (inventory.getStackInSlot(SECONDARY_INPUT_SLOT).isEmpty()) {
// Secondary slot is empty, try furnace recipes
if (hasFurnaceRecipe(INPUT_SLOT) && hasEnoughEnergyToCraft(MACHINE_ENERGY_COST)) {
// Recipe detected! We have enough energy to process
progress++;
useEnergy();
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, true));
setChanged(level, pos, blockState);
if (hasCraftingFinished()) {
outputBlastingResult(INPUT_SLOT, OUTPUT_SLOT);
resetProgress();
}
} else if (hasFurnaceRecipe(INPUT_SLOT) && !hasEnoughEnergyToCraft(MACHINE_ENERGY_COST)) {
// Recipe detected but we ran out of power
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, false));
setChanged(level, pos, blockState);
progress = progress > 0 ? progress - 1 : 0;
} else {
// Invalid recipe
resetProgress();
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, false));
setChanged(level, pos, blockState);
}
} else {
// Secondary slot is NOT empty, try alloying recipes
if (hasAlloyingRecipe(INPUT_SLOT, SECONDARY_INPUT_SLOT)) {
if (hasEnoughEnergyToCraft(MACHINE_ENERGY_COST)) {
// Alloy recipe detected! We have enough energy to process
progress++;
useEnergy();
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, true));
setChanged(level, pos, blockState);
if (hasCraftingFinished()) {
outputAlloyingResult(INPUT_SLOT, SECONDARY_INPUT_SLOT, OUTPUT_SLOT);
resetProgress();
}
} else {
// Recipe detected but we ran out of power
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, false));
setChanged(level, pos, blockState);
progress = progress > 0 ? progress - 1 : 0;
}
} else {
// Invalid recipe
resetProgress();
level.setBlockAndUpdate(pos, blockState.setValue(ElectricArcFurnace.LIT, false));
setChanged(level, pos, blockState);
}
}
}
private void chargeFromItem() {
ItemStack stack;
try {
stack = inventory.getStackInSlot(ENERGY_SLOT);
if (stack.isEmpty()) return;
IEnergyStorage itemEnergy = stack.getCapability(Capabilities.EnergyStorage.ITEM);
if (itemEnergy == null || !itemEnergy.canExtract()) return;
int freeCapacity = ENERGY_STORAGE.getMaxEnergyStored() - ENERGY_STORAGE.getEnergyStored();
if (freeCapacity <= 0) return;
int maxMove = Math.min(MAX_TRANSFER, freeCapacity);
// Simulate extraction first
int canExtract = itemEnergy.extractEnergy(maxMove, true);
if (canExtract <= 0) return;
// Receive into block (simulate then execute is safest)
int canReceive = ENERGY_STORAGE.receiveEnergy(canExtract, true);
if (canReceive <= 0) return;
int extracted = itemEnergy.extractEnergy(canReceive, false);
ENERGY_STORAGE.receiveEnergy(maxMove, false);
setChanged();
} catch (Exception e) {
}
}
private void outputBlastingResult(int slot, int resultSlot) {
Optional<RecipeHolder<BlastingRecipe>> recipe = getFurnaceRecipe(inventory.getStackInSlot(slot));
ItemStack output = recipe.get().value().getResultItem(null);
// 2x multiplier for smelting recipes
inventory.extractItem(slot, 1, false);
inventory.setStackInSlot(resultSlot, new ItemStack(output.getItem(),
inventory.getStackInSlot(resultSlot).getCount() + (output.getCount() * 2)));
}
private void outputAlloyingResult(int inputSlot, int secondaryInputSlot, int outputSlot) {
Optional<RecipeHolder<ElectricArcFurnaceRecipe>> recipe = getAlloyingRecipe(inventory.getStackInSlot(inputSlot), inventory.getStackInSlot(secondaryInputSlot));
ItemStack output = recipe.get().value().getResultItem(null);
inventory.extractItem(inputSlot, recipe.get().value().baseCount(), false);
inventory.extractItem(secondaryInputSlot, recipe.get().value().alloyCount(), false);
inventory.setStackInSlot(outputSlot, new ItemStack(output.getItem(), inventory.getStackInSlot(outputSlot).getCount() + (output.getCount())));
}
private void resetProgress() {
this.progress = 0;
this.maxProgress = DEFAULT_MAX_PROGRESS;
}
private void useEnergy() {
this.ENERGY_STORAGE.extractEnergy(MACHINE_ENERGY_COST, false);
}
private boolean hasEnoughEnergyToCraft(int energyCost) {
return ENERGY_STORAGE.getEnergyStored() >= energyCost;
}
private boolean canInsertItemIntoOutputSlot(ItemStack output, int slot) {
return inventory.getStackInSlot(slot).isEmpty() ||
inventory.getStackInSlot(slot).getItem() == output.getItem();
}
private boolean canInsertAmountIntoOutputSlot(int count, int slot) {
int maxCount = inventory.getStackInSlot(slot).isEmpty() ? 64 : inventory.getStackInSlot(slot).getMaxStackSize();
int currentCount = inventory.getStackInSlot(slot).getCount();
return maxCount >= currentCount + count;
}
private boolean isOutputSlotEmptyOrReceivable(int slot) {
return this.inventory.getStackInSlot(slot).isEmpty() ||
this.inventory.getStackInSlot(slot).getCount() < this.inventory.getStackInSlot(slot).getMaxStackSize();
}
private boolean hasCraftingFinished() {
maxProgress = DEFAULT_MAX_PROGRESS;
return this.progress >= this.maxProgress;
}
private boolean hasAlloyingRecipe(int slotBase, int slotAlloy) {
ItemStack baseStack = inventory.getStackInSlot(slotBase);
ItemStack alloyStack = inventory.getStackInSlot(slotAlloy);
Optional<RecipeHolder<ElectricArcFurnaceRecipe>> opt =
getAlloyingRecipe(baseStack, alloyStack);
if (opt.isEmpty()) return false;
ElectricArcFurnaceRecipe recipe = opt.get().value();
// 1) Check required input counts
if (baseStack.getCount() < recipe.baseCount()) return false;
if (alloyStack.getCount() < recipe.alloyCount()) return false;
// 2) Check output slot compatibility + space
ItemStack output = recipe.getResultItem(null);
maxProgress = recipe.cookTime();
return canInsertItemIntoOutputSlot(output, OUTPUT_SLOT)
&& canInsertAmountIntoOutputSlot(output.getCount(), OUTPUT_SLOT);
}
private Optional<RecipeHolder<ElectricArcFurnaceRecipe>> getAlloyingRecipe(ItemStack base, ItemStack alloy) {
if (base.isEmpty()) return Optional.empty();
if (alloy.isEmpty()) return Optional.empty();
return this.level.getRecipeManager()
.getRecipeFor(
ModRecipes.ELECTRIC_ARC_FURNACE_RECIPE_TYPE.get(),
new ElectricArcFurnaceRecipeInput(base, alloy),
level
);
}
private boolean hasFurnaceRecipe(int slot) {
Optional<RecipeHolder<BlastingRecipe>> recipe = getFurnaceRecipe(new ItemStack(inventory.getStackInSlot(slot).getItem().asItem(), 1));
if (recipe.isEmpty())
return false;
ItemStack output = recipe.get().value().getResultItem(null);
return canInsertAmountIntoOutputSlot(output.getCount() * 2, OUTPUT_SLOT) && canInsertItemIntoOutputSlot(output, OUTPUT_SLOT);
}
private Optional<RecipeHolder<BlastingRecipe>> getFurnaceRecipe(ItemStack stack) {
if (stack.isEmpty()) return Optional.empty();
return this.level.getRecipeManager()
.getRecipeFor(RecipeType.BLASTING, new SingleRecipeInput(stack), level);
}
public void sendUpdate() {
setChanged();
level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3);
}
private final IItemHandler fullHandler = new SidedSlotHandler(inventory, new int[]{INPUT_SLOT, SECONDARY_INPUT_SLOT, OUTPUT_SLOT, ENERGY_SLOT}, true, true);
private final IItemHandler emptyJeiHandler = new SidedSlotHandler(inventory, new int[]{}, false, false);
private final IItemHandler inputHandler = new SidedSlotHandler(inventory, new int[]{0,1}, true, true);
private final IItemHandler outputHandler = new SidedSlotHandler(inventory, new int[]{2,3}, false, true);
private final IItemHandler jadeHandler = new SidedSlotHandler(inventory, new int[]{0}, false, false);
public IItemHandler getItemHandler(Direction direction) {
if (direction != null)
return isFormed() ? fullHandler : null;
return isFormed() ? fullHandler : emptyJeiHandler;
}
public IEnergyStorage getEnergyStorage(@Nullable Direction direction) {
if (direction == null)
return isFormed() ? ENERGY_STORAGE : NULL_ENERGY_STORAGE;
return isFormed() ? ENERGY_STORAGE : null;
}
public IEnergyStorage getTrueEnergyStorage(@Nullable Direction direction) {
return this.ENERGY_STORAGE;
}
private final ModEnergyStorage ENERGY_STORAGE = createEnergyStorage(ENERGY_CAPACITY, MAX_TRANSFER);
private final ModEnergyStorage NULL_ENERGY_STORAGE = createEnergyStorage(0, 0);
private ModEnergyStorage createEnergyStorage(int capacity, int transfer) {
return new ModEnergyStorage(capacity, transfer) {
@Override
public void onEnergyChanged() {
setChanged();
getLevel().sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3);
}
};
}
public ItemStackHandler getInventory() {
return inventory;
}
public void drops() {
SimpleContainer inv = new SimpleContainer(inventory.getSlots());
for(int i = 0; i < inventory.getSlots(); i++) {
inv.setItem(i, inventory.getStackInSlot(i));
}
Containers.dropContents(this.level, this.worldPosition, inv);
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider pRegistries) {
tag.put("inventory", inventory.serializeNBT(pRegistries));
tag.putInt("electric_arc_furnace.progress", progress);
tag.putInt("electric_arc_furnace.maxProgress", maxProgress);
tag.putInt("electric_arc_furnace.energy", ENERGY_STORAGE.getEnergyStored());
super.saveAdditional(tag, pRegistries);
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider pRegistries) {
super.loadAdditional(tag, pRegistries);
inventory.deserializeNBT(pRegistries, tag.getCompound("inventory"));
progress = tag.getInt("electric_arc_furnace.progress");
maxProgress = tag.getInt("electric_arc_furnace.maxProgress");
ENERGY_STORAGE.setEnergy(tag.getInt("electric_arc_furnace.energy"));
}
@Override
public Component getDisplayName() {
return null;
}
@Override
public @Nullable AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) {
if (isFormed())
return new VacuumArcFurnaceMenu(i, inventory, this, this.data);
return new ElectricArcFurnaceMenu(i, inventory, this, this.data);
}
@Nullable
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider pRegistries) {
return saveWithoutMetadata(pRegistries);
}
private static BlockPos rotateY(BlockPos off, Direction facing) {
// Assumes "default" shape faces NORTH.
return switch (facing) {
case NORTH -> off;
case EAST -> new BlockPos(-off.getZ(), off.getY(), off.getX());
case SOUTH -> new BlockPos(-off.getX(), off.getY(), -off.getZ());
case WEST -> new BlockPos(off.getZ(), off.getY(), -off.getX());
default -> off;
};
}
public static final MultiblockHelper.ShapePart[] SHAPE = new MultiblockHelper.ShapePart[] {
//Layer -1
new MultiblockHelper.ShapePart(new BlockPos(1,-1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,-1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,-1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,-1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,-1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,-1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,-1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,-1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,-1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
//Layer 0
new MultiblockHelper.ShapePart(new BlockPos(1,0,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,0,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,0,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,0,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,0,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,0,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,0,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,0,1), s -> s.is(Blocks.AIR)),
//Layer 1
new MultiblockHelper.ShapePart(new BlockPos(1,1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(1,1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(-1,1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,1,0), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,1,1), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
new MultiblockHelper.ShapePart(new BlockPos(0,1,2), s -> s.is(ModBlocks.ARC_FURNACE_CASING_BLOCK)),
// new MultiblockHelper.ShapePart(new BlockPos(-1,0,0), s -> s.is(Blocks.AMETHYST_BLOCK))
};
/// MULTIBLOCK
/// LOGIC
/// BELOW
// record ShapePart (BlockPos offset, Predicate<BlockState> rule) {}
private boolean formed = false; // cached runtime state
// Assumes the default state is NORTH.
// (Left -Right, Up -Down, Back -Front)
// private static final BlockPos[] SHAPE = new BlockPos[] {
// new BlockPos(1, 0, 0),
// new BlockPos(0, 0, 1),
// new BlockPos(1, 0, 1),
//
// new BlockPos(0, 1, 0),
// new BlockPos(1, 1, 0),
// new BlockPos(0, 1, 1),
// new BlockPos(1, 1, 1),
// };
public boolean isFormed() {
return getBlockState().hasProperty(AphelionBlockStateProperties.FORMED)
&& getBlockState().getValue(AphelionBlockStateProperties.FORMED);
}
@Override
public void setFormed(boolean formed) {
this.formed = formed;
invalidateCapabilities();
if (!formed) {
drops();
}
}
private void setFormedState(boolean value) {
BlockState state = getBlockState();
if (state.hasProperty(ElectricArcFurnace.FORMED) && state.getValue(ElectricArcFurnace.FORMED) != value) {
level.setBlock(worldPosition, state.setValue(ElectricArcFurnace.FORMED, value), 3);
}
}
}