From d7b755e6bc0bca2de2e029a73f01b94538750ba8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 18:48:02 +0000 Subject: [PATCH] Keep jobs of attuned interfaces in the network when their target updates Targets such as furnaces notify their neighbours every time their contents change. Attuned crafting interfaces unregistered from the crafting network on each of these notifications, and only registered again on their next update. In between, their crafting jobs were missing from the dependency graph, so dependent jobs were reported as missing ingredients. Attuned interfaces now only re-register when their target gained or lost its recipe handler, or exposes other recipes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C5iUzweRYoCkKB5bvJdH8Y --- .../gametest/GameTestsAttunedTarget.java | 90 +++++++++++++++++++ .../PartTypeInterfaceCraftingAttuned.java | 19 +++- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAttunedTarget.java diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAttunedTarget.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAttunedTarget.java new file mode 100644 index 000000000..9074a8dff --- /dev/null +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsAttunedTarget.java @@ -0,0 +1,90 @@ +package org.cyclops.integratedcrafting.gametest; + +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.cyclops.commoncapabilities.IngredientComponents; +import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; +import org.cyclops.integratedcrafting.Reference; +import org.cyclops.integratedcrafting.api.crafting.CraftingJob; +import org.cyclops.integratedcrafting.api.crafting.CraftingJobDependencyGraph; +import org.cyclops.integratedcrafting.api.crafting.CraftingJobStatus; +import org.cyclops.integratedcrafting.api.network.ICraftingNetwork; +import org.cyclops.integratedcrafting.core.CraftingHelpers; +import org.cyclops.integratedcrafting.part.PartTypeInterfaceCraftingAttuned; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; + +import static org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting.createBasicNetwork; + +/** + * Tests for attuned crafting interfaces whose target changes while crafting. + * + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestsAttunedTarget { + + public static final String TEMPLATE_EMPTY = "empty10"; + public static final int TIMEOUT = 1000; + public static final BlockPos POS = BlockPos.ZERO.offset(2, 0, 2); + + /** + * A furnace notifies its neighbours whenever its contents change, + * which must not remove the jobs of its attuned crafting interface from the network. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testAttunedTargetUpdatesKeepJobDependencies(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + createBasicNetwork(helper, POS, true, Blocks.CRAFTING_TABLE, Blocks.FURNACE); + ChestBlockEntity chest = helper.getBlockEntity(POS.east()); + chest.setItem(0, new ItemStack(Items.COBBLESTONE, 1)); + + PartTypeInterfaceCraftingAttuned.State interfaceCraftingTable = positions.interfaceStates().get(0); + PartTypeInterfaceCraftingAttuned.State interfaceFurnace = positions.interfaceStates().get(1); + CraftingJob[] jobs = new CraftingJob[2]; + + helper.runAfterDelay(5, () -> { + INetwork network = getNetwork(positions); + jobs[0] = CraftingHelpers.calculateAndScheduleCraftingJob(network, IPositionedAddonsNetwork.DEFAULT_CHANNEL, + IngredientComponents.ITEMSTACK, new ItemStack(Items.STONE_BUTTON), ItemMatch.ITEM, true, false, + CraftingHelpers.getGlobalCraftingJobIdentifier(), null); + helper.assertTrue(jobs[0] != null, "Crafting job could not be scheduled"); + CraftingJobDependencyGraph graph = CraftingHelpers.getCraftingNetworkChecked(network).getCraftingJobDependencyGraph(); + helper.assertValueEqual(graph.getDependencies(jobs[0]).size(), 1, "Dependencies of the button job"); + jobs[1] = graph.getDependencies(jobs[0]).iterator().next(); + }); + + helper.onEachTick(() -> { + if (jobs[1] == null) { + return; + } + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(getNetwork(positions)); + + // As long as the furnace is smelting, the button job must keep waiting for it + if (interfaceFurnace.getCraftingJobHandler().getAllCraftingJobs().containsKey(jobs[1].getId())) { + helper.assertTrue(craftingNetwork.getCraftingJobDependencyGraph().getCraftingJob(jobs[1].getId()) != null, + "The smelting job left the dependency graph"); + helper.assertValueEqual(interfaceCraftingTable.getCraftingJobStatus(craftingNetwork, jobs[0].getChannel(), jobs[0].getId()), + CraftingJobStatus.PENDING_DEPENDENCIES, "Status of the button job"); + } + + if (chest.countItem(Items.STONE_BUTTON) == 1) { + helper.succeed(); + } + }); + } + + protected static INetwork getNetwork(GameTestHelpersIntegratedCrafting.INetworkPositions positions) { + return NetworkHelpers.getNetworkChecked(positions.writer().getPos().getLevel(true), + positions.writer().getPos().getBlockPos(), positions.writer().getSide()); + } +} diff --git a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingAttuned.java b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingAttuned.java index dff2afca9..b4e9fca04 100644 --- a/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingAttuned.java +++ b/src/main/java/org/cyclops/integratedcrafting/part/PartTypeInterfaceCraftingAttuned.java @@ -178,7 +178,10 @@ public boolean shouldTriggerBlockRenderUpdate(@Nullable State oldPartState, @Nul @Override public void onBlockNeighborChange(INetwork network, IPartNetwork partNetwork, PartTarget target, State state, BlockGetter world, Block neighbourBlock, BlockPos neighbourBlockPos) { - boolean isNeighbourTarget = neighbourBlockPos.equals(target.getTarget().getPos().getBlockPos()); + // Targets such as furnaces notify their neighbours on every operation, while their recipes remain the same. + // Only re-register if the target really changed, as this temporarily removes our crafting jobs from the network. + boolean isNeighbourTarget = neighbourBlockPos.equals(target.getTarget().getPos().getBlockPos()) + && state.hasTargetChanged(); boolean hadValidTarget = false; if (isNeighbourTarget) { hadValidTarget = state.hasValidTarget(); @@ -282,6 +285,20 @@ public boolean hasValidTarget() { return this.hasValidTarget; } + /** + * @return If the target gained or lost its recipe handler, or exposes other recipes than the ones that were last read. + */ + public boolean hasTargetChanged() { + if (getTarget() == null) { + return true; + } + Optional recipeHandler = getTargetRecipeHandler(); + if (recipeHandler.isPresent() != this.hasValidTarget) { + return true; + } + return recipeHandler.isPresent() && !Lists.newArrayList(recipeHandler.get().getRecipes()).equals(this.recipes); + } + @Override public void writeToNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { super.writeToNBT(valueDeseralizationContext, tag);