• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyclopsMC / IntegratedDynamics / 24301014474

12 Apr 2026 07:00AM UTC coverage: 53.89% (+0.07%) from 53.819%
24301014474

Pull #1655

github

web-flow
Fix mechanical drying basin to process item and fluid inputs with priority ordering

Agent-Logs-Url: https://github.com/CyclopsMC/IntegratedDynamics/sessions/177cd332-e686-4796-8b4b-b790df5fe999

Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com>
Pull Request #1655: [WIP] Fix input selection issue in mechanical drying basin

3086 of 8959 branches covered (34.45%)

Branch coverage included in aggregate %.

18829 of 31707 relevant lines covered (59.38%)

3.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.17
/src/main/java/org/cyclops/integrateddynamics/blockentity/BlockEntityDryingBasin.java
1
package org.cyclops.integrateddynamics.blockentity;
2

3
import net.minecraft.core.BlockPos;
4
import net.minecraft.core.Direction;
5
import net.minecraft.core.NonNullList;
6
import net.minecraft.core.particles.BlockParticleOption;
7
import net.minecraft.core.particles.ItemParticleOption;
8
import net.minecraft.core.particles.ParticleTypes;
9
import net.minecraft.world.Containers;
10
import net.minecraft.world.item.ItemStack;
11
import net.minecraft.world.item.crafting.RecipeHolder;
12
import net.minecraft.world.item.crafting.RecipeType;
13
import net.minecraft.world.level.Level;
14
import net.minecraft.world.level.block.Blocks;
15
import net.minecraft.world.level.block.entity.BlockEntityTicker;
16
import net.minecraft.world.level.block.entity.BlockEntityType;
17
import net.minecraft.world.level.block.state.BlockState;
18
import net.minecraft.world.level.storage.ValueInput;
19
import net.minecraft.world.level.storage.ValueOutput;
20
import net.neoforged.neoforge.capabilities.Capabilities;
21
import net.neoforged.neoforge.fluids.FluidStack;
22
import net.neoforged.neoforge.transfer.fluid.FluidResource;
23
import net.neoforged.neoforge.transfer.item.VanillaContainerWrapper;
24
import net.neoforged.neoforge.transfer.transaction.Transaction;
25
import org.apache.commons.lang3.tuple.Pair;
26
import org.cyclops.cyclopscore.blockentity.BlockEntityTickerDelayed;
27
import org.cyclops.cyclopscore.blockentity.CyclopsBlockEntity;
28
import org.cyclops.cyclopscore.capability.registrar.BlockEntityCapabilityRegistrar;
29
import org.cyclops.cyclopscore.datastructure.SingleCache;
30
import org.cyclops.cyclopscore.fluid.SingleUseTank;
31
import org.cyclops.cyclopscore.helper.IModHelpers;
32
import org.cyclops.cyclopscore.helper.IModHelpersNeoForge;
33
import org.cyclops.cyclopscore.inventory.SimpleInventory;
34
import org.cyclops.cyclopscore.inventory.SimpleInventoryState;
35
import org.cyclops.cyclopscore.persist.nbt.NBTPersist;
36
import org.cyclops.cyclopscore.recipe.type.IInventoryFluid;
37
import org.cyclops.cyclopscore.recipe.type.InventoryFluid;
38
import org.cyclops.integrateddynamics.IntegratedDynamics;
39
import org.cyclops.integrateddynamics.RegistryEntries;
40
import org.cyclops.integrateddynamics.core.recipe.handler.RecipeHandlerDryingBasin;
41
import org.cyclops.integrateddynamics.core.recipe.type.RecipeDryingBasin;
42

43
import java.util.Optional;
44
import java.util.function.Supplier;
45

46
/**
47
 * A part entity for drying stuff.
48
 * @author rubensworks
49
 */
50
public class BlockEntityDryingBasin extends CyclopsBlockEntity {
51

52
    private static final int WOOD_IGNITION_TEMPERATURE = 573; // 300 degrees celcius
53

54
    private final SimpleInventory inventory;
55
    private final SingleUseTank tank;
56

57
    @NBTPersist
2✔
58
    private Float randomRotation = 0F;
2✔
59
    @NBTPersist
3✔
60
    private int progress = 0;
61
    @NBTPersist
3✔
62
    private int fire = 0;
63

64
    private SingleCache<Pair<ItemStack, FluidStack>, Optional<RecipeHolder<RecipeDryingBasin>>> recipeCache;
65

66
    public BlockEntityDryingBasin(BlockPos blockPos, BlockState blockState) {
67
        super(RegistryEntries.BLOCK_ENTITY_DRYING_BASIN.get(), blockPos, blockState);
7✔
68

69
        // Create inventory and tank
70
        this.inventory = new SimpleInventory(1, 1) {
19✔
71
            @Override
72
            public boolean canPlaceItem(int i, ItemStack itemstack) {
73
                return getItem(0).isEmpty();
×
74
            }
75

76
            @Override
77
            public void setItem(int slotId, ItemStack itemstack) {
78
                super.setItem(slotId, itemstack);
4✔
79
                BlockEntityDryingBasin.this.randomRotation = level.getRandom().nextFloat() * 360;
11✔
80
                sendUpdate();
3✔
81
            }
1✔
82
        };
83
        this.tank = new SingleUseTank(IModHelpersNeoForge.get().getFluidHelpers().getBucketVolume());
8✔
84

85
        // Add dirty mark listeners to inventory and tank
86
        this.inventory.addDirtyMarkListener(this::sendUpdate);
5✔
87
        this.tank.addDirtyMarkListener(this.inventory::setChanged);
9✔
88

89
        // Efficient cache to retrieve the current craftable recipe.
90
        recipeCache = new SingleCache<>(new SingleCache.ICacheUpdater<Pair<ItemStack, FluidStack>, Optional<RecipeHolder<RecipeDryingBasin>>>() {
18✔
91
            @Override
92
            public Optional<RecipeHolder<RecipeDryingBasin>> getNewValue(Pair<ItemStack, FluidStack> key) {
93
                // First, try matching with both item and fluid inputs
94
                IInventoryFluid recipeInput = new InventoryFluid(
8✔
95
                        NonNullList.of(ItemStack.EMPTY, key.getLeft()),
10✔
96
                        NonNullList.of(FluidStack.EMPTY, key.getRight()));
6✔
97
                Optional<RecipeHolder<RecipeDryingBasin>> recipe = IModHelpers.get().getCraftingHelpers().findRecipe(getRegistry(), recipeInput, getLevel());
11✔
98
                if (recipe.isPresent()) {
3✔
99
                    return recipe;
2✔
100
                }
101

102
                // If both item and fluid are present but no combined recipe was found,
103
                // try item-only, to handle the case where the machine has two separate
104
                // types of inputs and should process one at a time.
105
                if (!key.getLeft().isEmpty() && !key.getRight().isEmpty()) {
10!
106
                    recipeInput = new InventoryFluid(
×
107
                            NonNullList.of(ItemStack.EMPTY, key.getLeft()),
×
108
                            NonNullList.of(FluidStack.EMPTY, FluidStack.EMPTY));
×
109
                    return IModHelpers.get().getCraftingHelpers().findRecipe(getRegistry(), recipeInput, getLevel());
×
110
                }
111

112
                return Optional.empty();
2✔
113
            }
114

115
            @Override
116
            public boolean isKeyEqual(Pair<ItemStack, FluidStack> cacheKey, Pair<ItemStack, FluidStack> newKey) {
117
                return cacheKey == null || newKey == null ||
6!
118
                        (ItemStack.matches(cacheKey.getLeft(), newKey.getLeft()) &&
8✔
119
                                FluidStack.matches(cacheKey.getRight(), newKey.getRight()));
10!
120
            }
121
        });
122
    }
1✔
123

124
    public static class CapabilityRegistrar extends BlockEntityCapabilityRegistrar<BlockEntityDryingBasin> {
125
        public CapabilityRegistrar(Supplier<BlockEntityType<? extends BlockEntityDryingBasin>> blockEntityType) {
126
            super(blockEntityType);
3✔
127
        }
1✔
128

129
        @Override
130
        public void populate() {
131
            add(
4✔
132
                    net.neoforged.neoforge.capabilities.Capabilities.Item.BLOCK,
133
                    (blockEntity, direction) -> VanillaContainerWrapper.of(blockEntity.getInventory())
×
134
            );
135
            add(
4✔
136
                    org.cyclops.commoncapabilities.api.capability.Capabilities.InventoryState.BLOCK,
137
                    (blockEntity, direction) -> new SimpleInventoryState(blockEntity.getInventory())
×
138
            );
139
            add(
4✔
140
                    Capabilities.Fluid.BLOCK,
141
                    (blockEntity, direction) -> blockEntity.getTank()
3✔
142
            );
143
            add(
4✔
144
                    org.cyclops.commoncapabilities.api.capability.Capabilities.RecipeHandler.BLOCK,
145
                    (blockEntity, direction) -> new RecipeHandlerDryingBasin<>(blockEntity::getLevel, RegistryEntries.RECIPETYPE_DRYING_BASIN.get())
×
146
            );
147
        }
1✔
148
    }
149

150
    public int getProgress() {
151
        return progress;
3✔
152
    }
153

154
    public void setProgress(int progress) {
155
        this.progress = progress;
3✔
156
    }
1✔
157

158
    public int getFire() {
159
        return fire;
3✔
160
    }
161

162
    public void setFire(int fire) {
163
        this.fire = fire;
3✔
164
    }
1✔
165

166
    public SimpleInventory getInventory() {
167
        return inventory;
3✔
168
    }
169

170
    public SingleUseTank getTank() {
171
        return tank;
3✔
172
    }
173

174
    @Override
175
    public void read(ValueInput input) {
176
        inventory.readFromNBT(input, "inventory");
5✔
177
        tank.deserialize(input, "tank");
5✔
178
        super.read(input);
3✔
179
    }
1✔
180

181
    @Override
182
    public void saveAdditional(ValueOutput output) {
183
        inventory.writeToNBT(output, "inventory");
5✔
184
        tank.serialize(output, "tank");
5✔
185
        super.saveAdditional(output);
3✔
186
    }
1✔
187

188
    protected RecipeType<RecipeDryingBasin> getRegistry() {
189
        return RegistryEntries.RECIPETYPE_DRYING_BASIN.get();
4✔
190
    }
191

192
    public Optional<RecipeHolder<RecipeDryingBasin>> getCurrentRecipe() {
193
        return recipeCache.get(Pair.of(getInventory().getItem(0).copy(), IModHelpersNeoForge.get().getFluidHelpers().copy(getTank().getFluid())));
17✔
194
    }
195

196
    /**
197
     * Get the random rotation for displaying the item.
198
     * @return The random rotation.
199
     */
200
    public float getRandomRotation() {
201
        return randomRotation;
×
202
    }
203

204
    @Override
205
    public void preRemoveSideEffects(BlockPos pos, BlockState state) {
206
        super.preRemoveSideEffects(pos, state);
×
207
        Containers.dropContents(level, pos, this.getInventory());
×
208
    }
×
209

210
    public static class TickerServer extends BlockEntityTickerDelayed<BlockEntityDryingBasin> {
3✔
211
        @Override
212
        protected void update(Level level, BlockPos pos, BlockState blockState, BlockEntityDryingBasin blockEntity) {
213
            super.update(level, pos, blockState, blockEntity);
6✔
214

215
            Optional<RecipeHolder<RecipeDryingBasin>> currentRecipe = blockEntity.getCurrentRecipe();
3✔
216
            if (!blockEntity.getTank().isEmpty() && blockEntity.getTank().getFluid().getFluid().getFluidType().getTemperature(blockEntity.getTank().getFluid()) >= WOOD_IGNITION_TEMPERATURE) {
15!
217
                blockEntity.setFire(blockEntity.getFire() + 1);
×
218
                if (blockEntity.getFire() >= 100) {
×
219
                    level.setBlockAndUpdate(pos, Blocks.FIRE.defaultBlockState());
×
220
                } else if (level.isEmptyBlock(pos.relative(Direction.UP)) && level.getRandom().nextInt(10) == 0) {
×
221
                    level.setBlockAndUpdate(pos.relative(Direction.UP), Blocks.FIRE.defaultBlockState());
×
222
                }
223

224
            } else if (currentRecipe.isPresent()) {
3✔
225
                RecipeDryingBasin recipe = currentRecipe.get().value();
6✔
226
                if (blockEntity.getProgress() >= recipe.getDuration()) {
5✔
227
                    // Consume input fluid
228
                    if (recipe.getInputFluid().isPresent()) {
4✔
229
                        int amount = IModHelpersNeoForge.get().getFluidHelpers().getAmount(recipe.getInputFluid().get());
8✔
230
                        try (var tx = Transaction.openRoot()) {
2✔
231
                            blockEntity.getTank().extract(FluidResource.of(recipe.getInputFluid().get()), amount, tx);
11✔
232
                            tx.commit();
2✔
233
                        }
234
                    }
235

236
                    // Produce output item
237
                    ItemStack output = recipe.getOutputItemFirst().orElse(ItemStack.EMPTY);
6✔
238
                    if (!output.isEmpty()) {
3!
239
                        output = output.copy();
3✔
240
                        blockEntity.getInventory().setItem(0, output);
6✔
241
                    } else {
242
                        blockEntity.getInventory().setItem(0, ItemStack.EMPTY);
×
243
                    }
244

245
                    // Produce output fluid
246
                    if (recipe.getOutputFluid().isPresent()) {
4!
247
                        int inserted;
248
                        try (var tx = Transaction.openRoot()) {
×
249
                            FluidStack fluidStack = recipe.getOutputFluid().get();
×
250
                            inserted = blockEntity.getTank().insert(FluidResource.of(fluidStack), fluidStack.getAmount(), tx);
×
251
                            tx.commit();
×
252
                        }
253
                        if (inserted == 0) {
×
254
                            IntegratedDynamics.clog(org.apache.logging.log4j.Level.ERROR, "Encountered an invalid recipe: " + currentRecipe.get().id());
×
255
                        }
256
                    }
257

258
                    blockEntity.setProgress(0);
3✔
259
                } else {
1✔
260
                    blockEntity.setProgress(blockEntity.getProgress() + 1);
6✔
261
                    blockEntity.setChanged();
2✔
262
                }
263
                blockEntity.setFire(0);
3✔
264
            } else {
1✔
265
                if ((blockEntity.getProgress() > 0) || (blockEntity.getFire() > 0)) {
6!
266
                    blockEntity.setProgress(0);
×
267
                    blockEntity.setFire(0);
×
268
                    blockEntity.setChanged();
×
269
                }
270
            }
271
        }
1✔
272
    }
273

274
    public static class TickerClient implements BlockEntityTicker<BlockEntityDryingBasin> {
×
275
        @Override
276
        public void tick(Level level, BlockPos pos, BlockState blockState, BlockEntityDryingBasin blockEntity) {
277
            if(blockEntity.getProgress() > 0 && level.getRandom().nextInt(5) == 0) {
×
278
                if(!blockEntity.getTank().isEmpty()) {
×
279
                    BlockState blockStateFluid = blockEntity.getTank().getFluid().getFluid().getFluidType().getBlockForFluidState(level, pos,
×
280
                            blockEntity.getTank().getFluid().getFluid().defaultFluidState());
×
281
                    if(blockStateFluid != null) {
×
282
                        level.addParticle(new BlockParticleOption(ParticleTypes.FALLING_DUST, blockStateFluid),
×
283
                                pos.getX() + Math.random() * 0.8D + 0.1D, pos.getY() + Math.random() * 0.1D + 0.9D,
×
284
                                pos.getZ() + Math.random() * 0.8D + 0.1D, 0, 0.1D, 0);
×
285
                    }
286
                }
287
                if(!blockEntity.getInventory().getItem(0).isEmpty()) {
×
288
                    ItemStack itemStack = blockEntity.getInventory().getItem(0);
×
289
                    level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, net.minecraft.world.item.ItemStackTemplate.fromNonEmptyStack(itemStack)),
×
290
                            pos.getX() + Math.random() * 0.8D + 0.1D, pos.getY() + Math.random() * 0.1D + 0.9D,
×
291
                            pos.getZ() + Math.random() * 0.8D + 0.1D, 0, 0.1D, 0);
×
292
                }
293
            }
294
        }
×
295
    }
296
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc