From da7fe967c0aeef31662b34ec89e76822beb054af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 19:05:20 +0000 Subject: [PATCH] Keep the initiator of crafting jobs that are distributed A job that is scheduled over multiple crafting interfaces is replaced by clones of itself, but the initiator was only set on the original job after scheduling, and clones did not carry it either. Distributed jobs therefore lost their initiator, so their owner was not shown in the crafting jobs gui. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QNDY2pZtkXqJrhztYCvNbh --- .../api/crafting/CraftingJob.java | 2 + .../core/CraftingHelpers.java | 14 +++-- .../GameTestsCraftingJobFinishedEvent.java | 61 ++++++++++++++++++- .../api/crafting/TestCraftingJob.java | 21 +++++++ 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJob.java b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJob.java index f8dde97f..8145e069 100644 --- a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJob.java +++ b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJob.java @@ -385,6 +385,8 @@ public CraftingJob clone(CraftingHelpers.IIdentifierGenerator identifierGenerato getIngredientsStorage() ); clone.setAmountTotal(getAmountTotal()); + clone.setInitiatorUuid(getInitiatorUuid()); + clone.setNotifyInitiator(isNotifyInitiator()); return clone; } } diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index c5e4456b..14cbabab 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -727,6 +727,12 @@ public static void scheduleCraftingJobs(ICraftingNetwork craftingNetwork, List startedJobs = Lists.newArrayList(); craftingNetwork.getCraftingJobDependencyGraph().importDependencies(craftingJobDependencyGraph); for (CraftingJob craftingJob : craftingJobDependencyGraph.getCraftingJobs()) { + // Set before scheduling, as scheduling may distribute the job over multiple crafting + // interfaces, in which case it is replaced by clones that have to inherit this. + if (initiator != null) { + craftingJob.setInitiatorUuid(initiator.toString()); + craftingJob.setNotifyInitiator(notifyInitiator); + } try { craftingNetwork.scheduleCraftingJob(craftingJob, allowDistribution, storageGetter); } catch (UnavailableCraftingInterfacesException e) { @@ -740,10 +746,6 @@ public static void scheduleCraftingJobs(ICraftingNetwork craftingNetwork, throw new UnavailableCraftingInterfacesException(craftingJobDependencyGraph.getCraftingJobs()); } startedJobs.add(craftingJob); - if (initiator != null) { - craftingJob.setInitiatorUuid(initiator.toString()); - craftingJob.setNotifyInitiator(notifyInitiator); - } } } @@ -763,10 +765,12 @@ public static CraftingJob scheduleCraftingJob(ICraftingNetwork craftingNetwork, CraftingJob craftingJob, boolean allowDistribution, @Nullable UUID initiator) throws UnavailableCraftingInterfacesException { - craftingNetwork.scheduleCraftingJob(craftingJob, allowDistribution, storageGetter); + // Set before scheduling, as scheduling may distribute the job over multiple crafting + // interfaces, in which case it is replaced by clones that have to inherit this. if (initiator != null) { craftingJob.setInitiatorUuid(initiator.toString()); } + craftingNetwork.scheduleCraftingJob(craftingJob, allowDistribution, storageGetter); return craftingJob; } diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsCraftingJobFinishedEvent.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsCraftingJobFinishedEvent.java index 5afede28..ad2e9a7f 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsCraftingJobFinishedEvent.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsCraftingJobFinishedEvent.java @@ -8,6 +8,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.ChestBlockEntity; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.common.NeoForge; @@ -26,6 +27,7 @@ import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import java.util.Iterator; import java.util.List; import java.util.UUID; @@ -120,6 +122,58 @@ public void testNoEventOnCancelledJob(GameTestHelper helper) { .thenSucceed(); } + /** + * A job that is distributed over multiple crafting interfaces stays attributed to its initiator. + */ + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testDistributedJobRetainsInitiator(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + GameTestHelpersIntegratedCrafting.createBasicNetwork(helper, POS, false, + Blocks.CRAFTING_TABLE, Blocks.CRAFTING_TABLE); + ChestBlockEntity chest = helper.getBlockEntity(POS.east()); + chest.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + // Both interfaces know the recipe, so the job gets split over the two of them + for (int i = 0; i < positions.interfaceRecipeAdders().size(); i++) { + positions.interfaceRecipeAdders().get(i).accept(Triple.of(0, RecipeType.CRAFTING, + ResourceLocation.fromNamespaceAndPath("minecraft", "chest"))); + } + + UUID initiator = UUID.randomUUID(); + EventCollector collector = EventCollector.start(initiator); + + helper.startSequence() + .thenIdle(20) + .thenExecute(() -> { + scheduleChestJob(helper, initiator, true, 4); + + List jobs = getCraftingJobs(helper); + helper.assertTrue(jobs.size() > 1, + "Expected the job to be split over both crafting interfaces, but found " + + jobs.size() + " job(s)"); + for (CraftingJob craftingJob : jobs) { + helper.assertTrue(initiator.toString().equals(craftingJob.getInitiatorUuid()), + "A split job lost its initiator"); + helper.assertTrue(craftingJob.isNotifyInitiator(), + "A split job lost its notify flag"); + } + }) + .thenWaitUntil(() -> helper.assertTrue(!collector.getRootJobs().isEmpty(), + "No completion event was emitted for the distributed job")) + .thenExecute(collector::stop) + .thenSucceed(); + } + + private static List getCraftingJobs(GameTestHelper helper) { + List jobs = Lists.newArrayList(); + Iterator it = CraftingHelpers.getCraftingNetworkChecked(getNetwork(helper)) + .getCraftingJobs(IPositionedAddonsNetworkIngredients.WILDCARD_CHANNEL); + while (it.hasNext()) { + jobs.add(it.next()); + } + return jobs; + } + private static void prepareNetwork(GameTestHelper helper) { GameTestHelpersIntegratedCrafting.INetworkPositions positions = GameTestHelpersIntegratedCrafting.createBasicNetwork(helper, POS); @@ -134,12 +188,17 @@ private static void prepareNetwork(GameTestHelper helper) { } private static CraftingJob scheduleChestJob(GameTestHelper helper, UUID initiator, boolean notifyInitiator) { + return scheduleChestJob(helper, initiator, notifyInitiator, 1); + } + + private static CraftingJob scheduleChestJob(GameTestHelper helper, UUID initiator, boolean notifyInitiator, + int amount) { INetwork network = getNetwork(helper); int channel = IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL; try { CraftingJobDependencyGraph dependencyGraph = new CraftingJobDependencyGraph(); CraftingJob craftingJob = CraftingHelpers.calculateCraftingJobs(network, channel, - IngredientComponents.ITEMSTACK, new ItemStack(Items.CHEST), ItemMatch.ITEM, true, + IngredientComponents.ITEMSTACK, new ItemStack(Items.CHEST, amount), ItemMatch.ITEM, true, CraftingHelpers.getGlobalCraftingJobIdentifier(), dependencyGraph, false); CraftingHelpers.scheduleCraftingJobs(CraftingHelpers.getCraftingNetworkChecked(network), CraftingHelpers.getNetworkStorageGetter(network, channel, false), dependencyGraph, true, diff --git a/src/test/java/org/cyclops/integratedcrafting/api/crafting/TestCraftingJob.java b/src/test/java/org/cyclops/integratedcrafting/api/crafting/TestCraftingJob.java index a85eb34b..ba30ec6b 100644 --- a/src/test/java/org/cyclops/integratedcrafting/api/crafting/TestCraftingJob.java +++ b/src/test/java/org/cyclops/integratedcrafting/api/crafting/TestCraftingJob.java @@ -35,4 +35,25 @@ public void testAmountTotalIsUnaffectedByCrafting() { assertThat(job.getAmountTotal(), equalTo(3)); } + @Test + public void testCloneRetainsInitiator() { + // Jobs are cloned when they are distributed over multiple crafting interfaces, + // so the clones have to remain attributable to whoever requested them. + job.setInitiatorUuid("00000000-0000-0000-0000-00000000beef"); + job.setNotifyInitiator(true); + + CraftingJob clone = job.clone(() -> 1); + + assertThat(clone.getInitiatorUuid(), equalTo("00000000-0000-0000-0000-00000000beef")); + assertThat(clone.isNotifyInitiator(), equalTo(true)); + } + + @Test + public void testCloneWithoutInitiator() { + CraftingJob clone = job.clone(() -> 1); + + assertThat(clone.getInitiatorUuid(), equalTo(null)); + assertThat(clone.isNotifyInitiator(), equalTo(false)); + } + }