/* * Ex Deorum * Copyright (c) 2024 thedarkcolour * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package thedarkcolour.exdeorum.blockentity; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; 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.material.Fluid; import net.minecraft.world.level.material.Fluids; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.common.util.Lazy; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.FluidUtil; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.templates.FluidTank; import net.minecraftforge.items.IItemHandler; import net.minecraftforge.items.ItemStackHandler; import net.minecraftforge.registries.ForgeRegistries; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import thedarkcolour.exdeorum.blockentity.helper.FluidHelper; import thedarkcolour.exdeorum.config.EConfig; import thedarkcolour.exdeorum.material.AbstractCrucibleMaterial; import thedarkcolour.exdeorum.recipe.crucible.CrucibleRecipe; import thedarkcolour.exdeorum.registry.EBlockEntities; import thedarkcolour.exdeorum.registry.EItems; import java.util.HashMap; import java.util.function.Consumer; @SuppressWarnings("deprecation") public abstract class AbstractCrucibleBlockEntity extends EBlockEntity { public static final Lazy> MELT_OVERRIDES = Lazy.concurrentOf(() -> { var map = new HashMap(); addMeltOverrides(map); return map; }); public static final int MAX_SOLIDS = 1000; public static final int MAX_FLUID_CAPACITY = 4000; private final AbstractCrucibleBlockEntity.ItemHandler item = new AbstractCrucibleBlockEntity.ItemHandler(); private final AbstractCrucibleBlockEntity.FluidHandler tank = new AbstractCrucibleBlockEntity.FluidHandler(); // Capabilities private final LazyOptional itemHandler = LazyOptional.of(() -> this.item); private final LazyOptional fluidHandler = LazyOptional.of(() -> this.tank); @Nullable private Block lastMelted; @Nullable private Fluid fluid = null; private short solids; private boolean needsLightUpdate; public final boolean transparent; public AbstractCrucibleBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); this.transparent = AbstractCrucibleMaterial.TRANSPARENT_CRUCIBLES.contains(state.getBlock()); } @NotNull @Override public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { if (!this.remove) { if (cap == ForgeCapabilities.FLUID_HANDLER) { return this.fluidHandler.cast(); } else if (cap == ForgeCapabilities.ITEM_HANDLER) { return this.itemHandler.cast(); } } return super.getCapability(cap, side); } @Override public void invalidateCaps() { super.invalidateCaps(); this.fluidHandler.invalidate(); this.itemHandler.invalidate(); } // NBT @Override public void saveAdditional(CompoundTag nbt) { super.saveAdditional(nbt); nbt.put("Tank", this.tank.writeToNBT(new CompoundTag())); nbt.putString("LastMelted", ForgeRegistries.BLOCKS.getKey(this.lastMelted).toString()); nbt.putString("Fluid", ForgeRegistries.FLUIDS.getKey(this.fluid).toString()); nbt.putShort("Solids", this.solids); } @Override public void load(CompoundTag nbt) { super.load(nbt); this.tank.readFromNBT(nbt.getCompound("Tank")); this.lastMelted = ForgeRegistries.BLOCKS.getValue(new ResourceLocation(nbt.getString("LastMelted"))); this.fluid = ForgeRegistries.FLUIDS.getValue(new ResourceLocation(nbt.getString("Fluid"))); this.solids = nbt.getShort("Solids"); this.needsLightUpdate = true; } @Override public void writeVisualData(FriendlyByteBuf buffer) { buffer.writeId(BuiltInRegistries.FLUID, this.tank.getFluid().getFluid()); buffer.writeVarInt(this.tank.getFluidAmount()); buffer.writeId(BuiltInRegistries.BLOCK, this.lastMelted != null ? this.lastMelted : Blocks.AIR); buffer.writeShort(this.solids); } @Override public void readVisualData(FriendlyByteBuf buffer) { Fluid fluid = buffer.readById(BuiltInRegistries.FLUID); if (fluid == null) { this.tank.setFluid(FluidStack.EMPTY); buffer.readVarInt(); } else { this.tank.setFluid(new FluidStack(fluid, buffer.readVarInt())); } var lastMelted = buffer.readById(BuiltInRegistries.BLOCK); this.lastMelted = lastMelted == Blocks.AIR ? null : lastMelted; this.solids = buffer.readShort(); } public InteractionResult use(Level level, Player player, InteractionHand hand) { var playerItem = player.getItemInHand(hand); if (playerItem.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).isPresent()) { return FluidUtil.interactWithFluidHandler(player, hand, this.tank) ? InteractionResult.sidedSuccess(level.isClientSide) : InteractionResult.PASS; } if (!level.isClientSide) { if (playerItem.getItem() == Items.GLASS_BOTTLE && this.getType() == EBlockEntities.WATER_CRUCIBLE.get() && EConfig.SERVER.allowWaterBottleTransfer.get()) { var fluid = new FluidStack(Fluids.WATER, 250); if (this.tank.drain(fluid, IFluidHandler.FluidAction.SIMULATE).getAmount() == 250) { BarrelBlockEntity.extractWaterBottle(this.tank, level, player, playerItem, fluid); markUpdated(); } } else if (canInsertItem(playerItem)) { tryMelt(playerItem, player.getAbilities().instabuild ? stack -> {} : stack -> stack.shrink(1)); } } return InteractionResult.sidedSuccess(level.isClientSide); } // Gets a crucible recipe, using the cache if possible @Nullable protected abstract CrucibleRecipe getRecipe(ItemStack item); /** * Tries to melt the specified item into the crucible. * * @param item Item to melt * @param shrinkAction What to do when item is melted */ private void tryMelt(ItemStack item, Consumer shrinkAction) { if (item.isEmpty()) return; var meltItem = item.getItem(); var recipe = getRecipe(item); if (recipe == null) { this.item.setStackInSlot(0, ItemStack.EMPTY); return; } var result = recipe.getResult(); var contained = this.tank.getFluid(); shrinkAction.accept(item); this.solids = (short) Math.min(this.solids + result.getAmount(), MAX_SOLIDS); if (contained.isEmpty()) { this.fluid = result.getFluid(); this.needsLightUpdate = true; } var melts = MELT_OVERRIDES.get(); if (melts.containsKey(meltItem)) { this.lastMelted = melts.get(meltItem); } else if (meltItem.getClass() == BlockItem.class) { this.lastMelted = ((BlockItem) meltItem).getBlock(); } else { // If we already have something else inside just use that instead of switching to default if (this.lastMelted == null) { this.lastMelted = getDefaultMeltBlock(); } } markUpdated(); } private boolean canInsertItem(ItemStack item) { if (item.isEmpty()) return false; var recipe = getRecipe(item); if (recipe != null) { var result = recipe.getResult(); var contained = this.tank.getFluid(); return (result.isFluidEqual(contained) || contained.isEmpty()) && result.getAmount() + this.solids <= MAX_SOLIDS; } return false; } public int getMeltingRate() { return 1; } public int getSolids() { return this.solids; } public FluidTank getTank() { return this.tank; } public abstract Block getDefaultMeltBlock(); @Nullable public Block getLastMelted() { return this.lastMelted; } @Override public void setRemoved() { this.itemHandler.invalidate(); this.fluidHandler.invalidate(); super.setRemoved(); } private static void addMeltOverrides(HashMap overrides) { overrides.put(Items.OAK_SAPLING, Blocks.OAK_LEAVES); overrides.put(Items.SPRUCE_SAPLING, Blocks.SPRUCE_LEAVES); overrides.put(Items.ACACIA_SAPLING, Blocks.ACACIA_LEAVES); overrides.put(Items.JUNGLE_SAPLING, Blocks.JUNGLE_LEAVES); overrides.put(Items.DARK_OAK_SAPLING, Blocks.DARK_OAK_LEAVES); overrides.put(Items.BIRCH_SAPLING, Blocks.BIRCH_LEAVES); overrides.put(Items.CHERRY_SAPLING, Blocks.CHERRY_LEAVES); overrides.put(Items.MANGROVE_PROPAGULE, Blocks.MANGROVE_LEAVES); overrides.put(Items.SWEET_BERRIES, Blocks.SPRUCE_LEAVES); overrides.put(Items.GLOW_BERRIES, Blocks.MOSS_BLOCK); overrides.put(EItems.GRASS_SEEDS.get(), Blocks.GRASS_BLOCK); overrides.put(EItems.MYCELIUM_SPORES.get(), Blocks.MYCELIUM); overrides.put(EItems.WARPED_NYLIUM_SPORES.get(), Blocks.WARPED_NYLIUM); overrides.put(EItems.CRIMSON_NYLIUM_SPORES.get(), Blocks.CRIMSON_NYLIUM); for (var sapling : ForgeRegistries.BLOCKS.getEntries()) { var item = sapling.getValue().asItem(); if (!overrides.containsKey(item)) { var key = sapling.getKey().location(); if (key.getPath().endsWith("sapling")) { try { overrides.put(item, ForgeRegistries.BLOCKS.getValue(new ResourceLocation(key.getNamespace(), key.getPath().replace("sapling", "leaves")))); } catch (Exception ignored) { } } } } } private static class FluidHandler extends FluidHelper { public FluidHandler() { super(MAX_FLUID_CAPACITY); } @Override public boolean isFluidValid(FluidStack stack) { return false; } } // inner class private class ItemHandler extends ItemStackHandler { @Override protected void onContentsChanged(int slot) { tryMelt(getItem(), item -> setStackInSlot(0, ItemStack.EMPTY)); } @Override protected int getStackLimit(int slot, @NotNull ItemStack stack) { return 1; } @Override public boolean isItemValid(int slot, @NotNull ItemStack stack) { return canInsertItem(stack); } public ItemStack getItem() { return this.stacks.get(0); } } // Only ticks on server public static class Ticker implements BlockEntityTicker { @Override public void tick(Level level, BlockPos pos, BlockState state, AbstractCrucibleBlockEntity crucible) { if (crucible.needsLightUpdate) { level.getLightEngine().checkBlock(crucible.worldPosition); crucible.needsLightUpdate = false; } // Update twice per tick if (!level.isClientSide) { var tank = crucible.tank; if ((level.getGameTime() % 10L) == 0L) { short delta = (short) Math.min(crucible.solids, crucible.getMeltingRate()); // Skip if no heat if (delta <= 0) return; if (tank.getSpace() >= delta) { // Remove solids crucible.solids -= delta; // Add lava if (tank.isEmpty()) { if (crucible.fluid != null) { tank.setFluid(new FluidStack(crucible.fluid, delta)); crucible.needsLightUpdate = true; } } else { tank.getFluid().grow(delta); } // Sync to client crucible.markUpdated(); } } if (tank.getFluidAmount() < MAX_FLUID_CAPACITY && crucible instanceof WaterCrucibleBlockEntity && level.isRainingAt(pos.above())) { BarrelBlockEntity.fillRainWater(crucible, tank); } } } } }