From a3d731744c6d62f5226c99bcaccd0212bc03a627 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sat, 1 Apr 2023 12:10:38 +0200 Subject: [PATCH] Fix #6892: Send processing pattern inputs in the encoded sparse order --- .../appeng/api/crafting/IPatternDetails.java | 22 ++++++ .../crafting/pattern/AEProcessingPattern.java | 34 ++++++++++ .../patternprovider/PatternProviderLogic.java | 14 ++-- .../testplots/AutoCraftingTestPlots.java | 68 +++++++++++++++++++ 4 files changed, 129 insertions(+), 9 deletions(-) diff --git a/src/main/java/appeng/api/crafting/IPatternDetails.java b/src/main/java/appeng/api/crafting/IPatternDetails.java index 781639f67cf..b554d0f6500 100644 --- a/src/main/java/appeng/api/crafting/IPatternDetails.java +++ b/src/main/java/appeng/api/crafting/IPatternDetails.java @@ -28,9 +28,11 @@ import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.level.Level; +import appeng.api.implementations.blockentities.ICraftingMachine; import appeng.api.stacks.AEItemKey; import appeng.api.stacks.AEKey; import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; /** * Information about a pattern for use by the autocrafting system. @@ -63,6 +65,22 @@ default GenericStack getPrimaryOutput() { */ GenericStack[] getOutputs(); + /** + * Gives the pattern a chance to reorder its inputs for pushing to external inventories (i.e. NOT to + * {@link ICraftingMachine}s). + * + * @param inputHolder For each {@link IInput}, the relevant items. The ownership is given to the pattern, do + * whatever with the key counters as long as all of their contents end up in the input sink. + * @param inputSink Where to push the inputs to. + */ + default void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) { + for (var inputList : inputHolder) { + for (var input : inputList) { + inputSink.pushInput(input.getKey(), input.getLongValue()); + } + } + } + interface IInput { /** * A list of possible inputs for this pattern: the first input is the primary input, others are just substitutes @@ -90,4 +108,8 @@ interface IInput { @Nullable AEKey getRemainingKey(AEKey template); } + + interface PatternInputSink { + void pushInput(AEKey key, long amount); + } } diff --git a/src/main/java/appeng/crafting/pattern/AEProcessingPattern.java b/src/main/java/appeng/crafting/pattern/AEProcessingPattern.java index 522907d045b..e9187b2f3f7 100644 --- a/src/main/java/appeng/crafting/pattern/AEProcessingPattern.java +++ b/src/main/java/appeng/crafting/pattern/AEProcessingPattern.java @@ -28,6 +28,7 @@ import appeng.api.stacks.AEItemKey; import appeng.api.stacks.AEKey; import appeng.api.stacks.GenericStack; +import appeng.api.stacks.KeyCounter; public class AEProcessingPattern implements IPatternDetails { public static final int MAX_INPUT_SLOTS = 9 * 9; @@ -87,6 +88,39 @@ public GenericStack[] getSparseOutputs() { return sparseOutputs; } + @Override + public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) { + if (sparseInputs.length == inputs.length) { + // No compression -> no need to reorder + IPatternDetails.super.pushInputsToExternalInventory(inputHolder, inputSink); + return; + } + + var allInputs = new KeyCounter(); + for (var counter : inputHolder) { + allInputs.addAll(counter); + } + + // Push according to sparse input order + for (var sparseInput : sparseInputs) { + if (sparseInput == null) { + continue; + } + + var key = sparseInput.what(); + var amount = sparseInput.amount(); + long available = allInputs.get(key); + + if (available < amount) { + throw new RuntimeException("Expected at least %d of %s when pushing pattern, but only %d available" + .formatted(amount, key, available)); + } + + inputSink.pushInput(key, amount); + allInputs.remove(key, amount); + } + } + private static class Input implements IInput { private final GenericStack[] template; private final long multiplier; diff --git a/src/main/java/appeng/helpers/patternprovider/PatternProviderLogic.java b/src/main/java/appeng/helpers/patternprovider/PatternProviderLogic.java index fe2187c0dcd..c4ffd48eee1 100644 --- a/src/main/java/appeng/helpers/patternprovider/PatternProviderLogic.java +++ b/src/main/java/appeng/helpers/patternprovider/PatternProviderLogic.java @@ -339,16 +339,12 @@ record PushTarget(Direction direction, PatternProviderTarget target) { } if (this.adapterAcceptsAll(adapter, inputHolder)) { - for (var inputList : inputHolder) { - for (var input : inputList) { - var what = input.getKey(); - long amount = input.getLongValue(); - var inserted = adapter.insert(what, amount, Actionable.MODULATE); - if (inserted < amount) { - this.addToSendList(what, amount - inserted); - } + patternDetails.pushInputsToExternalInventory(inputHolder, (what, amount) -> { + var inserted = adapter.insert(what, amount, Actionable.MODULATE); + if (inserted < amount) { + this.addToSendList(what, amount - inserted); } - } + }); onPushPatternSuccess(patternDetails); this.sendDirection = direction; this.sendStacksOut(); diff --git a/src/main/java/appeng/server/testplots/AutoCraftingTestPlots.java b/src/main/java/appeng/server/testplots/AutoCraftingTestPlots.java index f4dac053303..b2db3bde7ba 100644 --- a/src/main/java/appeng/server/testplots/AutoCraftingTestPlots.java +++ b/src/main/java/appeng/server/testplots/AutoCraftingTestPlots.java @@ -23,6 +23,7 @@ import appeng.api.stacks.GenericStack; import appeng.blockentity.crafting.PatternProviderBlockEntity; import appeng.blockentity.misc.InscriberBlockEntity; +import appeng.blockentity.storage.SkyChestBlockEntity; import appeng.core.definitions.AEBlocks; import appeng.core.definitions.AEItems; import appeng.core.definitions.AEParts; @@ -31,6 +32,7 @@ import appeng.menu.AutoCraftingMenu; import appeng.server.testworld.PlotBuilder; import appeng.server.testworld.TestCraftingJob; +import appeng.util.inv.AppEngInternalInventory; public final class AutoCraftingTestPlots { private AutoCraftingTestPlots() { @@ -316,4 +318,70 @@ public static void patternProviderFacesRoundRobin(PlotBuilder plot) { .thenSucceed(); }); } + + /** + * Tests that identical processing pattern inputs get "unstacked" when they are pushed. This is validated using a + * sky stone chest with a changed slot limit of 1. + */ + @TestPlot("processing_pattern_inputs_unstacking") + public static void processingPatternInputsUnstacking(PlotBuilder plot) { + var chestPos = new BlockPos(1, 0, -3); + + craftingCube(plot); + + plot.cable("0 0 -2"); + plot.blockEntity("0 0 -3", AEBlocks.PATTERN_PROVIDER, provider -> { + var pattern = PatternDetailsHelper.encodeProcessingPattern( + new GenericStack[] { + GenericStack.fromItemStack(new ItemStack(Items.STONE)), + GenericStack.fromItemStack(new ItemStack(Items.STONE)), + GenericStack.fromItemStack(new ItemStack(Items.DIAMOND)), + GenericStack.fromItemStack(new ItemStack(Items.STONE)), + }, + new GenericStack[] { + GenericStack.fromItemStack(AEItems.CERTUS_QUARTZ_DUST.stack()) + }); + provider.getLogic().getPatternInv().addItems(pattern); + }); + plot.blockEntity(chestPos, AEBlocks.SKY_STONE_CHEST, skyChest -> { + var inv = (AppEngInternalInventory) skyChest.getInternalInventory(); + for (int i = 0; i < inv.size(); i++) { + inv.setMaxStackSize(i, 1); + } + }); + plot.cable("0 0 -4"); + { + var db = plot.drive(new BlockPos(0, 0, -5)); + db.addCreativeCell().add(Items.STONE).add(Items.DIAMOND); + db.addItemCell64k(); + } + plot.cable("0 1 -5").part(Direction.NORTH, AEParts.CRAFTING_TERMINAL); + plot.creativeEnergyCell("0 -1 -5"); + + // Check item distribution in chest + plot.test(helper -> { + var craftingJob = new TestCraftingJob(helper, BlockPos.ZERO, AEItemKey.of(AEItems.CERTUS_QUARTZ_DUST), 1); + helper.startSequence() + .thenWaitUntil(craftingJob::tickUntilStarted) + .thenIdle(1) // give time to push out job + .thenExecute(() -> { + var chest = (SkyChestBlockEntity) helper.getBlockEntity(chestPos); + var inv = chest.getInternalInventory(); + for (int i = 0; i < 4; ++i) { + helper.check(inv.getStackInSlot(i).getCount() == 1, + "Chest should have 1 item in slot " + i, chestPos); + } + + helper.check(inv.getStackInSlot(0).is(Items.STONE), "Chest should have stone in slot 0", + chestPos); + helper.check(inv.getStackInSlot(1).is(Items.STONE), "Chest should have stone in slot 1", + chestPos); + helper.check(inv.getStackInSlot(2).is(Items.DIAMOND), "Chest should have diamond in slot 2", + chestPos); + helper.check(inv.getStackInSlot(3).is(Items.STONE), "Chest should have stone in slot 3", + chestPos); + }) + .thenSucceed(); + }); + } }