From 9a819eb8d6cedcda7e1d42cfa27654a38be880de Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Wed, 8 Oct 2025 01:02:33 +0300 Subject: [PATCH 01/20] make placing recipe from recipe book working with custom ingredients --- .../item/crafting/PlacementInfoBridge.java | 35 ++++ .../world/item/crafting/RecipeBridge.java | 32 ++++ .../item/crafting/ShapedRecipeBridge.java | 2 +- .../item/recipe/book/RecipeBookUtil.java | 88 +++++++++ .../SpongeStackedContentsOutputWrapper.java | 60 +++++++ .../book/SpongeStackedItemContents.java | 123 +++++++++++++ .../shapeless/SpongeShapelessRecipe.java | 7 +- .../recipe/ingredient/IngredientUtil.java | 5 +- .../recipebook/ServerPlaceRecipeMixin.java | 167 ++++++++++++++++++ .../item/crafting/PlacementInfoMixin.java | 58 ++++++ .../core/world/item/crafting/RecipeMixin.java | 33 ++++ .../item/crafting/ShapedRecipeMixin.java | 17 +- .../item/crafting/ShapelessRecipeMixin.java | 23 ++- .../item/crafting/SingleItemRecipeMixin.java | 17 +- src/mixins/resources/mixins.sponge.core.json | 3 + 15 files changed, 658 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java create mode 100644 src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java create mode 100644 src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java create mode 100644 src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java create mode 100644 src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java create mode 100644 src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java create mode 100644 src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java create mode 100644 src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java new file mode 100644 index 00000000000..086b4139a7b --- /dev/null +++ b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java @@ -0,0 +1,35 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.bridge.world.item.crafting; + +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +public interface PlacementInfoBridge { + + List> bridge$stackIngredientInfos(); +} diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java new file mode 100644 index 00000000000..e856f7989df --- /dev/null +++ b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java @@ -0,0 +1,32 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.bridge.world.item.crafting; + +public interface RecipeBridge { + + default boolean bridge$hasCustomIngredients() { + return false; + } +} diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java index 8ea8346f8ae..7efbae8803b 100644 --- a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java +++ b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java @@ -26,6 +26,6 @@ import net.minecraft.world.item.crafting.ShapedRecipePattern; -public interface ShapedRecipeBridge extends RecipeResultBridge { +public interface ShapedRecipeBridge extends RecipeBridge, RecipeResultBridge { ShapedRecipePattern bridge$pattern(); } diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java b/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java new file mode 100644 index 00000000000..b22aaa6e904 --- /dev/null +++ b/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java @@ -0,0 +1,88 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.item.recipe.book; + +import net.minecraft.core.Holder; +import net.minecraft.recipebook.ServerPlaceRecipe; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public final class RecipeBookUtil { + + /** + * Copied from {@link ServerPlaceRecipe#moveItemToGrid(Slot, Holder, int)} + * and adjusted to use exemplary {@link ItemStack} instead of just item type. + */ + public static int moveItemToGrid( + final Inventory inventory, final Slot craftInputSlot, final ItemStack exemplaryStackToMove, final int amount + ) { + final ItemStack craftInputStack = craftInputSlot.getItem(); + final int itemToMoveIndex = RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, exemplaryStackToMove, craftInputStack); + if (itemToMoveIndex == -1) { + return -1; + } else { + final ItemStack inventoryStack = inventory.getItem(itemToMoveIndex); + final ItemStack movedStack; + if (amount < inventoryStack.getCount()) { + movedStack = inventory.removeItem(itemToMoveIndex, amount); + } else { + movedStack = inventory.removeItemNoUpdate(itemToMoveIndex); + } + + int movedAmount = movedStack.getCount(); + if (craftInputStack.isEmpty()) { + craftInputSlot.set(movedStack); + } else { + craftInputStack.grow(movedAmount); + } + + return amount - movedAmount; + } + } + + /** + * Copied from {@link Inventory#findSlotMatchingCraftingIngredient(Holder, ItemStack)} + * and adjusted to use exemplary {@link ItemStack} instead of just item type. + */ + public static int findSlotMatchingCraftingIngredient( + final Inventory inventory, final ItemStack exemplaryStackToMove, final ItemStack craftInputStack + ) { + for (int i = 0; i < inventory.items.size(); i++) { + final ItemStack stack = inventory.items.get(i); + if (!stack.isEmpty() + && Inventory.isUsableForCrafting(stack) + && ItemStack.isSameItemSameComponents(exemplaryStackToMove, stack) + && (craftInputStack.isEmpty() || ItemStack.isSameItemSameComponents(craftInputStack, stack))) { + return i; + } + } + + return -1; + } + + private RecipeBookUtil() { + } +} diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java new file mode 100644 index 00000000000..d34ceee8772 --- /dev/null +++ b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java @@ -0,0 +1,60 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.item.recipe.book; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +public class SpongeStackedContentsOutputWrapper implements StackedContents.Output> { + + private final StackedContents.Output> originalOutput; + private final StackedContents.Output wrappedStackOutput; + + public SpongeStackedContentsOutputWrapper( + final StackedContents.Output> original, + final StackedContents.Output toWrap + ) { + this.originalOutput = original; + this.wrappedStackOutput = stack -> { + originalOutput.accept(stack.getItemHolder()); + toWrap.accept(stack); + }; + } + + /** + * @deprecated Should not be used directly. + */ + @Deprecated + @Override + public void accept(final Holder holder) { + this.originalOutput.accept(holder); + } + + public StackedContents.Output stackOutput() { + return this.wrappedStackOutput; + } +} diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java new file mode 100644 index 00000000000..5f91918b96a --- /dev/null +++ b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java @@ -0,0 +1,123 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.item.recipe.book; + +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenCustomHashMap; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.PlacementInfo; +import net.minecraft.world.item.crafting.Recipe; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; + +import java.util.List; +import java.util.function.Function; + +public class SpongeStackedItemContents extends StackedItemContents { + + private static final Hash.Strategy STACK_HASH_STRATEGY = new Hash.Strategy<>() { + @Override + public int hashCode(final ItemStack o) { + return ItemStack.hashItemAndComponents(o); + } + + @Override + public boolean equals(final @Nullable ItemStack a, final @Nullable ItemStack b) { + return (a == b) || (a != null && b != null && ItemStack.isSameItemSameComponents(a, b)); + } + }; + + private final Object2ReferenceMap stackInterner = new Object2ReferenceOpenCustomHashMap<>(STACK_HASH_STRATEGY); + private final StackedContents stackedContents = new StackedContents<>(); + + @Override + public void accountStack(final ItemStack stack, final int maxStackSize) { + if (!stack.isEmpty()) { + // StackedContents works on Reference2IntMap, so if we meet stack which "same" copy + // has already been accounted we would need to provide the stack that was met first. + final ItemStack stackToAccount = this.stackInterner.computeIfAbsent(stack, Function.identity()); + this.stackedContents.account(stackToAccount, Math.min(stack.getCount(), maxStackSize)); + } + } + + @Override + public boolean canCraft( + final Recipe recipe, final int amount, + final StackedContents.@Nullable Output> output + ) { + final PlacementInfo placement = recipe.placementInfo(); + return !placement.isImpossibleToPlace() + && this.stackedContents.tryPick( + ((PlacementInfoBridge) placement).bridge$stackIngredientInfos(), + amount, this.unwrapStackOutput(output)); + } + + @Override + public boolean canCraft( + final List>> ingredients, + final StackedContents.@Nullable Output> output + ) { + // By default, this method is not called in the context the instance of this class is created. + // If this happens, it's either error in Sponge impl or + // mixin from some mod (which should be inspected instead of silently doing something that impl does not expect). + throw new UnsupportedOperationException("This method should not have been called, please report about it"); + } + + @Override + public int getBiggestCraftableStack( + final Recipe recipe, final int maxCount, + final StackedContents.@Nullable Output> output + ) { + return this.stackedContents.tryPickAll( + ((PlacementInfoBridge) recipe.placementInfo()).bridge$stackIngredientInfos(), + maxCount, this.unwrapStackOutput(output)); + } + + @Override + public void clear() { + this.stackInterner.clear(); + this.stackedContents.clear(); + } + + private StackedContents.@Nullable Output unwrapStackOutput( + final StackedContents.@Nullable Output> output + ) { + if (output == null) { + return null; + } else if (output instanceof final SpongeStackedContentsOutputWrapper spongeOutput) { + return spongeOutput.stackOutput(); + } else { + // By default, this method is only called with wrapped outputs. + // If this happens, there is either error in Sponge impl or + // mixin from some mod and Sponge mixin is not applied last. + throw new UnsupportedOperationException("This should not have happened, please report about it"); + } + } +} diff --git a/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java index 7981d28781e..259ed35782c 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java @@ -33,7 +33,7 @@ import net.minecraft.world.item.crafting.ShapelessRecipe; import net.minecraft.world.level.Level; import org.spongepowered.common.accessor.world.item.crafting.ShapelessRecipeAccessor; -import org.spongepowered.common.item.recipe.ingredient.SpongeIngredient; +import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; import java.util.ArrayList; import java.util.Collection; @@ -50,8 +50,6 @@ */ public class SpongeShapelessRecipe extends ShapelessRecipe { - private final boolean onlyVanillaIngredients; - private final Function resultFunction; private final Function> remainingItemsFunction; @@ -62,14 +60,13 @@ public SpongeShapelessRecipe(final String groupIn, final Function resultFunction, final Function> remainingItemsFunction) { super(groupIn, category, spongeResultStack, recipeItemsIn); - this.onlyVanillaIngredients = recipeItemsIn.stream().noneMatch(i -> i instanceof SpongeIngredient); this.resultFunction = resultFunction; this.remainingItemsFunction = remainingItemsFunction; } @Override public boolean matches(final CraftingInput $$0, final Level $$1) { - if (this.onlyVanillaIngredients) { + if (!((RecipeBridge) this).bridge$hasCustomIngredients()) { return super.matches($$0, $$1); } return SpongeShapelessRecipe.matches($$0.items(), ((ShapelessRecipeAccessor) this).accessor$ingredients()); diff --git a/src/main/java/org/spongepowered/common/item/recipe/ingredient/IngredientUtil.java b/src/main/java/org/spongepowered/common/item/recipe/ingredient/IngredientUtil.java index 228ba81923a..15507ac6cb5 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/ingredient/IngredientUtil.java +++ b/src/main/java/org/spongepowered/common/item/recipe/ingredient/IngredientUtil.java @@ -79,6 +79,7 @@ public static org.spongepowered.api.item.recipe.crafting.Ingredient of(ResourceK return IngredientUtil.fromNative(ingredient); } - - + public static boolean isCustom(final @Nullable Ingredient ingredient) { + return ingredient instanceof SpongeIngredient; + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java new file mode 100644 index 00000000000..589f7f84890 --- /dev/null +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -0,0 +1,167 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.mixin.core.recipebook; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import net.minecraft.core.Holder; +import net.minecraft.recipebook.ServerPlaceRecipe; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeHolder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; +import org.spongepowered.common.item.recipe.book.RecipeBookUtil; +import org.spongepowered.common.item.recipe.book.SpongeStackedContentsOutputWrapper; +import org.spongepowered.common.item.recipe.book.SpongeStackedItemContents; + +import java.util.ArrayList; +import java.util.List; + +/** + * Makes recipe placing to work with sponge custom ingredients. + * Fallbacks to the original logic if recipe does not have any custom ingredient. + */ +@Mixin(ServerPlaceRecipe.class) +public abstract class ServerPlaceRecipeMixin { + + @Shadow @Final private Inventory inventory; + + /** + * {@link Share} is not applicable here because there is + * lambda mixin that does not get shared value passed into it. + * The method this field is used for modifies + * inventory so it should never be called async. + */ + private @Nullable List impl$stackList; + + @WrapOperation( + method = "placeRecipe(Lnet/minecraft/recipebook/ServerPlaceRecipe$CraftingMenuAccess;IILjava/util/List;Ljava/util/List;Lnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/item/crafting/RecipeHolder;ZZ)Lnet/minecraft/world/inventory/RecipeBookMenu$PostPlaceAction;", + at = @At( + value = "NEW", + target = "()Lnet/minecraft/world/entity/player/StackedItemContents;" + ) + ) + private static StackedItemContents impl$useCustomStackedItemContents( + final Operation original, + final ServerPlaceRecipe.CraftingMenuAccess menu, + final int gridWidth, + final int gridHeight, + final List inputGridSlots, + final List slotsToClear, + final Inventory inventory, + final RecipeHolder recipe, + final boolean useMaxItems, + final boolean isCreative + ) { + if (((RecipeBridge) recipe.value()).bridge$hasCustomIngredients()) { + return new SpongeStackedItemContents(); + } else { + return original.call(); + } + } + + @Inject( + method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", + at = @At( + value = "NEW", + target = "()Ljava/util/ArrayList;" + ) + ) + private void impl$setStackList( + final RecipeHolder recipe, final StackedItemContents stackedContents, final CallbackInfo ci + ) { + if (stackedContents instanceof SpongeStackedItemContents) { + this.impl$stackList = new ArrayList<>(); + } + } + + @Inject( + method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", + at = @At("RETURN") + ) + private void impl$unsetStackList(final CallbackInfo ci) { + this.impl$stackList = null; + } + + @Inject( + method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", + at = @At( + value = "INVOKE", + target = "Ljava/util/List;clear()V" + ) + ) + private void impl$clearStackList(final CallbackInfo ci) { + if (this.impl$stackList != null) { + this.impl$stackList.clear(); + } + } + + @ModifyArg( + method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/StackedItemContents;canCraft(Lnet/minecraft/world/item/crafting/Recipe;ILnet/minecraft/world/entity/player/StackedContents$Output;)Z" + ) + ) + private StackedContents.Output> impl$wrapContentsOutput( + final StackedContents.Output> originalOutput + ) { + return this.impl$stackList == null + ? originalOutput + : new SpongeStackedContentsOutputWrapper(originalOutput, this.impl$stackList::add); + } + + @WrapOperation( + method = "lambda$placeRecipe$0", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/recipebook/ServerPlaceRecipe;moveItemToGrid(Lnet/minecraft/world/inventory/Slot;Lnet/minecraft/core/Holder;I)I" + ) + ) + private int impl$useAdjustedItemMoveLogic( + final ServerPlaceRecipe instance, final Slot craftInputSlot, + final Holder exemplaryItem, final int amountToMove, + final Operation original, + final List> exemplaryItems, final int totalAmountToCraft, + final Integer exemplaryItemIndex, final int slotIndex, final int x, final int y + ) { + return this.impl$stackList == null + ? original.call(instance, craftInputSlot, exemplaryItem, amountToMove) + : RecipeBookUtil.moveItemToGrid(this.inventory, craftInputSlot, this.impl$stackList.get(exemplaryItemIndex), amountToMove); + } +} diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java new file mode 100644 index 00000000000..0855a007575 --- /dev/null +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java @@ -0,0 +1,58 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.mixin.core.world.item.crafting; + +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.PlacementInfo; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; + +import java.util.List; + +@Mixin(PlacementInfo.class) +public abstract class PlacementInfoMixin implements PlacementInfoBridge { + + @Shadow @Final private List ingredients; + + private List> impl$stackIngredientInfos; + + @Inject(method = "", at = @At("RETURN")) + private void impl$setIngredientInfos(final CallbackInfo ci) { + this.impl$stackIngredientInfos = this.ingredients.stream() + .>map(ingredient -> ingredient::test) + .toList(); + } + + public List> bridge$stackIngredientInfos() { + return this.impl$stackIngredientInfos; + } +} diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java new file mode 100644 index 00000000000..c3db55d6ada --- /dev/null +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java @@ -0,0 +1,33 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.mixin.core.world.item.crafting; + +import net.minecraft.world.item.crafting.Recipe; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; + +@Mixin(Recipe.class) +public interface RecipeMixin extends RecipeBridge { +} diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java index dc3d3956890..eaa5eb3986e 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java @@ -30,7 +30,11 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.ShapedRecipeBridge; +import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; @Mixin(ShapedRecipe.class) public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { @@ -41,6 +45,14 @@ public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { // @formatter=on + private boolean impl$hasCustomIngredients; + + @Inject(method = "(Ljava/lang/String;Lnet/minecraft/world/item/crafting/CraftingBookCategory;Lnet/minecraft/world/item/crafting/ShapedRecipePattern;Lnet/minecraft/world/item/ItemStack;Z)V", at = @At("RETURN")) + private void impl$checkCustomIngredients(final CallbackInfo ci) { + this.impl$hasCustomIngredients = this.pattern.ingredients().stream() + .map(i -> i.orElse(null)) + .anyMatch(IngredientUtil::isCustom); + } @Override public ShapedRecipePattern bridge$pattern() { @@ -52,5 +64,8 @@ public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { return this.result; } - + @Override + public boolean bridge$hasCustomIngredients() { + return this.impl$hasCustomIngredients; + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java index 25d9dd221c1..73320c19706 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java @@ -25,23 +25,42 @@ package org.spongepowered.common.mixin.core.world.item.crafting; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.ShapelessRecipe; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; import org.spongepowered.common.bridge.world.item.crafting.RecipeResultBridge; +import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; + +import java.util.List; @Mixin(ShapelessRecipe.class) -public abstract class ShapelessRecipeMixin implements RecipeResultBridge { +public abstract class ShapelessRecipeMixin implements RecipeBridge, RecipeResultBridge { // @formatter=off @Shadow @Final ItemStack result; - + @Shadow @Final private List ingredients; // @formatter=on + private boolean impl$hasCustomIngredients; + + @Inject(method = "", at = @At("RETURN")) + private void impl$checkCustomIngredients(final CallbackInfo ci) { + this.impl$hasCustomIngredients = this.ingredients.stream().anyMatch(IngredientUtil::isCustom); + } @Override public ItemStack bridge$result() { return this.result; } + + @Override + public boolean bridge$hasCustomIngredients() { + return this.impl$hasCustomIngredients; + } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java index b0925a42a67..becdb442397 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java @@ -30,22 +30,37 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.RecipeResultBridge; +import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; /** * {@link net.minecraft.world.item.crafting.StonecutterRecipe} */ @Mixin(SingleItemRecipe.class) -public abstract class SingleItemRecipeMixin implements RecipeResultBridge { +public abstract class SingleItemRecipeMixin implements RecipeMixin, RecipeResultBridge { // @formatter=off @Shadow @Final private ItemStack result; @Shadow public abstract Ingredient shadow$input(); // @formatter=on + private boolean impl$hasCustomIngredients; + + @Inject(method = "", at = @At("RETURN")) + private void impl$checkCustomIngredient(final CallbackInfo ci) { + this.impl$hasCustomIngredients = IngredientUtil.isCustom(this.shadow$input()); + } @Override public ItemStack bridge$result() { return this.result; } + + @Override + public boolean bridge$hasCustomIngredients() { + return this.impl$hasCustomIngredients; + } } diff --git a/src/mixins/resources/mixins.sponge.core.json b/src/mixins/resources/mixins.sponge.core.json index e850c287944..ed03340d6a9 100644 --- a/src/mixins/resources/mixins.sponge.core.json +++ b/src/mixins/resources/mixins.sponge.core.json @@ -52,6 +52,7 @@ "network.syncher.EntityDataAccessorMixin", "network.syncher.SynchedEntityData_BuilderMixin", "network.syncher.SynchedEntityDataMixin", + "recipebook.ServerPlaceRecipeMixin", "resources.RegistryDataLoader_LoaderMixin", "resources.RegistryDataLoaderMixin", "server.MinecraftServerMixin", @@ -211,7 +212,9 @@ "world.item.TeleportRandomlyConsumeEffectMixin", "world.item.crafting.AbstractCookingRecipeMixin", "world.item.crafting.IngredientMixin", + "world.item.crafting.PlacementInfoMixin", "world.item.crafting.RecipeManagerMixin", + "world.item.crafting.RecipeMixin", "world.item.crafting.ShapedRecipeMixin", "world.item.crafting.ShapelessRecipeMixin", "world.item.crafting.SingleItemRecipeMixin", From 0f14cb9ca099b43937edc024b9f6d9505910f7b3 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 00:27:24 +0300 Subject: [PATCH 02/20] remove adjusted ServerPlaceRecipe#moveItemToGrid copy --- .../item/recipe/book/RecipeBookUtil.java | 33 ------------------- .../recipebook/ServerPlaceRecipeMixin.java | 27 ++++++++++++--- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java b/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java index b22aaa6e904..9c829f1243a 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java +++ b/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java @@ -25,44 +25,11 @@ package org.spongepowered.common.item.recipe.book; import net.minecraft.core.Holder; -import net.minecraft.recipebook.ServerPlaceRecipe; import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; public final class RecipeBookUtil { - /** - * Copied from {@link ServerPlaceRecipe#moveItemToGrid(Slot, Holder, int)} - * and adjusted to use exemplary {@link ItemStack} instead of just item type. - */ - public static int moveItemToGrid( - final Inventory inventory, final Slot craftInputSlot, final ItemStack exemplaryStackToMove, final int amount - ) { - final ItemStack craftInputStack = craftInputSlot.getItem(); - final int itemToMoveIndex = RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, exemplaryStackToMove, craftInputStack); - if (itemToMoveIndex == -1) { - return -1; - } else { - final ItemStack inventoryStack = inventory.getItem(itemToMoveIndex); - final ItemStack movedStack; - if (amount < inventoryStack.getCount()) { - movedStack = inventory.removeItem(itemToMoveIndex, amount); - } else { - movedStack = inventory.removeItemNoUpdate(itemToMoveIndex); - } - - int movedAmount = movedStack.getCount(); - if (craftInputStack.isEmpty()) { - craftInputSlot.set(movedStack); - } else { - craftInputStack.grow(movedAmount); - } - - return amount - movedAmount; - } - } - /** * Copied from {@link Inventory#findSlotMatchingCraftingIngredient(Holder, ItemStack)} * and adjusted to use exemplary {@link ItemStack} instead of just item type. diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index 589f7f84890..35b815ed962 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -68,6 +68,7 @@ public abstract class ServerPlaceRecipeMixin { * inventory so it should never be called async. */ private @Nullable List impl$stackList; + private @Nullable ItemStack impl$currentExemplaryStackToMove; @WrapOperation( method = "placeRecipe(Lnet/minecraft/recipebook/ServerPlaceRecipe$CraftingMenuAccess;IILjava/util/List;Ljava/util/List;Lnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/item/crafting/RecipeHolder;ZZ)Lnet/minecraft/world/inventory/RecipeBookMenu$PostPlaceAction;", @@ -153,15 +154,33 @@ public abstract class ServerPlaceRecipeMixin { target = "Lnet/minecraft/recipebook/ServerPlaceRecipe;moveItemToGrid(Lnet/minecraft/world/inventory/Slot;Lnet/minecraft/core/Holder;I)I" ) ) - private int impl$useAdjustedItemMoveLogic( + private int impl$adjustItemMoveLogic( final ServerPlaceRecipe instance, final Slot craftInputSlot, final Holder exemplaryItem, final int amountToMove, final Operation original, final List> exemplaryItems, final int totalAmountToCraft, final Integer exemplaryItemIndex, final int slotIndex, final int x, final int y ) { - return this.impl$stackList == null - ? original.call(instance, craftInputSlot, exemplaryItem, amountToMove) - : RecipeBookUtil.moveItemToGrid(this.inventory, craftInputSlot, this.impl$stackList.get(exemplaryItemIndex), amountToMove); + if (this.impl$stackList != null) { + this.impl$currentExemplaryStackToMove = this.impl$stackList.get(exemplaryItemIndex); + } + final int result = original.call(instance, craftInputSlot, exemplaryItem, amountToMove); + this.impl$currentExemplaryStackToMove = null; + return result; + } + + @WrapOperation( + method = "moveItemToGrid", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Inventory;findSlotMatchingCraftingIngredient(Lnet/minecraft/core/Holder;Lnet/minecraft/world/item/ItemStack;)I" + ) + ) + private int impl$adjustMatchingSlotFinder( + final Inventory instance, final Holder exemplaryItem, final ItemStack craftInputStack, final Operation original + ) { + return this.impl$currentExemplaryStackToMove == null + ? original.call(instance, exemplaryItem, craftInputStack) + : RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$currentExemplaryStackToMove, craftInputStack); } } From 25562d65ecfeddebde85158384d9b66e1426922f Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 01:29:16 +0300 Subject: [PATCH 03/20] move RecipeBridge logic to PlacementInfoBridge --- .../item/crafting/PlacementInfoBridge.java | 4 ++- .../world/item/crafting/RecipeBridge.java | 32 ------------------ .../item/crafting/ShapedRecipeBridge.java | 2 +- .../book/SpongeStackedItemContents.java | 4 +-- .../shapeless/SpongeShapelessRecipe.java | 4 +-- .../recipebook/ServerPlaceRecipeMixin.java | 4 +-- .../item/crafting/PlacementInfoMixin.java | 14 ++++++-- .../core/world/item/crafting/RecipeMixin.java | 33 ------------------- .../item/crafting/ShapedRecipeMixin.java | 17 +--------- .../item/crafting/ShapelessRecipeMixin.java | 23 ++----------- .../item/crafting/SingleItemRecipeMixin.java | 17 +--------- src/mixins/resources/mixins.sponge.core.json | 1 - 12 files changed, 26 insertions(+), 129 deletions(-) delete mode 100644 src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java delete mode 100644 src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java index 086b4139a7b..39de20616ee 100644 --- a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java +++ b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/PlacementInfoBridge.java @@ -31,5 +31,7 @@ public interface PlacementInfoBridge { - List> bridge$stackIngredientInfos(); + boolean bridge$hasCustomIngredients(); + + List> bridge$getStackIngredientInfos(); } diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java deleted file mode 100644 index e856f7989df..00000000000 --- a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/RecipeBridge.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.common.bridge.world.item.crafting; - -public interface RecipeBridge { - - default boolean bridge$hasCustomIngredients() { - return false; - } -} diff --git a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java index 7efbae8803b..8ea8346f8ae 100644 --- a/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java +++ b/src/main/java/org/spongepowered/common/bridge/world/item/crafting/ShapedRecipeBridge.java @@ -26,6 +26,6 @@ import net.minecraft.world.item.crafting.ShapedRecipePattern; -public interface ShapedRecipeBridge extends RecipeBridge, RecipeResultBridge { +public interface ShapedRecipeBridge extends RecipeResultBridge { ShapedRecipePattern bridge$pattern(); } diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java index 5f91918b96a..7754ef5380e 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java +++ b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java @@ -75,7 +75,7 @@ public boolean canCraft( final PlacementInfo placement = recipe.placementInfo(); return !placement.isImpossibleToPlace() && this.stackedContents.tryPick( - ((PlacementInfoBridge) placement).bridge$stackIngredientInfos(), + ((PlacementInfoBridge) placement).bridge$getStackIngredientInfos(), amount, this.unwrapStackOutput(output)); } @@ -96,7 +96,7 @@ public int getBiggestCraftableStack( final StackedContents.@Nullable Output> output ) { return this.stackedContents.tryPickAll( - ((PlacementInfoBridge) recipe.placementInfo()).bridge$stackIngredientInfos(), + ((PlacementInfoBridge) recipe.placementInfo()).bridge$getStackIngredientInfos(), maxCount, this.unwrapStackOutput(output)); } diff --git a/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java index 259ed35782c..2c9b8d6496a 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/shapeless/SpongeShapelessRecipe.java @@ -33,7 +33,7 @@ import net.minecraft.world.item.crafting.ShapelessRecipe; import net.minecraft.world.level.Level; import org.spongepowered.common.accessor.world.item.crafting.ShapelessRecipeAccessor; -import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; +import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; import java.util.ArrayList; import java.util.Collection; @@ -66,7 +66,7 @@ public SpongeShapelessRecipe(final String groupIn, @Override public boolean matches(final CraftingInput $$0, final Level $$1) { - if (!((RecipeBridge) this).bridge$hasCustomIngredients()) { + if (!((PlacementInfoBridge) this.placementInfo()).bridge$hasCustomIngredients()) { return super.matches($$0, $$1); } return SpongeShapelessRecipe.matches($$0.items(), ((ShapelessRecipeAccessor) this).accessor$ingredients()); diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index 35b815ed962..0abfe9f2709 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -44,7 +44,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; +import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; import org.spongepowered.common.item.recipe.book.RecipeBookUtil; import org.spongepowered.common.item.recipe.book.SpongeStackedContentsOutputWrapper; import org.spongepowered.common.item.recipe.book.SpongeStackedItemContents; @@ -89,7 +89,7 @@ public abstract class ServerPlaceRecipeMixin { final boolean useMaxItems, final boolean isCreative ) { - if (((RecipeBridge) recipe.value()).bridge$hasCustomIngredients()) { + if (((PlacementInfoBridge) recipe.value().placementInfo()).bridge$hasCustomIngredients()) { return new SpongeStackedItemContents(); } else { return original.call(); diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java index 0855a007575..8a48f055bdf 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java @@ -35,6 +35,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; +import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; import java.util.List; @@ -43,16 +44,25 @@ public abstract class PlacementInfoMixin implements PlacementInfoBridge { @Shadow @Final private List ingredients; + private boolean impl$hasCustomIngredients; private List> impl$stackIngredientInfos; @Inject(method = "", at = @At("RETURN")) - private void impl$setIngredientInfos(final CallbackInfo ci) { + private void impl$setSpongeData(final CallbackInfo ci) { + this.impl$hasCustomIngredients = this.ingredients.stream() + .anyMatch(IngredientUtil::isCustom); + this.impl$stackIngredientInfos = this.ingredients.stream() .>map(ingredient -> ingredient::test) .toList(); } - public List> bridge$stackIngredientInfos() { + @Override + public boolean bridge$hasCustomIngredients() { + return this.impl$hasCustomIngredients; + } + + public List> bridge$getStackIngredientInfos() { return this.impl$stackIngredientInfos; } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java deleted file mode 100644 index c3db55d6ada..00000000000 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/RecipeMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.common.mixin.core.world.item.crafting; - -import net.minecraft.world.item.crafting.Recipe; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; - -@Mixin(Recipe.class) -public interface RecipeMixin extends RecipeBridge { -} diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java index eaa5eb3986e..dc3d3956890 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapedRecipeMixin.java @@ -30,11 +30,7 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.ShapedRecipeBridge; -import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; @Mixin(ShapedRecipe.class) public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { @@ -45,14 +41,6 @@ public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { // @formatter=on - private boolean impl$hasCustomIngredients; - - @Inject(method = "(Ljava/lang/String;Lnet/minecraft/world/item/crafting/CraftingBookCategory;Lnet/minecraft/world/item/crafting/ShapedRecipePattern;Lnet/minecraft/world/item/ItemStack;Z)V", at = @At("RETURN")) - private void impl$checkCustomIngredients(final CallbackInfo ci) { - this.impl$hasCustomIngredients = this.pattern.ingredients().stream() - .map(i -> i.orElse(null)) - .anyMatch(IngredientUtil::isCustom); - } @Override public ShapedRecipePattern bridge$pattern() { @@ -64,8 +52,5 @@ public abstract class ShapedRecipeMixin implements ShapedRecipeBridge { return this.result; } - @Override - public boolean bridge$hasCustomIngredients() { - return this.impl$hasCustomIngredients; - } + } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java index 73320c19706..25d9dd221c1 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/ShapelessRecipeMixin.java @@ -25,42 +25,23 @@ package org.spongepowered.common.mixin.core.world.item.crafting; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.ShapelessRecipe; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.common.bridge.world.item.crafting.RecipeBridge; import org.spongepowered.common.bridge.world.item.crafting.RecipeResultBridge; -import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; - -import java.util.List; @Mixin(ShapelessRecipe.class) -public abstract class ShapelessRecipeMixin implements RecipeBridge, RecipeResultBridge { +public abstract class ShapelessRecipeMixin implements RecipeResultBridge { // @formatter=off @Shadow @Final ItemStack result; - @Shadow @Final private List ingredients; - // @formatter=on - private boolean impl$hasCustomIngredients; + // @formatter=on - @Inject(method = "", at = @At("RETURN")) - private void impl$checkCustomIngredients(final CallbackInfo ci) { - this.impl$hasCustomIngredients = this.ingredients.stream().anyMatch(IngredientUtil::isCustom); - } @Override public ItemStack bridge$result() { return this.result; } - - @Override - public boolean bridge$hasCustomIngredients() { - return this.impl$hasCustomIngredients; - } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java index becdb442397..b0925a42a67 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/SingleItemRecipeMixin.java @@ -30,37 +30,22 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.RecipeResultBridge; -import org.spongepowered.common.item.recipe.ingredient.IngredientUtil; /** * {@link net.minecraft.world.item.crafting.StonecutterRecipe} */ @Mixin(SingleItemRecipe.class) -public abstract class SingleItemRecipeMixin implements RecipeMixin, RecipeResultBridge { +public abstract class SingleItemRecipeMixin implements RecipeResultBridge { // @formatter=off @Shadow @Final private ItemStack result; @Shadow public abstract Ingredient shadow$input(); // @formatter=on - private boolean impl$hasCustomIngredients; - - @Inject(method = "", at = @At("RETURN")) - private void impl$checkCustomIngredient(final CallbackInfo ci) { - this.impl$hasCustomIngredients = IngredientUtil.isCustom(this.shadow$input()); - } @Override public ItemStack bridge$result() { return this.result; } - - @Override - public boolean bridge$hasCustomIngredients() { - return this.impl$hasCustomIngredients; - } } diff --git a/src/mixins/resources/mixins.sponge.core.json b/src/mixins/resources/mixins.sponge.core.json index ed03340d6a9..3ef440abdba 100644 --- a/src/mixins/resources/mixins.sponge.core.json +++ b/src/mixins/resources/mixins.sponge.core.json @@ -214,7 +214,6 @@ "world.item.crafting.IngredientMixin", "world.item.crafting.PlacementInfoMixin", "world.item.crafting.RecipeManagerMixin", - "world.item.crafting.RecipeMixin", "world.item.crafting.ShapedRecipeMixin", "world.item.crafting.ShapelessRecipeMixin", "world.item.crafting.SingleItemRecipeMixin", From 7bede09af0b05b6a675363af742b1eb0f254bcf4 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 16:12:13 +0300 Subject: [PATCH 04/20] reduce the amount of injections --- .../SpongeStackedContentsOutputWrapper.java | 60 ----------- .../book/SpongeStackedItemContents.java | 24 +++-- .../recipebook/ServerPlaceRecipeMixin.java | 102 ++++-------------- .../item/crafting/PlacementInfoMixin.java | 1 + 4 files changed, 37 insertions(+), 150 deletions(-) delete mode 100644 src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java deleted file mode 100644 index d34ceee8772..00000000000 --- a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedContentsOutputWrapper.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.common.item.recipe.book; - -import net.minecraft.core.Holder; -import net.minecraft.world.entity.player.StackedContents; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -public class SpongeStackedContentsOutputWrapper implements StackedContents.Output> { - - private final StackedContents.Output> originalOutput; - private final StackedContents.Output wrappedStackOutput; - - public SpongeStackedContentsOutputWrapper( - final StackedContents.Output> original, - final StackedContents.Output toWrap - ) { - this.originalOutput = original; - this.wrappedStackOutput = stack -> { - originalOutput.accept(stack.getItemHolder()); - toWrap.accept(stack); - }; - } - - /** - * @deprecated Should not be used directly. - */ - @Deprecated - @Override - public void accept(final Holder holder) { - this.originalOutput.accept(holder); - } - - public StackedContents.Output stackOutput() { - return this.wrappedStackOutput; - } -} diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java index 7754ef5380e..4f0dbe71e6e 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java +++ b/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java @@ -57,6 +57,8 @@ public boolean equals(final @Nullable ItemStack a, final @Nullable ItemStack b) private final Object2ReferenceMap stackInterner = new Object2ReferenceOpenCustomHashMap<>(STACK_HASH_STRATEGY); private final StackedContents stackedContents = new StackedContents<>(); + private StackedContents.@Nullable Output stackOutput; + @Override public void accountStack(final ItemStack stack, final int maxStackSize) { if (!stack.isEmpty()) { @@ -76,7 +78,7 @@ public boolean canCraft( return !placement.isImpossibleToPlace() && this.stackedContents.tryPick( ((PlacementInfoBridge) placement).bridge$getStackIngredientInfos(), - amount, this.unwrapStackOutput(output)); + amount, this.createStackOutput(output)); } @Override @@ -97,7 +99,7 @@ public int getBiggestCraftableStack( ) { return this.stackedContents.tryPickAll( ((PlacementInfoBridge) recipe.placementInfo()).bridge$getStackIngredientInfos(), - maxCount, this.unwrapStackOutput(output)); + maxCount, this.createStackOutput(output)); } @Override @@ -106,18 +108,22 @@ public void clear() { this.stackedContents.clear(); } - private StackedContents.@Nullable Output unwrapStackOutput( + public void setStackOutput(final StackedContents.@Nullable Output stackOutput) { + this.stackOutput = stackOutput; + } + + private StackedContents.@Nullable Output createStackOutput( final StackedContents.@Nullable Output> output ) { if (output == null) { return null; - } else if (output instanceof final SpongeStackedContentsOutputWrapper spongeOutput) { - return spongeOutput.stackOutput(); + } else if (this.stackOutput == null) { + return stack -> output.accept(stack.getItemHolder()); } else { - // By default, this method is only called with wrapped outputs. - // If this happens, there is either error in Sponge impl or - // mixin from some mod and Sponge mixin is not applied last. - throw new UnsupportedOperationException("This should not have happened, please report about it"); + return stack -> { + output.accept(stack.getItemHolder()); + this.stackOutput.accept(stack); + }; } } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index 0abfe9f2709..cad23b66aff 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -24,13 +24,12 @@ */ package org.spongepowered.common.mixin.core.recipebook; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Share; import net.minecraft.core.Holder; import net.minecraft.recipebook.ServerPlaceRecipe; import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.entity.player.StackedItemContents; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.Item; @@ -42,11 +41,9 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; import org.spongepowered.common.item.recipe.book.RecipeBookUtil; -import org.spongepowered.common.item.recipe.book.SpongeStackedContentsOutputWrapper; import org.spongepowered.common.item.recipe.book.SpongeStackedItemContents; import java.util.ArrayList; @@ -61,14 +58,7 @@ public abstract class ServerPlaceRecipeMixin { @Shadow @Final private Inventory inventory; - /** - * {@link Share} is not applicable here because there is - * lambda mixin that does not get shared value passed into it. - * The method this field is used for modifies - * inventory so it should never be called async. - */ private @Nullable List impl$stackList; - private @Nullable ItemStack impl$currentExemplaryStackToMove; @WrapOperation( method = "placeRecipe(Lnet/minecraft/recipebook/ServerPlaceRecipe$CraftingMenuAccess;IILjava/util/List;Ljava/util/List;Lnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/item/crafting/RecipeHolder;ZZ)Lnet/minecraft/world/inventory/RecipeBookMenu$PostPlaceAction;", @@ -80,45 +70,32 @@ public abstract class ServerPlaceRecipeMixin { private static StackedItemContents impl$useCustomStackedItemContents( final Operation original, final ServerPlaceRecipe.CraftingMenuAccess menu, - final int gridWidth, - final int gridHeight, - final List inputGridSlots, - final List slotsToClear, - final Inventory inventory, - final RecipeHolder recipe, - final boolean useMaxItems, - final boolean isCreative + final int gridWidth, final int gridHeight, + final List inputGridSlots, final List slotsToClear, + final Inventory inventory, final RecipeHolder recipe, + final boolean useMaxItems, final boolean isCreative ) { - if (((PlacementInfoBridge) recipe.value().placementInfo()).bridge$hasCustomIngredients()) { - return new SpongeStackedItemContents(); - } else { - return original.call(); - } + return ((PlacementInfoBridge) recipe.value().placementInfo()).bridge$hasCustomIngredients() + ? new SpongeStackedItemContents() + : original.call(); } - @Inject( - method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", - at = @At( - value = "NEW", - target = "()Ljava/util/ArrayList;" - ) - ) - private void impl$setStackList( - final RecipeHolder recipe, final StackedItemContents stackedContents, final CallbackInfo ci + @WrapMethod(method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V") + private void impl$handleApplicableStacks( + final RecipeHolder recipe, final StackedItemContents contents, final Operation original ) { - if (stackedContents instanceof SpongeStackedItemContents) { + if (contents instanceof final SpongeStackedItemContents spongeContents) { this.impl$stackList = new ArrayList<>(); + // In wrapped method the used output (if not null) is always List>::add + spongeContents.setStackOutput(this.impl$stackList::add); + original.call(recipe, contents); + spongeContents.setStackOutput(null); + this.impl$stackList = null; + } else { + original.call(recipe, contents); } } - @Inject( - method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", - at = @At("RETURN") - ) - private void impl$unsetStackList(final CallbackInfo ci) { - this.impl$stackList = null; - } - @Inject( method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", at = @At( @@ -132,43 +109,6 @@ public abstract class ServerPlaceRecipeMixin { } } - @ModifyArg( - method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/StackedItemContents;canCraft(Lnet/minecraft/world/item/crafting/Recipe;ILnet/minecraft/world/entity/player/StackedContents$Output;)Z" - ) - ) - private StackedContents.Output> impl$wrapContentsOutput( - final StackedContents.Output> originalOutput - ) { - return this.impl$stackList == null - ? originalOutput - : new SpongeStackedContentsOutputWrapper(originalOutput, this.impl$stackList::add); - } - - @WrapOperation( - method = "lambda$placeRecipe$0", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/recipebook/ServerPlaceRecipe;moveItemToGrid(Lnet/minecraft/world/inventory/Slot;Lnet/minecraft/core/Holder;I)I" - ) - ) - private int impl$adjustItemMoveLogic( - final ServerPlaceRecipe instance, final Slot craftInputSlot, - final Holder exemplaryItem, final int amountToMove, - final Operation original, - final List> exemplaryItems, final int totalAmountToCraft, - final Integer exemplaryItemIndex, final int slotIndex, final int x, final int y - ) { - if (this.impl$stackList != null) { - this.impl$currentExemplaryStackToMove = this.impl$stackList.get(exemplaryItemIndex); - } - final int result = original.call(instance, craftInputSlot, exemplaryItem, amountToMove); - this.impl$currentExemplaryStackToMove = null; - return result; - } - @WrapOperation( method = "moveItemToGrid", at = @At( @@ -179,8 +119,8 @@ public abstract class ServerPlaceRecipeMixin { private int impl$adjustMatchingSlotFinder( final Inventory instance, final Holder exemplaryItem, final ItemStack craftInputStack, final Operation original ) { - return this.impl$currentExemplaryStackToMove == null + return this.impl$stackList == null ? original.call(instance, exemplaryItem, craftInputStack) - : RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$currentExemplaryStackToMove, craftInputStack); + : RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$stackList.removeFirst(), craftInputStack); } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java index 8a48f055bdf..f713a19a420 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/world/item/crafting/PlacementInfoMixin.java @@ -62,6 +62,7 @@ public abstract class PlacementInfoMixin implements PlacementInfoBridge { return this.impl$hasCustomIngredients; } + @Override public List> bridge$getStackIngredientInfos() { return this.impl$stackIngredientInfos; } From ab60d1cf9fe0bffe10f80f4b09db2c2fc0327c0d Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 17:13:01 +0300 Subject: [PATCH 05/20] move some things --- .../item/recipe/book/RecipeBookUtil.java | 55 ------------------- .../item/recipe/crafting/RecipeUtil.java | 23 ++++++++ .../SpongeStackedItemContents.java | 2 +- .../recipebook/ServerPlaceRecipeMixin.java | 6 +- 4 files changed, 27 insertions(+), 59 deletions(-) delete mode 100644 src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java rename src/main/java/org/spongepowered/common/item/recipe/{book => crafting}/SpongeStackedItemContents.java (99%) diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java b/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java deleted file mode 100644 index 9c829f1243a..00000000000 --- a/src/main/java/org/spongepowered/common/item/recipe/book/RecipeBookUtil.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of Sponge, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.common.item.recipe.book; - -import net.minecraft.core.Holder; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.item.ItemStack; - -public final class RecipeBookUtil { - - /** - * Copied from {@link Inventory#findSlotMatchingCraftingIngredient(Holder, ItemStack)} - * and adjusted to use exemplary {@link ItemStack} instead of just item type. - */ - public static int findSlotMatchingCraftingIngredient( - final Inventory inventory, final ItemStack exemplaryStackToMove, final ItemStack craftInputStack - ) { - for (int i = 0; i < inventory.items.size(); i++) { - final ItemStack stack = inventory.items.get(i); - if (!stack.isEmpty() - && Inventory.isUsableForCrafting(stack) - && ItemStack.isSameItemSameComponents(exemplaryStackToMove, stack) - && (craftInputStack.isEmpty() || ItemStack.isSameItemSameComponents(craftInputStack, stack))) { - return i; - } - } - - return -1; - } - - private RecipeBookUtil() { - } -} diff --git a/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java index 180026731e4..9b417c934b8 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java @@ -24,7 +24,10 @@ */ package org.spongepowered.common.item.recipe.crafting; +import net.minecraft.core.Holder; import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.display.DisplayContentsFactory; import net.minecraft.world.item.crafting.display.SlotDisplayContext; import org.spongepowered.api.Sponge; @@ -51,4 +54,24 @@ public T addRemainder(T var1, List var2) { return null; } } + + /** + * Copied from {@link Inventory#findSlotMatchingCraftingIngredient(Holder, ItemStack)} + * and adjusted to use exemplary {@link ItemStack} instead of just item type. + */ + public static int findSlotMatchingCraftingIngredient( + final Inventory inventory, final ItemStack exemplaryStackToMove, final ItemStack craftInputStack + ) { + for (int i = 0; i < inventory.items.size(); i++) { + final ItemStack stack = inventory.items.get(i); + if (!stack.isEmpty() + && Inventory.isUsableForCrafting(stack) + && ItemStack.isSameItemSameComponents(exemplaryStackToMove, stack) + && (craftInputStack.isEmpty() || ItemStack.isSameItemSameComponents(craftInputStack, stack))) { + return i; + } + } + + return -1; + } } diff --git a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java similarity index 99% rename from src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java rename to src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java index 4f0dbe71e6e..bf8c8720e73 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/book/SpongeStackedItemContents.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.common.item.recipe.book; +package org.spongepowered.common.item.recipe.crafting; import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index cad23b66aff..cc79ed8cf72 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -43,8 +43,8 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; -import org.spongepowered.common.item.recipe.book.RecipeBookUtil; -import org.spongepowered.common.item.recipe.book.SpongeStackedItemContents; +import org.spongepowered.common.item.recipe.crafting.RecipeUtil; +import org.spongepowered.common.item.recipe.crafting.SpongeStackedItemContents; import java.util.ArrayList; import java.util.List; @@ -121,6 +121,6 @@ public abstract class ServerPlaceRecipeMixin { ) { return this.impl$stackList == null ? original.call(instance, exemplaryItem, craftInputStack) - : RecipeBookUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$stackList.removeFirst(), craftInputStack); + : RecipeUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$stackList.removeFirst(), craftInputStack); } } From f08fc220cc418c018b3a1cb3f371269d309e4f02 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 18:29:52 +0300 Subject: [PATCH 06/20] follow the review --- .../item/recipe/crafting/RecipeUtil.java | 23 ------- .../crafting/SpongeStackedItemContents.java | 50 +++++++-------- .../recipebook/ServerPlaceRecipeMixin.java | 63 ++++--------------- 3 files changed, 36 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java index 9b417c934b8..180026731e4 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/RecipeUtil.java @@ -24,10 +24,7 @@ */ package org.spongepowered.common.item.recipe.crafting; -import net.minecraft.core.Holder; import net.minecraft.util.context.ContextMap; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.display.DisplayContentsFactory; import net.minecraft.world.item.crafting.display.SlotDisplayContext; import org.spongepowered.api.Sponge; @@ -54,24 +51,4 @@ public T addRemainder(T var1, List var2) { return null; } } - - /** - * Copied from {@link Inventory#findSlotMatchingCraftingIngredient(Holder, ItemStack)} - * and adjusted to use exemplary {@link ItemStack} instead of just item type. - */ - public static int findSlotMatchingCraftingIngredient( - final Inventory inventory, final ItemStack exemplaryStackToMove, final ItemStack craftInputStack - ) { - for (int i = 0; i < inventory.items.size(); i++) { - final ItemStack stack = inventory.items.get(i); - if (!stack.isEmpty() - && Inventory.isUsableForCrafting(stack) - && ItemStack.isSameItemSameComponents(exemplaryStackToMove, stack) - && (craftInputStack.isEmpty() || ItemStack.isSameItemSameComponents(craftInputStack, stack))) { - return i; - } - } - - return -1; - } } diff --git a/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java b/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java index bf8c8720e73..2b5bea2351a 100644 --- a/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java +++ b/src/main/java/org/spongepowered/common/item/recipe/crafting/SpongeStackedItemContents.java @@ -37,10 +37,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; -import java.util.List; import java.util.function.Function; -public class SpongeStackedItemContents extends StackedItemContents { +public final class SpongeStackedItemContents extends StackedItemContents { private static final Hash.Strategy STACK_HASH_STRATEGY = new Hash.Strategy<>() { @Override @@ -54,13 +53,27 @@ public boolean equals(final @Nullable ItemStack a, final @Nullable ItemStack b) } }; - private final Object2ReferenceMap stackInterner = new Object2ReferenceOpenCustomHashMap<>(STACK_HASH_STRATEGY); + private final Object2ReferenceMap stackInterner = + new Object2ReferenceOpenCustomHashMap<>(SpongeStackedItemContents.STACK_HASH_STRATEGY); private final StackedContents stackedContents = new StackedContents<>(); - private StackedContents.@Nullable Output stackOutput; + private final StackedContents.Output addCallback; + private final Runnable clearCallback; + + public SpongeStackedItemContents( + final StackedContents.Output addCallback, final Runnable clearCallback + ) { + this.addCallback = addCallback; + this.clearCallback = clearCallback; + } @Override public void accountStack(final ItemStack stack, final int maxStackSize) { + // Account to parent contents because it's used in + // #canCraft(List>>, StackedContents.Output>) + // and we can't safely override it to use StackedContents + super.accountStack(stack, maxStackSize); + if (!stack.isEmpty()) { // StackedContents works on Reference2IntMap, so if we meet stack which "same" copy // has already been accounted we would need to provide the stack that was met first. @@ -81,17 +94,6 @@ public boolean canCraft( amount, this.createStackOutput(output)); } - @Override - public boolean canCraft( - final List>> ingredients, - final StackedContents.@Nullable Output> output - ) { - // By default, this method is not called in the context the instance of this class is created. - // If this happens, it's either error in Sponge impl or - // mixin from some mod (which should be inspected instead of silently doing something that impl does not expect). - throw new UnsupportedOperationException("This method should not have been called, please report about it"); - } - @Override public int getBiggestCraftableStack( final Recipe recipe, final int maxCount, @@ -104,26 +106,22 @@ public int getBiggestCraftableStack( @Override public void clear() { + super.clear(); this.stackInterner.clear(); this.stackedContents.clear(); } - public void setStackOutput(final StackedContents.@Nullable Output stackOutput) { - this.stackOutput = stackOutput; - } - private StackedContents.@Nullable Output createStackOutput( final StackedContents.@Nullable Output> output ) { if (output == null) { return null; - } else if (this.stackOutput == null) { - return stack -> output.accept(stack.getItemHolder()); - } else { - return stack -> { - output.accept(stack.getItemHolder()); - this.stackOutput.accept(stack); - }; } + + this.clearCallback.run(); + return stack -> { + output.accept(stack.getItemHolder()); + this.addCallback.accept(stack); + }; } } diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index cc79ed8cf72..5561f1f5c3e 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -24,29 +24,23 @@ */ package org.spongepowered.common.mixin.core.recipebook; -import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import net.minecraft.core.Holder; +import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.recipebook.ServerPlaceRecipe; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.StackedItemContents; import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeHolder; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; -import org.spongepowered.common.item.recipe.crafting.RecipeUtil; import org.spongepowered.common.item.recipe.crafting.SpongeStackedItemContents; -import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; /** @@ -56,9 +50,7 @@ @Mixin(ServerPlaceRecipe.class) public abstract class ServerPlaceRecipeMixin { - @Shadow @Final private Inventory inventory; - - private @Nullable List impl$stackList; + private Deque impl$stackList = new ArrayDeque<>(); @WrapOperation( method = "placeRecipe(Lnet/minecraft/recipebook/ServerPlaceRecipe$CraftingMenuAccess;IILjava/util/List;Ljava/util/List;Lnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/item/crafting/RecipeHolder;ZZ)Lnet/minecraft/world/inventory/RecipeBookMenu$PostPlaceAction;", @@ -73,54 +65,23 @@ public abstract class ServerPlaceRecipeMixin { final int gridWidth, final int gridHeight, final List inputGridSlots, final List slotsToClear, final Inventory inventory, final RecipeHolder recipe, - final boolean useMaxItems, final boolean isCreative + final boolean useMaxItems, final boolean isCreative, + final @Local ServerPlaceRecipe placeRecipe ) { + final ServerPlaceRecipeMixin mixed = (ServerPlaceRecipeMixin) (Object) placeRecipe; return ((PlacementInfoBridge) recipe.value().placementInfo()).bridge$hasCustomIngredients() - ? new SpongeStackedItemContents() + ? new SpongeStackedItemContents(mixed.impl$stackList::add, mixed.impl$stackList::clear) : original.call(); } - @WrapMethod(method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V") - private void impl$handleApplicableStacks( - final RecipeHolder recipe, final StackedItemContents contents, final Operation original - ) { - if (contents instanceof final SpongeStackedItemContents spongeContents) { - this.impl$stackList = new ArrayList<>(); - // In wrapped method the used output (if not null) is always List>::add - spongeContents.setStackOutput(this.impl$stackList::add); - original.call(recipe, contents); - spongeContents.setStackOutput(null); - this.impl$stackList = null; - } else { - original.call(recipe, contents); - } - } - - @Inject( - method = "placeRecipe(Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/world/entity/player/StackedItemContents;)V", - at = @At( - value = "INVOKE", - target = "Ljava/util/List;clear()V" - ) - ) - private void impl$clearStackList(final CallbackInfo ci) { - if (this.impl$stackList != null) { - this.impl$stackList.clear(); - } - } - - @WrapOperation( + @ModifyArg( method = "moveItemToGrid", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Inventory;findSlotMatchingCraftingIngredient(Lnet/minecraft/core/Holder;Lnet/minecraft/world/item/ItemStack;)I" ) ) - private int impl$adjustMatchingSlotFinder( - final Inventory instance, final Holder exemplaryItem, final ItemStack craftInputStack, final Operation original - ) { - return this.impl$stackList == null - ? original.call(instance, exemplaryItem, craftInputStack) - : RecipeUtil.findSlotMatchingCraftingIngredient(inventory, this.impl$stackList.removeFirst(), craftInputStack); + private ItemStack impl$adjustMatchingSlotFinder(final ItemStack craftInputStack) { + return this.impl$stackList.isEmpty() ? craftInputStack : this.impl$stackList.poll(); } } From 1d935e24596019ca4a50ec2bbb7a4c91ce96f449 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 26 Oct 2025 20:03:15 +0300 Subject: [PATCH 07/20] fail finding slot when exemplary and craft input stacks can't stack --- .../recipebook/ServerPlaceRecipeMixin.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index 5561f1f5c3e..4a229440f35 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -27,15 +27,16 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.core.Holder; import net.minecraft.recipebook.ServerPlaceRecipe; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.StackedItemContents; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeHolder; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; import org.spongepowered.common.item.recipe.crafting.SpongeStackedItemContents; @@ -74,14 +75,25 @@ public abstract class ServerPlaceRecipeMixin { : original.call(); } - @ModifyArg( + @WrapOperation( method = "moveItemToGrid", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Inventory;findSlotMatchingCraftingIngredient(Lnet/minecraft/core/Holder;Lnet/minecraft/world/item/ItemStack;)I" ) ) - private ItemStack impl$adjustMatchingSlotFinder(final ItemStack craftInputStack) { - return this.impl$stackList.isEmpty() ? craftInputStack : this.impl$stackList.poll(); + private int impl$adjustMatchingSlotFinder( + final Inventory instance, final Holder exemplaryItem, final ItemStack craftInputStack, + final Operation original + ) { + if (this.impl$stackList.isEmpty()) { + return original.call(instance, exemplaryItem, craftInputStack); + } else { + final ItemStack input = this.impl$stackList.poll(); + if (!craftInputStack.isEmpty() && !ItemStack.isSameItemSameComponents(craftInputStack, input)) { + return -1; + } + return original.call(instance, exemplaryItem, input); + } } } From 17ae6d8527047fe5d07966150774caf1e96463a0 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Thu, 30 Oct 2025 16:56:37 +0300 Subject: [PATCH 08/20] add RecipePlaceTest --- .../recipebook/ServerPlaceRecipeMixin.java | 31 +- .../common/recipe/RecipePlaceTest.java | 431 ++++++++++++++++++ 2 files changed, 449 insertions(+), 13 deletions(-) create mode 100644 src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java diff --git a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java index 4a229440f35..88d4da1fa57 100644 --- a/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java +++ b/src/mixins/java/org/spongepowered/common/mixin/core/recipebook/ServerPlaceRecipeMixin.java @@ -35,6 +35,7 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeHolder; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.common.bridge.world.item.crafting.PlacementInfoBridge; @@ -42,7 +43,6 @@ import java.util.ArrayDeque; import java.util.Deque; -import java.util.List; /** * Makes recipe placing to work with sponge custom ingredients. @@ -52,6 +52,8 @@ public abstract class ServerPlaceRecipeMixin { private Deque impl$stackList = new ArrayDeque<>(); + private @Nullable Slot impl$lastSlot; + private @Nullable ItemStack impl$lastStack; @WrapOperation( method = "placeRecipe(Lnet/minecraft/recipebook/ServerPlaceRecipe$CraftingMenuAccess;IILjava/util/List;Ljava/util/List;Lnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/item/crafting/RecipeHolder;ZZ)Lnet/minecraft/world/inventory/RecipeBookMenu$PostPlaceAction;", @@ -62,11 +64,7 @@ public abstract class ServerPlaceRecipeMixin { ) private static StackedItemContents impl$useCustomStackedItemContents( final Operation original, - final ServerPlaceRecipe.CraftingMenuAccess menu, - final int gridWidth, final int gridHeight, - final List inputGridSlots, final List slotsToClear, - final Inventory inventory, final RecipeHolder recipe, - final boolean useMaxItems, final boolean isCreative, + final @Local(argsOnly = true) RecipeHolder recipe, final @Local ServerPlaceRecipe placeRecipe ) { final ServerPlaceRecipeMixin mixed = (ServerPlaceRecipeMixin) (Object) placeRecipe; @@ -84,16 +82,23 @@ public abstract class ServerPlaceRecipeMixin { ) private int impl$adjustMatchingSlotFinder( final Inventory instance, final Holder exemplaryItem, final ItemStack craftInputStack, - final Operation original + final Operation original, + final @Local(argsOnly = true) Slot craftInputSlot ) { + if (this.impl$lastSlot == craftInputSlot) { + return original.call(instance, exemplaryItem, this.impl$lastStack); + } + if (this.impl$stackList.isEmpty()) { return original.call(instance, exemplaryItem, craftInputStack); - } else { - final ItemStack input = this.impl$stackList.poll(); - if (!craftInputStack.isEmpty() && !ItemStack.isSameItemSameComponents(craftInputStack, input)) { - return -1; - } - return original.call(instance, exemplaryItem, input); } + + if (!craftInputStack.isEmpty() && !ItemStack.isSameItemSameComponents(craftInputStack, this.impl$stackList.peek())) { + return -1; + } + + this.impl$lastSlot = craftInputSlot; + this.impl$lastStack = this.impl$stackList.poll(); + return original.call(instance, exemplaryItem, this.impl$lastStack); } } diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java new file mode 100644 index 00000000000..564ee1707a7 --- /dev/null +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -0,0 +1,431 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.recipe; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import com.mojang.authlib.GameProfile; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.RecipeBookMenu; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeInput; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.data.Keys; +import org.spongepowered.api.item.ItemTypes; +import org.spongepowered.api.item.inventory.Inventory; +import org.spongepowered.api.item.inventory.ItemStack; +import org.spongepowered.api.item.inventory.Slot; +import org.spongepowered.api.item.recipe.RecipeTypes; +import org.spongepowered.api.item.recipe.cooking.CookingRecipe; +import org.spongepowered.api.item.recipe.crafting.CraftingRecipe; +import org.spongepowered.api.item.recipe.crafting.Ingredient; +import org.spongepowered.api.registry.RegistryTypes; +import org.spongepowered.api.util.Builder; +import org.spongepowered.common.SpongeCommon; +import org.spongepowered.common.accessor.world.inventory.AbstractCraftingMenuAccessor; +import org.spongepowered.common.accessor.world.inventory.AbstractFurnaceMenuAccessor; +import org.spongepowered.common.item.util.ItemStackUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public final class RecipePlaceTest { + + // Utilities + + private static int CONTAINER_COUNTER = 0; + + private static RecipeBookMenu createMenu(final MenuType menuType, final ServerPlayer player) { + return menuType.create(RecipePlaceTest.CONTAINER_COUNTER++, player.getInventory()); + } + + private static String stackToString(final ItemStack stack) { + return stack.isEmpty() + ? "(empty)" + : String.format("(%s: %s, max: %s)", + stack.type().key(RegistryTypes.ITEM_TYPE).value(), stack.quantity(), stack.maxStackQuantity()); + } + + private static String inventoryToString(final Inventory inventory, final boolean removeEmpty) { + return inventory.slots().stream() + .map(Slot::peek) + .filter(stack -> !removeEmpty || !stack.isEmpty()) + .map(RecipePlaceTest::stackToString) + .collect(Collectors.joining(", ")); + } + + private static ItemStack withQuantity(final ItemStack stack, final int quantity) { + final ItemStack copy = stack.copy(); + copy.setQuantity(quantity); + return copy; + } + + // Tests + + private static void testRecipe( + final RecipeBookMenu menu, final ServerPlayer player, final TestContext context, + final Function inputInventoryProvider, final Function inputProvider + ) { + final Inventory playerInventory = (Inventory) player.getInventory(); + final List initialInventory = context.inventory(); + playerInventory.clear(); + for (int i = 0; i < initialInventory.size(); ++i) { + playerInventory.set(i, initialInventory.get(i)); + } + + final List initialInput = context.input(); + final Inventory input = (Inventory) inputInventoryProvider.apply(menu); + assertTrue(initialInput.size() <= input.capacity(), + () -> String.format("Initial input size (%s) is greater than actual input size (%s)", + initialInput.size(), input.capacity())); + + input.clear(); + for (int i = 0; i < initialInput.size(); ++i) { + input.set(i, initialInput.get(i)); + } + + final List inputs = new ArrayList<>(); + final List inventories = new ArrayList<>(); + inputs.add(RecipePlaceTest.inventoryToString(input, false)); + inventories.add(RecipePlaceTest.inventoryToString(playerInventory, true)); + for (int i = 0; i < context.clickAmount(); ++i) { + menu.handlePlacement(context.shiftClick(), true, context.recipe(), player.serverLevel(), player.getInventory()); + inputs.add(RecipePlaceTest.inventoryToString(input, false)); + inventories.add(RecipePlaceTest.inventoryToString(playerInventory, true)); + } + + final Supplier history = () -> "Placement history:\n" + + IntStream.range(0, inputs.size()) + .map(click -> inputs.size() - click - 1) + .mapToObj(click -> String.format(""" + - After click %s: + - Input: %s + - Inventory: %s""", + click, inputs.get(click), inventories.get(click))) + .collect(Collectors.joining("\n")); + + if (context.expectedCrafts() != 0) { + assertTrue(((Recipe) context.recipe().value()).matches(inputProvider.apply((T) input), player.level()), + () -> "Recipe does not match\n" + history.get()); + } + + final List quantities = input.slots().stream() + .map(Slot::peek) + .map(ItemStack::quantity) + .distinct() + .toList(); + final int max = quantities.stream().max(Integer::compare).get(); + final int min = quantities.stream().filter(quantity -> quantity != 0).min(Integer::compare).orElse(context.expectedCrafts()); + assertTrue(context.expectedCrafts() == max && min == max, + () -> String.format("Expected %s items in each slot but found %s\n%s", + context.expectedCrafts(), context.expectedCrafts() == max ? min : max, history.get())); + } + + private static void testRecipe( + final MenuType menuType, final ServerPlayer player, final TestContext context, + final Function inputInventoryProvider, final Function inputProvider + ) { + RecipePlaceTest.testRecipe(RecipePlaceTest.createMenu(menuType, player), player, context, inputInventoryProvider, inputProvider); + } + + private static void testCraftingRecipe(final ServerPlayer player, final TestContext context) { + RecipePlaceTest.testRecipe(MenuType.CRAFTING, player, context, + menu -> ((AbstractCraftingMenuAccessor) menu).accessor$craftSlots(), + CraftingContainer::asCraftInput); + } + + private static void testSmeltingRecipe(final ServerPlayer player, final TestContext context) { + RecipePlaceTest.testRecipe(MenuType.FURNACE, player, context, + menu -> ((Inventory) ((AbstractFurnaceMenuAccessor) menu).accessor$container()).slot(0).get(), + slot -> new SingleRecipeInput(ItemStackUtil.toNative(slot.peek()))); + } + + // TestContexts + + private static TestContext context(final String key, final Builder, ?> spongeRecipe) { + return new TestContext(new RecipeHolder<>( + net.minecraft.resources.ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("sponge", key)), + (Recipe) spongeRecipe.build())); + } + + private static Stream populateTests( + final TestContext baseTest, + final int regularExpectedCrafts, + // This exists due to vanilla having a bug with shift-placing that + // pulls item to crafting grid up to exactly ItemType's max stack size + // even if ItemStack's max stack size is less or more than that. + final int shiftExpectedCrafts, + final List partialInventory, final List partialInput, + final List badInventory, final List badInput + ) { + final List totalInitialInventory = Stream.concat(partialInventory.stream(), partialInput.stream()).toList(); + final List baseInputs = List.of( + baseTest.name("Empty input").input(List.of()), + baseTest.name("Bad input").input(badInput) + ); + + final Stream toFail = baseInputs.stream() + .flatMap(context -> Stream.of( + context.name("Empty inventory").inventory(List.of()), + context.name("Bad inventory").inventory(badInventory) + )) + .flatMap(context -> Stream.of(context, context.shift())) + // 2 clicks is enough to ensure we always fail + .flatMap(context -> Stream.of(context, context.clicks(2))) + .map(context -> context.crafts(0)); + + final TestContext regularTest = baseTest.name("Partial input").inventory(partialInventory).input(partialInput); + final Stream toMatchSingleClick = Stream.concat( + baseInputs.stream().map(context -> context.inventory(totalInitialInventory)), + Stream.of(regularTest) + ).flatMap(context -> Stream.of( + context.crafts(1), + context.shift().crafts(shiftExpectedCrafts) + )); + + // If tests above pass, after first click we end up with the same layout no matter the initial input. + // So we can perform multiple-click tests on a single input. + final Stream toMatchMultipleClicks = Stream.of(regularTest) + .flatMap(context -> Stream.concat( + IntStream.rangeClosed(2, regularExpectedCrafts) + .mapToObj(clicks -> context.clicks(clicks).crafts(clicks)), + Stream.of( + context.clicks(regularExpectedCrafts + 1).crafts(regularExpectedCrafts), + context.shift().clicks(2).crafts(shiftExpectedCrafts)) + )); + + return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); + } + + private static Stream streamCraftingRecipes() { + final ItemStack empty = ItemStack.empty(); + final ItemStack bedrock = ItemStack.of(ItemTypes.BEDROCK); + final ItemStack stone64 = ItemStack.of(ItemTypes.STONE, 64); + final ItemStack pearl = ItemStack.of(ItemTypes.ENDER_PEARL); + final ItemStack pearl16 = RecipePlaceTest.withQuantity(pearl, 16); + final ItemStack smallPearl = pearl.copy(); + smallPearl.offer(Keys.MAX_STACK_SIZE, 32); + final ItemStack smallPearl4 = RecipePlaceTest.withQuantity(smallPearl, 4); + final ItemStack smallPearl32 = RecipePlaceTest.withQuantity(smallPearl, 32); + final ItemStack bigPearl = pearl.copy(); + bigPearl.offer(Keys.MAX_STACK_SIZE, 8); + final ItemStack bigPearl4 = RecipePlaceTest.withQuantity(bigPearl, 4); + final ItemStack bigPearl8 = RecipePlaceTest.withQuantity(bigPearl, 8); + final ItemStack result = ItemStack.of(ItemTypes.BARRIER); + + final Ingredient stoneIngredient = Ingredient.of(stone64.type()); + final Ingredient anyPearlIngredient = Ingredient.of(pearl.type()); + final Ingredient smallPearlIngredient = Ingredient.of(ResourceKey.sponge("small_pearl"), + stack -> stack.type() == smallPearl.type() + && stack.maxStackQuantity() == smallPearl.maxStackQuantity(), + pearl); + final Ingredient bigPearlIngredient = Ingredient.of(ResourceKey.sponge("big_pearl"), + stack -> stack.type() == bigPearl.type() + && stack.maxStackQuantity() == bigPearl.maxStackQuantity(), + pearl); + + return Stream.of( + RecipePlaceTest.populateTests( + RecipePlaceTest.context("regular_shaped_crafting", CraftingRecipe.shapedBuilder() + .aisle("S S", " P ") + .where('S', stoneIngredient) + .where('P', anyPearlIngredient) + .result(result)), + 8, 16, + List.of(stone64, bigPearl8, bigPearl8), + List.of( + stone64, empty, empty, + empty, bigPearl4, empty), + Collections.nCopies(9, bedrock), Collections.nCopies(9, bedrock) + ), + + RecipePlaceTest.populateTests( + RecipePlaceTest.context("custom_shaped_crafting", CraftingRecipe.shapedBuilder() + .aisle("SSS", "BBB", "SSS") + .where('S', smallPearlIngredient) + .where('B', bigPearlIngredient) + .result(result)), + 4, 4, + List.of(smallPearl32, bigPearl, bigPearl, smallPearl32, bigPearl, bigPearl, smallPearl32), + List.of( + smallPearl4, empty, smallPearl4, + bigPearl4, empty, bigPearl4, + smallPearl, empty, empty + ), + List.of(pearl16), Collections.nCopies(9, pearl) + ), + + RecipePlaceTest.populateTests( + RecipePlaceTest.context("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient)), + 16, 16, + List.of(smallPearl32), + List.of( + smallPearl4, stone64), + Collections.nCopies(10, bedrock), Collections.nCopies(3, bedrock) + )/*, + + //TODO uncomment after shapeless recipe fix + RecipePlaceTest.populateTests( + RecipePlaceTest.context("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients( + smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, + bigPearlIngredient, bigPearlIngredient, bigPearlIngredient)), + 8, 16, + Collections.nCopies(5, bigPearl8), + List.of( + smallPearl32, smallPearl32, smallPearl, + bigPearl4, bigPearl4, empty), + List.of(pearl16), Collections.nCopies(6, pearl) + )*/ + ).flatMap(Function.identity()); + } + + private static Stream streamSmeltingRecipes() { + final ItemStack bedrock = ItemStack.of(ItemTypes.BEDROCK, 64); + final ItemStack snowball = ItemStack.of(ItemTypes.SNOWBALL); + final ItemStack snowball4 = RecipePlaceTest.withQuantity(snowball, 4); + final ItemStack bigSnowball = snowball.copy(); + bigSnowball.offer(Keys.MAX_STACK_SIZE, 4); + final ItemStack result = ItemStack.of(ItemTypes.BARRIER); + + return Stream.of( + RecipePlaceTest.populateTests( + RecipePlaceTest.context("regular_smelting", CookingRecipe.builder() + .type(RecipeTypes.SMELTING) + .ingredient(Ingredient.of(snowball.type())) + .result(result)), + 4, 8, + Collections.nCopies(8, bigSnowball), List.of(), + List.of(bedrock), List.of(bedrock) + ), + + RecipePlaceTest.populateTests( + RecipePlaceTest.context("custom_smelting", CookingRecipe.builder() + .type(RecipeTypes.SMELTING) + .ingredient(Ingredient.of(ResourceKey.sponge("big_snowball"), + stack -> stack.type() == bigSnowball.type() + && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), + snowball)) + .result(result)), + 4, 8, + Collections.nCopies(8, bigSnowball), List.of(), + List.of(snowball4), List.of(snowball4) + ) + ).flatMap(Function.identity()); + } + + @TestFactory + public Stream testRecipes() { + final ServerPlayer player = new FakePlayer(SpongeCommon.server().overworld(), new GameProfile(UUID.randomUUID(), "Player")); + return Stream.of( + RecipePlaceTest.streamCraftingRecipes().map(context -> + dynamicTest(context.asTestName(), () -> RecipePlaceTest.testCraftingRecipe(player, context))), + RecipePlaceTest.streamSmeltingRecipes().map(context -> + dynamicTest(context.asTestName(), () -> RecipePlaceTest.testSmeltingRecipe(player, context))) + ).flatMap(Function.identity()); + } + + private record TestContext( + RecipeHolder recipe, String testName, int expectedCrafts, boolean shiftClick, int clickAmount, + List inventory, List input + ) { + public TestContext(final RecipeHolder recipe) { + this(recipe, "", 1, false, 1, List.of(), List.of()); + } + + public TestContext name(final String testName) { + final String newTestName = this.testName.isEmpty() ? testName : (this.testName + ", " + testName); + return new TestContext(this.recipe, newTestName, this.expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, this.input); + } + + public TestContext crafts(final int expectedCrafts) { + return new TestContext(this.recipe, this.testName, expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, this.input); + } + + public TestContext shift() { + return new TestContext(this.recipe, this.testName, this.expectedCrafts, true, this.clickAmount, this.inventory, this.input); + } + + public TestContext clicks(final int clickAmount) { + return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, clickAmount, this.inventory, this.input); + } + + public TestContext inventory(final List items) { + return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, this.clickAmount, items, this.input); + } + + public TestContext input(final List items) { + return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, items); + } + + public String asTestName() { + return String.format("%s recipe (crafts: %s, shift: %s, clicks: %s, %s)", + this.recipe.id().location().getPath(), + this.expectedCrafts, + this.shiftClick, + this.clickAmount, + this.testName.isEmpty() ? "Regular" : this.testName); + } + } + + private static final class FakePlayer extends ServerPlayer { + + public FakePlayer(final ServerLevel level, final GameProfile name) { + super(level.getServer(), level, name, ClientInformation.createDefault()); + this.connection = new GamePacketListener(this); + } + + private static final class GamePacketListener extends ServerGamePacketListenerImpl { + private static final Connection DUMMY_CONNECTION = new Connection(PacketFlow.SERVERBOUND); + + public GamePacketListener(final ServerPlayer player) { + super(player.server, GamePacketListener.DUMMY_CONNECTION, player, CommonListenerCookie.createInitial(player.getGameProfile(), false)); + } + } + } +} From 073359894108cfd6fea48af8aa3888559b9eb258 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 31 Oct 2025 01:38:03 +0300 Subject: [PATCH 09/20] follow the review --- .../common/recipe/RecipePlaceTest.java | 545 ++++++++++-------- 1 file changed, 302 insertions(+), 243 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index 564ee1707a7..acb61b43db1 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -37,13 +37,10 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.CommonListenerCookie; import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.RecipeBookMenu; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeHolder; -import net.minecraft.world.item.crafting.RecipeInput; -import net.minecraft.world.item.crafting.SingleRecipeInput; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.spongepowered.api.ResourceKey; @@ -63,24 +60,18 @@ import org.spongepowered.common.accessor.world.inventory.AbstractFurnaceMenuAccessor; import org.spongepowered.common.item.util.ItemStackUtil; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; public final class RecipePlaceTest { - // Utilities - - private static int CONTAINER_COUNTER = 0; - private static RecipeBookMenu createMenu(final MenuType menuType, final ServerPlayer player) { - return menuType.create(RecipePlaceTest.CONTAINER_COUNTER++, player.getInventory()); + return menuType.create(1, player.getInventory()); } private static String stackToString(final ItemStack stack) { @@ -90,161 +81,31 @@ private static String stackToString(final ItemStack stack) { stack.type().key(RegistryTypes.ITEM_TYPE).value(), stack.quantity(), stack.maxStackQuantity()); } - private static String inventoryToString(final Inventory inventory, final boolean removeEmpty) { - return inventory.slots().stream() - .map(Slot::peek) + private static String stacksToString(final boolean removeEmpty, final List items) { + return items.stream() .filter(stack -> !removeEmpty || !stack.isEmpty()) .map(RecipePlaceTest::stackToString) .collect(Collectors.joining(", ")); } + private static List createExpectedInput(final List items, final int quantity) { + return items.stream() + .map(ItemStack::copy) + .peek(stack -> stack.setQuantity(quantity)) + .toList(); + } + private static ItemStack withQuantity(final ItemStack stack, final int quantity) { final ItemStack copy = stack.copy(); copy.setQuantity(quantity); return copy; } - // Tests - - private static void testRecipe( - final RecipeBookMenu menu, final ServerPlayer player, final TestContext context, - final Function inputInventoryProvider, final Function inputProvider - ) { - final Inventory playerInventory = (Inventory) player.getInventory(); - final List initialInventory = context.inventory(); - playerInventory.clear(); - for (int i = 0; i < initialInventory.size(); ++i) { - playerInventory.set(i, initialInventory.get(i)); - } - - final List initialInput = context.input(); - final Inventory input = (Inventory) inputInventoryProvider.apply(menu); - assertTrue(initialInput.size() <= input.capacity(), - () -> String.format("Initial input size (%s) is greater than actual input size (%s)", - initialInput.size(), input.capacity())); - - input.clear(); - for (int i = 0; i < initialInput.size(); ++i) { - input.set(i, initialInput.get(i)); - } - - final List inputs = new ArrayList<>(); - final List inventories = new ArrayList<>(); - inputs.add(RecipePlaceTest.inventoryToString(input, false)); - inventories.add(RecipePlaceTest.inventoryToString(playerInventory, true)); - for (int i = 0; i < context.clickAmount(); ++i) { - menu.handlePlacement(context.shiftClick(), true, context.recipe(), player.serverLevel(), player.getInventory()); - inputs.add(RecipePlaceTest.inventoryToString(input, false)); - inventories.add(RecipePlaceTest.inventoryToString(playerInventory, true)); - } - - final Supplier history = () -> "Placement history:\n" + - IntStream.range(0, inputs.size()) - .map(click -> inputs.size() - click - 1) - .mapToObj(click -> String.format(""" - - After click %s: - - Input: %s - - Inventory: %s""", - click, inputs.get(click), inventories.get(click))) - .collect(Collectors.joining("\n")); - - if (context.expectedCrafts() != 0) { - assertTrue(((Recipe) context.recipe().value()).matches(inputProvider.apply((T) input), player.level()), - () -> "Recipe does not match\n" + history.get()); - } - - final List quantities = input.slots().stream() - .map(Slot::peek) - .map(ItemStack::quantity) - .distinct() - .toList(); - final int max = quantities.stream().max(Integer::compare).get(); - final int min = quantities.stream().filter(quantity -> quantity != 0).min(Integer::compare).orElse(context.expectedCrafts()); - assertTrue(context.expectedCrafts() == max && min == max, - () -> String.format("Expected %s items in each slot but found %s\n%s", - context.expectedCrafts(), context.expectedCrafts() == max ? min : max, history.get())); - } - - private static void testRecipe( - final MenuType menuType, final ServerPlayer player, final TestContext context, - final Function inputInventoryProvider, final Function inputProvider - ) { - RecipePlaceTest.testRecipe(RecipePlaceTest.createMenu(menuType, player), player, context, inputInventoryProvider, inputProvider); - } - - private static void testCraftingRecipe(final ServerPlayer player, final TestContext context) { - RecipePlaceTest.testRecipe(MenuType.CRAFTING, player, context, - menu -> ((AbstractCraftingMenuAccessor) menu).accessor$craftSlots(), - CraftingContainer::asCraftInput); - } - - private static void testSmeltingRecipe(final ServerPlayer player, final TestContext context) { - RecipePlaceTest.testRecipe(MenuType.FURNACE, player, context, - menu -> ((Inventory) ((AbstractFurnaceMenuAccessor) menu).accessor$container()).slot(0).get(), - slot -> new SingleRecipeInput(ItemStackUtil.toNative(slot.peek()))); - } - - // TestContexts - - private static TestContext context(final String key, final Builder, ?> spongeRecipe) { - return new TestContext(new RecipeHolder<>( - net.minecraft.resources.ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("sponge", key)), - (Recipe) spongeRecipe.build())); - } - - private static Stream populateTests( - final TestContext baseTest, - final int regularExpectedCrafts, - // This exists due to vanilla having a bug with shift-placing that - // pulls item to crafting grid up to exactly ItemType's max stack size - // even if ItemStack's max stack size is less or more than that. - final int shiftExpectedCrafts, - final List partialInventory, final List partialInput, - final List badInventory, final List badInput - ) { - final List totalInitialInventory = Stream.concat(partialInventory.stream(), partialInput.stream()).toList(); - final List baseInputs = List.of( - baseTest.name("Empty input").input(List.of()), - baseTest.name("Bad input").input(badInput) - ); - - final Stream toFail = baseInputs.stream() - .flatMap(context -> Stream.of( - context.name("Empty inventory").inventory(List.of()), - context.name("Bad inventory").inventory(badInventory) - )) - .flatMap(context -> Stream.of(context, context.shift())) - // 2 clicks is enough to ensure we always fail - .flatMap(context -> Stream.of(context, context.clicks(2))) - .map(context -> context.crafts(0)); - - final TestContext regularTest = baseTest.name("Partial input").inventory(partialInventory).input(partialInput); - final Stream toMatchSingleClick = Stream.concat( - baseInputs.stream().map(context -> context.inventory(totalInitialInventory)), - Stream.of(regularTest) - ).flatMap(context -> Stream.of( - context.crafts(1), - context.shift().crafts(shiftExpectedCrafts) - )); - - // If tests above pass, after first click we end up with the same layout no matter the initial input. - // So we can perform multiple-click tests on a single input. - final Stream toMatchMultipleClicks = Stream.of(regularTest) - .flatMap(context -> Stream.concat( - IntStream.rangeClosed(2, regularExpectedCrafts) - .mapToObj(clicks -> context.clicks(clicks).crafts(clicks)), - Stream.of( - context.clicks(regularExpectedCrafts + 1).crafts(regularExpectedCrafts), - context.shift().clicks(2).crafts(shiftExpectedCrafts)) - )); - - return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); - } - private static Stream streamCraftingRecipes() { final ItemStack empty = ItemStack.empty(); final ItemStack bedrock = ItemStack.of(ItemTypes.BEDROCK); - final ItemStack stone64 = ItemStack.of(ItemTypes.STONE, 64); + final ItemStack stone = ItemStack.of(ItemTypes.STONE); + final ItemStack stone64 = RecipePlaceTest.withQuantity(stone, 64); final ItemStack pearl = ItemStack.of(ItemTypes.ENDER_PEARL); final ItemStack pearl16 = RecipePlaceTest.withQuantity(pearl, 16); final ItemStack smallPearl = pearl.copy(); @@ -269,60 +130,87 @@ private static Stream streamCraftingRecipes() { pearl); return Stream.of( - RecipePlaceTest.populateTests( - RecipePlaceTest.context("regular_shaped_crafting", CraftingRecipe.shapedBuilder() - .aisle("S S", " P ") - .where('S', stoneIngredient) - .where('P', anyPearlIngredient) - .result(result)), - 8, 16, - List.of(stone64, bigPearl8, bigPearl8), - List.of( + new TestPopulator("regular_shaped_crafting", CraftingRecipe.shapedBuilder() + .aisle("S S", " P ") + .where('S', stoneIngredient) + .where('P', anyPearlIngredient) + .result(result)) + + .expectCrafts(8, 16) + .expectInput(List.of( + stone, empty, stone, + empty, bigPearl, empty, + empty, empty, empty)) + + .partialInventory(List.of(stone64, bigPearl8, bigPearl8)) + .partialInput(List.of( stone64, empty, empty, - empty, bigPearl4, empty), - Collections.nCopies(9, bedrock), Collections.nCopies(9, bedrock) - ), - - RecipePlaceTest.populateTests( - RecipePlaceTest.context("custom_shaped_crafting", CraftingRecipe.shapedBuilder() - .aisle("SSS", "BBB", "SSS") - .where('S', smallPearlIngredient) - .where('B', bigPearlIngredient) - .result(result)), - 4, 4, - List.of(smallPearl32, bigPearl, bigPearl, smallPearl32, bigPearl, bigPearl, smallPearl32), - List.of( + empty, bigPearl4, empty, + empty, empty, empty)) + + .badInventory(Collections.nCopies(9, bedrock)) + .badInput(Collections.nCopies(9, bedrock)), + + new TestPopulator("custom_shaped_crafting", CraftingRecipe.shapedBuilder() + .aisle("SSS", "BBB", "SSS") + .where('S', smallPearlIngredient) + .where('B', bigPearlIngredient) + .result(result)) + + .expectCrafts(4, 4) + .expectInput(List.of( + smallPearl, smallPearl, smallPearl, + bigPearl, bigPearl, bigPearl, + smallPearl, smallPearl, smallPearl)) + + .partialInventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) + .partialInput(List.of( smallPearl4, empty, smallPearl4, bigPearl4, empty, bigPearl4, - smallPearl, empty, empty - ), - List.of(pearl16), Collections.nCopies(9, pearl) - ), - - RecipePlaceTest.populateTests( - RecipePlaceTest.context("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() - .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient)), - 16, 16, - List.of(smallPearl32), - List.of( - smallPearl4, stone64), - Collections.nCopies(10, bedrock), Collections.nCopies(3, bedrock) - )/*, + smallPearl, empty, empty)) + + .badInventory(List.of(pearl16)) + .badInput(Collections.nCopies(9, pearl)), + + new TestPopulator("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient)) + + .expectCrafts(16, 16) + .expectInput(List.of( + smallPearl, stone, smallPearl, + empty, empty, empty, + empty, empty, empty)) + + .partialInventory(List.of(smallPearl32)) + .partialInput(List.of( + smallPearl4, stone64, empty, + empty, empty, empty, + empty, empty, empty)) + + .badInventory(Collections.nCopies(10, bedrock)) + .badInput(Collections.nCopies(3, bedrock))/*, //TODO uncomment after shapeless recipe fix - RecipePlaceTest.populateTests( - RecipePlaceTest.context("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() - .addIngredients( - smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, - bigPearlIngredient, bigPearlIngredient, bigPearlIngredient)), - 8, 16, - Collections.nCopies(5, bigPearl8), - List.of( + new TestPopulator("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients( + smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, + bigPearlIngredient, bigPearlIngredient, bigPearlIngredient)) + + .expectCrafts(8, 16) + .expectInput(List.of( + smallPearl, smallPearl, smallPearl, + bigPearl, bigPearl, bigPearl, + empty, empty, empty)) + + .partialInventory(Collections.nCopies(5, bigPearl8)) + .partialInput(List.of( smallPearl32, smallPearl32, smallPearl, - bigPearl4, bigPearl4, empty), - List.of(pearl16), Collections.nCopies(6, pearl) - )*/ - ).flatMap(Function.identity()); + bigPearl4, bigPearl4, empty, + empty, empty, empty)) + + .badInventory(List.of(pearl16)) + .badInput(Collections.nCopies(6, pearl))*/ + ).flatMap(TestPopulator::populate); } private static Stream streamSmeltingRecipes() { @@ -334,82 +222,253 @@ private static Stream streamSmeltingRecipes() { final ItemStack result = ItemStack.of(ItemTypes.BARRIER); return Stream.of( - RecipePlaceTest.populateTests( - RecipePlaceTest.context("regular_smelting", CookingRecipe.builder() - .type(RecipeTypes.SMELTING) - .ingredient(Ingredient.of(snowball.type())) - .result(result)), - 4, 8, - Collections.nCopies(8, bigSnowball), List.of(), - List.of(bedrock), List.of(bedrock) - ), - - RecipePlaceTest.populateTests( - RecipePlaceTest.context("custom_smelting", CookingRecipe.builder() - .type(RecipeTypes.SMELTING) - .ingredient(Ingredient.of(ResourceKey.sponge("big_snowball"), - stack -> stack.type() == bigSnowball.type() - && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), - snowball)) - .result(result)), - 4, 8, - Collections.nCopies(8, bigSnowball), List.of(), - List.of(snowball4), List.of(snowball4) - ) - ).flatMap(Function.identity()); + new TestPopulator("regular_smelting", CookingRecipe.builder() + .type(RecipeTypes.SMELTING) + .ingredient(Ingredient.of(snowball.type())) + .result(result)) + + .expectCrafts(4, 8) + .expectInput(List.of(bigSnowball)) + + .partialInventory(Collections.nCopies(8, bigSnowball)) + .badInventory(List.of(bedrock)) + .badInput(List.of(bedrock)), + + new TestPopulator("custom_smelting", CookingRecipe.builder() + .type(RecipeTypes.SMELTING) + .ingredient(Ingredient.of(ResourceKey.sponge("big_snowball"), + stack -> stack.type() == bigSnowball.type() + && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), + snowball)) + .result(result)) + + .expectCrafts(4, 8) + .expectInput(List.of(bigSnowball)) + + .partialInventory(Collections.nCopies(8, bigSnowball)) + .badInventory(List.of(snowball4)) + .badInput(List.of(snowball4)) + ).flatMap(TestPopulator::populate); } @TestFactory public Stream testRecipes() { - final ServerPlayer player = new FakePlayer(SpongeCommon.server().overworld(), new GameProfile(UUID.randomUUID(), "Player")); return Stream.of( RecipePlaceTest.streamCraftingRecipes().map(context -> - dynamicTest(context.asTestName(), () -> RecipePlaceTest.testCraftingRecipe(player, context))), + dynamicTest(context.asTestName(), context::testCrafting)), RecipePlaceTest.streamSmeltingRecipes().map(context -> - dynamicTest(context.asTestName(), () -> RecipePlaceTest.testSmeltingRecipe(player, context))) + dynamicTest(context.asTestName(), context::testSmelting)) ).flatMap(Function.identity()); } + private static final class TestPopulator { + + private final TestContext base; + + private int expectedRegularCrafts; + // This exists due to vanilla having a bug with shift-placing that + // pulls item to crafting grid up to exactly ItemType's max stack size + // even if ItemStack's max stack size is less or more than that. + private int expectedShiftCrafts; + private List expectedInput = List.of(); + + // "Partial" items are used in actual crafting + // All of them should be considered for expected crafts + private List partialInventory = List.of(); + private List partialInput = List.of(); + // "Bad" items exist to ensure placer clears input and doesn't pull wrong items if there is no other choice + private List badInventory = List.of(); + private List badInput = List.of(); + + public TestPopulator(final String key, final Builder, ?> recipe) { + this(new RecipeHolder<>( + net.minecraft.resources.ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("sponge", key)), + (Recipe) recipe.build())); + } + + public TestPopulator(final RecipeHolder recipe) { + this.base = new TestContext(recipe); + } + + public TestPopulator expectCrafts(final int regularCrafts, final int shiftCrafts) { + this.expectedRegularCrafts = regularCrafts; + this.expectedShiftCrafts = shiftCrafts; + return this; + } + + public TestPopulator expectInput(final List items) { + this.expectedInput = items; + return this; + } + + public TestPopulator partialInventory(final List items) { + this.partialInventory = items; + return this; + } + + public TestPopulator partialInput(final List items) { + this.partialInput = items; + return this; + } + + public TestPopulator badInventory(final List items) { + this.badInventory = items; + return this; + } + + public TestPopulator badInput(final List items) { + this.badInput = items; + return this; + } + + public Stream populate() { + final List totalInitialInventory = Stream.concat(this.partialInventory.stream(), this.partialInput.stream()).toList(); + final List expectedShiftInput = RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedShiftCrafts); + final List baseInputs = List.of( + this.base.name("Empty input").input(List.of()), + this.base.name("Bad input").input(this.badInput) + ); + + final Stream toFail = baseInputs.stream() + .flatMap(context -> Stream.of( + context.name("Empty inventory").inventory(List.of()), + context.name("Bad inventory").inventory(this.badInventory) + )) + .flatMap(context -> Stream.of(context, context.shift())) + // 2 clicks is enough to ensure we always fail + .map(context -> context.expectInputs(List.of(List.of(), List.of()))); + + final Stream toMatchSingleClick = baseInputs.stream() + .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) + .flatMap(context -> Stream.of( + context.expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), + context.shift().expectInput(expectedShiftInput) + )); + + // After first click we end up with the same layout no matter the initial input. + // So we can perform multiple-click tests on a single input. + final Stream toMatchMultipleClicks = Stream.of(this.base) + .map(context -> context + .name("Partial input").input(this.partialInput) + .name("Partial inventory").inventory(this.partialInventory)) + .flatMap(context -> Stream.of( + context + .expectInputs(IntStream.rangeClosed(1, this.expectedRegularCrafts) + .mapToObj(clicks -> RecipePlaceTest.createExpectedInput(this.expectedInput, clicks)) + .toList()) + .expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedRegularCrafts)), + context.shift().expectInputs(List.of(expectedShiftInput, expectedShiftInput)) + )); + + return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); + } + } + private record TestContext( - RecipeHolder recipe, String testName, int expectedCrafts, boolean shiftClick, int clickAmount, - List inventory, List input + RecipeHolder recipe, String testName, boolean shiftClick, + List inventory, List input, + List> expectedInputs ) { public TestContext(final RecipeHolder recipe) { - this(recipe, "", 1, false, 1, List.of(), List.of()); + this(recipe, "", false, List.of(), List.of(), List.of()); } public TestContext name(final String testName) { final String newTestName = this.testName.isEmpty() ? testName : (this.testName + ", " + testName); - return new TestContext(this.recipe, newTestName, this.expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, this.input); + return new TestContext(this.recipe, newTestName, this.shiftClick, this.inventory, this.input, this.expectedInputs); } - public TestContext crafts(final int expectedCrafts) { - return new TestContext(this.recipe, this.testName, expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, this.input); + public TestContext shift() { + return new TestContext(this.recipe, this.testName, true, this.inventory, this.input, this.expectedInputs); } - public TestContext shift() { - return new TestContext(this.recipe, this.testName, this.expectedCrafts, true, this.clickAmount, this.inventory, this.input); + public TestContext inventory(final List items) { + return new TestContext(this.recipe, this.testName, this.shiftClick, items, this.input, this.expectedInputs); } - public TestContext clicks(final int clickAmount) { - return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, clickAmount, this.inventory, this.input); + public TestContext input(final List items) { + return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, items, this.expectedInputs); } - public TestContext inventory(final List items) { - return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, this.clickAmount, items, this.input); + public TestContext expectInputs(final List> expectedInputs) { + return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, this.input, expectedInputs); } - public TestContext input(final List items) { - return new TestContext(this.recipe, this.testName, this.expectedCrafts, this.shiftClick, this.clickAmount, this.inventory, items); + public TestContext expectInput(final List expectedInput) { + return this.expectInputs(Stream.concat(this.expectedInputs.stream(), Stream.of(expectedInput)).toList()); } public String asTestName() { - return String.format("%s recipe (crafts: %s, shift: %s, clicks: %s, %s)", + return String.format("Place recipe %s (Shift click: %s, Total clicks: %s, %s)", this.recipe.id().location().getPath(), - this.expectedCrafts, this.shiftClick, - this.clickAmount, - this.testName.isEmpty() ? "Regular" : this.testName); + this.expectedInputs.size(), + this.testName); + } + + // Tests + + public void testCrafting() { + this.test(MenuType.CRAFTING, menu -> (Inventory) ((AbstractCraftingMenuAccessor) menu).accessor$craftSlots()); + } + + public void testSmelting() { + this.test(MenuType.FURNACE, menu -> ((Inventory) ((AbstractFurnaceMenuAccessor) menu).accessor$container()).slot(0).get()); + } + + public void test( + final MenuType menuType, + final Function inputProvider + ) { + this.test(player -> RecipePlaceTest.createMenu(menuType, player), inputProvider); + } + + public void test( + final Function menuProvider, + final Function inputProvider + ) { + final ServerPlayer player = new FakePlayer(SpongeCommon.server().overworld(), new GameProfile(UUID.randomUUID(), "Player")); + final RecipeBookMenu menu = menuProvider.apply(player); + + final Inventory playerInventory = (Inventory) player.getInventory(); + final List initialInventory = this.inventory(); + for (int i = 0; i < initialInventory.size(); ++i) { + playerInventory.set(i, initialInventory.get(i)); + } + + final Inventory input = inputProvider.apply(menu); + final List initialInput = this.input(); + for (int i = 0; i < initialInput.size(); ++i) { + input.set(i, initialInput.get(i)); + } + + for (int i = 0; i < this.expectedInputs().size(); ++i) { + menu.handlePlacement(this.shiftClick(), true, this.recipe(), player.serverLevel(), player.getInventory()); + + final List actualInput = input.slots().stream().map(Slot::peek).toList(); + final List expectedInput = this.expectedInputs().get(i); + final int click = i+1; + for (int j = 0; j < actualInput.size(); ++j) { + final ItemStack actualStack = actualInput.get(j); + final ItemStack expectedStack = expectedInput.size() <= j ? ItemStack.empty() : expectedInput.get(j); + assertTrue(net.minecraft.world.item.ItemStack.matches( + ItemStackUtil.toNative(actualStack), ItemStackUtil.toNative(expectedStack)), + () -> String.format(""" + Actual input doesn't match expected input after click %s + Test: %s, + Expected input: %s + Actual input: %s + Initial input: %s + Initial inventory: %s""", + click, + this.asTestName(), + RecipePlaceTest.stacksToString(false, expectedInput), + RecipePlaceTest.stacksToString(false, actualInput), + RecipePlaceTest.stacksToString(false, initialInput), + RecipePlaceTest.stacksToString(true, initialInventory) + )); + } + } } } From 1a94490baedb49156b17a001903b01c83327bfbb Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 31 Oct 2025 04:20:25 +0300 Subject: [PATCH 10/20] make tests more flexible --- .../common/recipe/RecipePlaceTest.java | 111 +++++++++++++----- 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index acb61b43db1..cc55ac521ed 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -263,7 +263,7 @@ public Stream testRecipes() { private static final class TestPopulator { - private final TestContext base; + private TestContext base; private int expectedRegularCrafts; // This exists due to vanilla having a bug with shift-placing that @@ -290,6 +290,11 @@ public TestPopulator(final RecipeHolder recipe) { this.base = new TestContext(recipe); } + public TestPopulator test(final TestEntry test) { + this.base = this.base.test(test); + return this; + } + public TestPopulator expectCrafts(final int regularCrafts, final int shiftCrafts) { this.expectedRegularCrafts = regularCrafts; this.expectedShiftCrafts = shiftCrafts; @@ -336,13 +341,13 @@ public Stream populate() { )) .flatMap(context -> Stream.of(context, context.shift())) // 2 clicks is enough to ensure we always fail - .map(context -> context.expectInputs(List.of(List.of(), List.of()))); + .map(context -> context.clicks(2).expectInputs(List.of(List.of(), List.of()))); final Stream toMatchSingleClick = baseInputs.stream() .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) .flatMap(context -> Stream.of( - context.expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), - context.shift().expectInput(expectedShiftInput) + context.expectInputAt(0, RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), + context.shift().expectInputAt(0, expectedShiftInput) )); // After first click we end up with the same layout no matter the initial input. @@ -353,11 +358,13 @@ public Stream populate() { .name("Partial inventory").inventory(this.partialInventory)) .flatMap(context -> Stream.of( context - .expectInputs(IntStream.rangeClosed(1, this.expectedRegularCrafts) - .mapToObj(clicks -> RecipePlaceTest.createExpectedInput(this.expectedInput, clicks)) - .toList()) - .expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedRegularCrafts)), - context.shift().expectInputs(List.of(expectedShiftInput, expectedShiftInput)) + .clicks(this.expectedRegularCrafts+1) + .expectInputs(Stream.concat( + IntStream.rangeClosed(1, this.expectedRegularCrafts) + .mapToObj(clicks -> RecipePlaceTest.createExpectedInput(this.expectedInput, clicks)), + Stream.of(RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedRegularCrafts)) + ).toList()), + context.shift().clicks(2).expectInputs(List.of(expectedShiftInput, expectedShiftInput)) )); return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); @@ -365,44 +372,68 @@ public Stream populate() { } private record TestContext( - RecipeHolder recipe, String testName, boolean shiftClick, + RecipeHolder recipe, String testName, + boolean shiftClick, int clicks, List inventory, List input, - List> expectedInputs + List tests ) { public TestContext(final RecipeHolder recipe) { - this(recipe, "", false, List.of(), List.of(), List.of()); + this(recipe, "", false, 1, List.of(), List.of(), List.of()); } public TestContext name(final String testName) { final String newTestName = this.testName.isEmpty() ? testName : (this.testName + ", " + testName); - return new TestContext(this.recipe, newTestName, this.shiftClick, this.inventory, this.input, this.expectedInputs); + return new TestContext(this.recipe, newTestName, this.shiftClick, this.clicks, this.inventory, this.input, this.tests); } public TestContext shift() { - return new TestContext(this.recipe, this.testName, true, this.inventory, this.input, this.expectedInputs); + return new TestContext(this.recipe, this.testName, true, this.clicks, this.inventory, this.input, this.tests); + } + + public TestContext clicks(final int clicks) { + return new TestContext(this.recipe, this.testName, this.shiftClick, clicks, this.inventory, this.input, this.tests); } public TestContext inventory(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, items, this.input, this.expectedInputs); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, items, this.input, this.tests); } public TestContext input(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, items, this.expectedInputs); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, this.inventory, items, this.tests); } - public TestContext expectInputs(final List> expectedInputs) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, this.input, expectedInputs); + public TestContext test(final TestEntry test) { + final List newTests = Stream.concat(this.tests.stream(), Stream.of(test)).toList(); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, this.inventory, this.input, newTests); + } + + public TestContext testAt(final int clickToTest, final TestEntry test) { + return this.test((context, input, click) -> { + if (click != clickToTest) { + return true; + } + + return test.test(context, input, click); + }); + } + + public TestContext expectInputAt(final int clickToTest, final List expectedInput) { + return this.testAt(clickToTest, TestEntry.matchInput(expectedInput)); } - public TestContext expectInput(final List expectedInput) { - return this.expectInputs(Stream.concat(this.expectedInputs.stream(), Stream.of(expectedInput)).toList()); + public TestContext expectInputs(final List> expectedInputs) { + TestContext test = this; + for (int i = 0; i < expectedInputs.size(); ++i) { + test = test.expectInputAt(i, expectedInputs.get(i)); + } + return test; } public String asTestName() { return String.format("Place recipe %s (Shift click: %s, Total clicks: %s, %s)", this.recipe.id().location().getPath(), this.shiftClick, - this.expectedInputs.size(), + this.clicks, this.testName); } @@ -442,34 +473,54 @@ public void test( input.set(i, initialInput.get(i)); } - for (int i = 0; i < this.expectedInputs().size(); ++i) { + for (int i = 0; i < this.clicks(); ++i) { menu.handlePlacement(this.shiftClick(), true, this.recipe(), player.serverLevel(), player.getInventory()); + for (final TestEntry test : this.tests()) { + if (!test.test(this, input, i)) { + break; + } + } + } + } + } + + @FunctionalInterface + private interface TestEntry { + static TestEntry matchInput(final List expectedInput) { + return (context, input, click) -> { final List actualInput = input.slots().stream().map(Slot::peek).toList(); - final List expectedInput = this.expectedInputs().get(i); - final int click = i+1; for (int j = 0; j < actualInput.size(); ++j) { final ItemStack actualStack = actualInput.get(j); final ItemStack expectedStack = expectedInput.size() <= j ? ItemStack.empty() : expectedInput.get(j); assertTrue(net.minecraft.world.item.ItemStack.matches( ItemStackUtil.toNative(actualStack), ItemStackUtil.toNative(expectedStack)), () -> String.format(""" - Actual input doesn't match expected input after click %s + Actual input doesn't match expected input after click â„–%s Test: %s, Expected input: %s Actual input: %s Initial input: %s Initial inventory: %s""", - click, - this.asTestName(), + click+1, + context.asTestName(), RecipePlaceTest.stacksToString(false, expectedInput), RecipePlaceTest.stacksToString(false, actualInput), - RecipePlaceTest.stacksToString(false, initialInput), - RecipePlaceTest.stacksToString(true, initialInventory) + RecipePlaceTest.stacksToString(false, context.input()), + RecipePlaceTest.stacksToString(true, context.inventory()) )); } - } + + return true; + }; } + + /** + * Performs the test on recipe input after each placement. Clicks start at 0. + * + * @return True if the following tests should be performed within the given click + */ + boolean test(TestContext context, Inventory input, int click); } private static final class FakePlayer extends ServerPlayer { From 3b01885432712cc62de8f210e6727d558f28bc3d Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 31 Oct 2025 19:24:32 +0300 Subject: [PATCH 11/20] Revert "make tests more flexible" This reverts commit 1a94490baedb49156b17a001903b01c83327bfbb. --- .../common/recipe/RecipePlaceTest.java | 111 +++++------------- 1 file changed, 30 insertions(+), 81 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index cc55ac521ed..acb61b43db1 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -263,7 +263,7 @@ public Stream testRecipes() { private static final class TestPopulator { - private TestContext base; + private final TestContext base; private int expectedRegularCrafts; // This exists due to vanilla having a bug with shift-placing that @@ -290,11 +290,6 @@ public TestPopulator(final RecipeHolder recipe) { this.base = new TestContext(recipe); } - public TestPopulator test(final TestEntry test) { - this.base = this.base.test(test); - return this; - } - public TestPopulator expectCrafts(final int regularCrafts, final int shiftCrafts) { this.expectedRegularCrafts = regularCrafts; this.expectedShiftCrafts = shiftCrafts; @@ -341,13 +336,13 @@ public Stream populate() { )) .flatMap(context -> Stream.of(context, context.shift())) // 2 clicks is enough to ensure we always fail - .map(context -> context.clicks(2).expectInputs(List.of(List.of(), List.of()))); + .map(context -> context.expectInputs(List.of(List.of(), List.of()))); final Stream toMatchSingleClick = baseInputs.stream() .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) .flatMap(context -> Stream.of( - context.expectInputAt(0, RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), - context.shift().expectInputAt(0, expectedShiftInput) + context.expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), + context.shift().expectInput(expectedShiftInput) )); // After first click we end up with the same layout no matter the initial input. @@ -358,13 +353,11 @@ public Stream populate() { .name("Partial inventory").inventory(this.partialInventory)) .flatMap(context -> Stream.of( context - .clicks(this.expectedRegularCrafts+1) - .expectInputs(Stream.concat( - IntStream.rangeClosed(1, this.expectedRegularCrafts) - .mapToObj(clicks -> RecipePlaceTest.createExpectedInput(this.expectedInput, clicks)), - Stream.of(RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedRegularCrafts)) - ).toList()), - context.shift().clicks(2).expectInputs(List.of(expectedShiftInput, expectedShiftInput)) + .expectInputs(IntStream.rangeClosed(1, this.expectedRegularCrafts) + .mapToObj(clicks -> RecipePlaceTest.createExpectedInput(this.expectedInput, clicks)) + .toList()) + .expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedRegularCrafts)), + context.shift().expectInputs(List.of(expectedShiftInput, expectedShiftInput)) )); return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); @@ -372,68 +365,44 @@ public Stream populate() { } private record TestContext( - RecipeHolder recipe, String testName, - boolean shiftClick, int clicks, + RecipeHolder recipe, String testName, boolean shiftClick, List inventory, List input, - List tests + List> expectedInputs ) { public TestContext(final RecipeHolder recipe) { - this(recipe, "", false, 1, List.of(), List.of(), List.of()); + this(recipe, "", false, List.of(), List.of(), List.of()); } public TestContext name(final String testName) { final String newTestName = this.testName.isEmpty() ? testName : (this.testName + ", " + testName); - return new TestContext(this.recipe, newTestName, this.shiftClick, this.clicks, this.inventory, this.input, this.tests); + return new TestContext(this.recipe, newTestName, this.shiftClick, this.inventory, this.input, this.expectedInputs); } public TestContext shift() { - return new TestContext(this.recipe, this.testName, true, this.clicks, this.inventory, this.input, this.tests); - } - - public TestContext clicks(final int clicks) { - return new TestContext(this.recipe, this.testName, this.shiftClick, clicks, this.inventory, this.input, this.tests); + return new TestContext(this.recipe, this.testName, true, this.inventory, this.input, this.expectedInputs); } public TestContext inventory(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, items, this.input, this.tests); + return new TestContext(this.recipe, this.testName, this.shiftClick, items, this.input, this.expectedInputs); } public TestContext input(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, this.inventory, items, this.tests); - } - - public TestContext test(final TestEntry test) { - final List newTests = Stream.concat(this.tests.stream(), Stream.of(test)).toList(); - return new TestContext(this.recipe, this.testName, this.shiftClick, this.clicks, this.inventory, this.input, newTests); - } - - public TestContext testAt(final int clickToTest, final TestEntry test) { - return this.test((context, input, click) -> { - if (click != clickToTest) { - return true; - } - - return test.test(context, input, click); - }); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, items, this.expectedInputs); } - public TestContext expectInputAt(final int clickToTest, final List expectedInput) { - return this.testAt(clickToTest, TestEntry.matchInput(expectedInput)); + public TestContext expectInputs(final List> expectedInputs) { + return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, this.input, expectedInputs); } - public TestContext expectInputs(final List> expectedInputs) { - TestContext test = this; - for (int i = 0; i < expectedInputs.size(); ++i) { - test = test.expectInputAt(i, expectedInputs.get(i)); - } - return test; + public TestContext expectInput(final List expectedInput) { + return this.expectInputs(Stream.concat(this.expectedInputs.stream(), Stream.of(expectedInput)).toList()); } public String asTestName() { return String.format("Place recipe %s (Shift click: %s, Total clicks: %s, %s)", this.recipe.id().location().getPath(), this.shiftClick, - this.clicks, + this.expectedInputs.size(), this.testName); } @@ -473,54 +442,34 @@ public void test( input.set(i, initialInput.get(i)); } - for (int i = 0; i < this.clicks(); ++i) { + for (int i = 0; i < this.expectedInputs().size(); ++i) { menu.handlePlacement(this.shiftClick(), true, this.recipe(), player.serverLevel(), player.getInventory()); - for (final TestEntry test : this.tests()) { - if (!test.test(this, input, i)) { - break; - } - } - } - } - } - - @FunctionalInterface - private interface TestEntry { - static TestEntry matchInput(final List expectedInput) { - return (context, input, click) -> { final List actualInput = input.slots().stream().map(Slot::peek).toList(); + final List expectedInput = this.expectedInputs().get(i); + final int click = i+1; for (int j = 0; j < actualInput.size(); ++j) { final ItemStack actualStack = actualInput.get(j); final ItemStack expectedStack = expectedInput.size() <= j ? ItemStack.empty() : expectedInput.get(j); assertTrue(net.minecraft.world.item.ItemStack.matches( ItemStackUtil.toNative(actualStack), ItemStackUtil.toNative(expectedStack)), () -> String.format(""" - Actual input doesn't match expected input after click â„–%s + Actual input doesn't match expected input after click %s Test: %s, Expected input: %s Actual input: %s Initial input: %s Initial inventory: %s""", - click+1, - context.asTestName(), + click, + this.asTestName(), RecipePlaceTest.stacksToString(false, expectedInput), RecipePlaceTest.stacksToString(false, actualInput), - RecipePlaceTest.stacksToString(false, context.input()), - RecipePlaceTest.stacksToString(true, context.inventory()) + RecipePlaceTest.stacksToString(false, initialInput), + RecipePlaceTest.stacksToString(true, initialInventory) )); } - - return true; - }; + } } - - /** - * Performs the test on recipe input after each placement. Clicks start at 0. - * - * @return True if the following tests should be performed within the given click - */ - boolean test(TestContext context, Inventory input, int click); } private static final class FakePlayer extends ServerPlayer { From 6fb1659e9a7e780aa8892249f13fc5d8c824e31a Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 31 Oct 2025 21:17:22 +0300 Subject: [PATCH 12/20] test option for creative mode, tests for full inventory --- .../common/recipe/RecipePlaceTest.java | 145 ++++++++++++------ 1 file changed, 99 insertions(+), 46 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index acb61b43db1..1d9e0f1b195 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -60,6 +60,7 @@ import org.spongepowered.common.accessor.world.inventory.AbstractFurnaceMenuAccessor; import org.spongepowered.common.item.util.ItemStackUtil; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -129,12 +130,12 @@ private static Stream streamCraftingRecipes() { && stack.maxStackQuantity() == bigPearl.maxStackQuantity(), pearl); - return Stream.of( - new TestPopulator("regular_shaped_crafting", CraftingRecipe.shapedBuilder() + return Stream.of( + new DefaultedTestPopulator(new TestContext("regular_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("S S", " P ") .where('S', stoneIngredient) .where('P', anyPearlIngredient) - .result(result)) + .result(result))) .expectCrafts(8, 16) .expectInput(List.of( @@ -151,11 +152,11 @@ private static Stream streamCraftingRecipes() { .badInventory(Collections.nCopies(9, bedrock)) .badInput(Collections.nCopies(9, bedrock)), - new TestPopulator("custom_shaped_crafting", CraftingRecipe.shapedBuilder() + new DefaultedTestPopulator(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("SSS", "BBB", "SSS") .where('S', smallPearlIngredient) .where('B', bigPearlIngredient) - .result(result)) + .result(result))) .expectCrafts(4, 4) .expectInput(List.of( @@ -172,8 +173,8 @@ private static Stream streamCraftingRecipes() { .badInventory(List.of(pearl16)) .badInput(Collections.nCopies(9, pearl)), - new TestPopulator("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() - .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient)) + new DefaultedTestPopulator(new TestContext("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient))) .expectCrafts(16, 16) .expectInput(List.of( @@ -191,10 +192,10 @@ private static Stream streamCraftingRecipes() { .badInput(Collections.nCopies(3, bedrock))/*, //TODO uncomment after shapeless recipe fix - new TestPopulator("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() + new DefaultedTestPopulator(new TestContext("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, - bigPearlIngredient, bigPearlIngredient, bigPearlIngredient)) + bigPearlIngredient, bigPearlIngredient, bigPearlIngredient))) .expectCrafts(8, 16) .expectInput(List.of( @@ -221,11 +222,11 @@ private static Stream streamSmeltingRecipes() { bigSnowball.offer(Keys.MAX_STACK_SIZE, 4); final ItemStack result = ItemStack.of(ItemTypes.BARRIER); - return Stream.of( - new TestPopulator("regular_smelting", CookingRecipe.builder() + return Stream.of( + new DefaultedTestPopulator(new TestContext("regular_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(snowball.type())) - .result(result)) + .result(result))) .expectCrafts(4, 8) .expectInput(List.of(bigSnowball)) @@ -234,13 +235,13 @@ private static Stream streamSmeltingRecipes() { .badInventory(List.of(bedrock)) .badInput(List.of(bedrock)), - new TestPopulator("custom_smelting", CookingRecipe.builder() + new DefaultedTestPopulator(new TestContext("custom_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(ResourceKey.sponge("big_snowball"), stack -> stack.type() == bigSnowball.type() && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), snowball)) - .result(result)) + .result(result))) .expectCrafts(4, 8) .expectInput(List.of(bigSnowball)) @@ -252,7 +253,7 @@ private static Stream streamSmeltingRecipes() { } @TestFactory - public Stream testRecipes() { + public Stream generateTests() { return Stream.of( RecipePlaceTest.streamCraftingRecipes().map(context -> dynamicTest(context.asTestName(), context::testCrafting)), @@ -261,7 +262,38 @@ public Stream testRecipes() { ).flatMap(Function.identity()); } - private static final class TestPopulator { + @FunctionalInterface + private interface TestPopulator { + + Stream populate(); + } + + private static final class SimpleTestPopulator implements TestPopulator { + + private final TestContext base; + private final List tests = new ArrayList<>(); + + public SimpleTestPopulator(final TestContext base) { + this.base = base; + } + + public SimpleTestPopulator add(final Function testProvider) { + this.tests.add(testProvider.apply(this.base)); + return this; + } + + public SimpleTestPopulator addAll(final Function> testProvider) { + testProvider.apply(this.base).forEach(this.tests::add); + return this; + } + + @Override + public Stream populate() { + return this.tests.stream(); + } + } + + private static final class DefaultedTestPopulator implements TestPopulator { private final TestContext base; @@ -280,47 +312,42 @@ private static final class TestPopulator { private List badInventory = List.of(); private List badInput = List.of(); - public TestPopulator(final String key, final Builder, ?> recipe) { - this(new RecipeHolder<>( - net.minecraft.resources.ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("sponge", key)), - (Recipe) recipe.build())); - } - - public TestPopulator(final RecipeHolder recipe) { - this.base = new TestContext(recipe); + public DefaultedTestPopulator(final TestContext base) { + this.base = base; } - public TestPopulator expectCrafts(final int regularCrafts, final int shiftCrafts) { + public DefaultedTestPopulator expectCrafts(final int regularCrafts, final int shiftCrafts) { this.expectedRegularCrafts = regularCrafts; this.expectedShiftCrafts = shiftCrafts; return this; } - public TestPopulator expectInput(final List items) { + public DefaultedTestPopulator expectInput(final List items) { this.expectedInput = items; return this; } - public TestPopulator partialInventory(final List items) { + public DefaultedTestPopulator partialInventory(final List items) { this.partialInventory = items; return this; } - public TestPopulator partialInput(final List items) { + public DefaultedTestPopulator partialInput(final List items) { this.partialInput = items; return this; } - public TestPopulator badInventory(final List items) { + public DefaultedTestPopulator badInventory(final List items) { this.badInventory = items; return this; } - public TestPopulator badInput(final List items) { + public DefaultedTestPopulator badInput(final List items) { this.badInput = items; return this; } + @Override public Stream populate() { final List totalInitialInventory = Stream.concat(this.partialInventory.stream(), this.partialInput.stream()).toList(); final List expectedShiftInput = RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedShiftCrafts); @@ -329,17 +356,19 @@ public Stream populate() { this.base.name("Bad input").input(this.badInput) ); - final Stream toFail = baseInputs.stream() + final Stream testBadLayouts = baseInputs.stream() .flatMap(context -> Stream.of( context.name("Empty inventory").inventory(List.of()), context.name("Bad inventory").inventory(this.badInventory) )) .flatMap(context -> Stream.of(context, context.shift())) - // 2 clicks is enough to ensure we always fail + .flatMap(context -> Stream.of(context, context.creative())) + // 2 clicks is enough to ensure we always get empty input .map(context -> context.expectInputs(List.of(List.of(), List.of()))); - final Stream toMatchSingleClick = baseInputs.stream() + final Stream testSingleClick = baseInputs.stream() .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) + .flatMap(context -> Stream.of(context, context.creative())) .flatMap(context -> Stream.of( context.expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), context.shift().expectInput(expectedShiftInput) @@ -347,10 +376,11 @@ public Stream populate() { // After first click we end up with the same layout no matter the initial input. // So we can perform multiple-click tests on a single input. - final Stream toMatchMultipleClicks = Stream.of(this.base) + final Stream testMultipleClicks = Stream.of(this.base) .map(context -> context .name("Partial input").input(this.partialInput) .name("Partial inventory").inventory(this.partialInventory)) + .flatMap(context -> Stream.of(context, context.creative())) .flatMap(context -> Stream.of( context .expectInputs(IntStream.rangeClosed(1, this.expectedRegularCrafts) @@ -360,38 +390,60 @@ public Stream populate() { context.shift().expectInputs(List.of(expectedShiftInput, expectedShiftInput)) )); - return Stream.concat(toFail, Stream.concat(toMatchSingleClick, toMatchMultipleClicks)); + final Stream testFullInventory = Stream.of(this.base) + .map(context -> context + .name("Partial input").input(this.partialInput) + .name("Full inventory").inventory(Collections.nCopies(36, ItemStack.of(ItemTypes.BARRIER, 64)))) + .flatMap(context -> Stream.of(context, context.shift())) + .flatMap(context -> Stream.of( + context.expectInput(this.partialInput), + context.creative().expectInput(List.of()) + )); + + return Stream.of(testBadLayouts, testSingleClick, testMultipleClicks, testFullInventory) + .flatMap(Function.identity()); } } private record TestContext( - RecipeHolder recipe, String testName, boolean shiftClick, + RecipeHolder recipe, String testName, + boolean shiftClick, boolean creativeMode, List inventory, List input, List> expectedInputs ) { + public TestContext(final String key, final Builder, ?> recipe) { + this(new RecipeHolder<>( + net.minecraft.resources.ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath("sponge", key)), + (Recipe) recipe.build())); + } + public TestContext(final RecipeHolder recipe) { - this(recipe, "", false, List.of(), List.of(), List.of()); + this(recipe, "", false, false, List.of(), List.of(), List.of()); } public TestContext name(final String testName) { final String newTestName = this.testName.isEmpty() ? testName : (this.testName + ", " + testName); - return new TestContext(this.recipe, newTestName, this.shiftClick, this.inventory, this.input, this.expectedInputs); + return new TestContext(this.recipe, newTestName, this.shiftClick, this.creativeMode, this.inventory, this.input, this.expectedInputs); } public TestContext shift() { - return new TestContext(this.recipe, this.testName, true, this.inventory, this.input, this.expectedInputs); + return new TestContext(this.recipe, this.testName, true, this.creativeMode, this.inventory, this.input, this.expectedInputs); + } + + public TestContext creative() { + return new TestContext(this.recipe, this.testName, this.shiftClick, true, this.inventory, this.input, this.expectedInputs); } public TestContext inventory(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, items, this.input, this.expectedInputs); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.creativeMode, items, this.input, this.expectedInputs); } public TestContext input(final List items) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, items, this.expectedInputs); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.creativeMode, this.inventory, items, this.expectedInputs); } public TestContext expectInputs(final List> expectedInputs) { - return new TestContext(this.recipe, this.testName, this.shiftClick, this.inventory, this.input, expectedInputs); + return new TestContext(this.recipe, this.testName, this.shiftClick, this.creativeMode, this.inventory, this.input, expectedInputs); } public TestContext expectInput(final List expectedInput) { @@ -399,11 +451,12 @@ public TestContext expectInput(final List expectedInput) { } public String asTestName() { - return String.format("Place recipe %s (Shift click: %s, Total clicks: %s, %s)", + return String.format("Place recipe %s, %s (Shift click: %s, Creative mode: %s, Total clicks: %s)", this.recipe.id().location().getPath(), + this.testName, this.shiftClick, - this.expectedInputs.size(), - this.testName); + this.creativeMode, + this.expectedInputs.size()); } // Tests @@ -443,7 +496,7 @@ public void test( } for (int i = 0; i < this.expectedInputs().size(); ++i) { - menu.handlePlacement(this.shiftClick(), true, this.recipe(), player.serverLevel(), player.getInventory()); + menu.handlePlacement(this.shiftClick(), this.creativeMode(), this.recipe(), player.serverLevel(), player.getInventory()); final List actualInput = input.slots().stream().map(Slot::peek).toList(); final List expectedInput = this.expectedInputs().get(i); From a0bb7ae984fd25cdeb354b3cbe4ced875237d3b9 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 2 Nov 2025 20:26:44 +0300 Subject: [PATCH 13/20] move bad layout tests to its own populator --- .../common/recipe/RecipePlaceTest.java | 266 +++++++++++------- 1 file changed, 158 insertions(+), 108 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index 1d9e0f1b195..e1a92125010 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -130,88 +130,104 @@ private static Stream streamCraftingRecipes() { && stack.maxStackQuantity() == bigPearl.maxStackQuantity(), pearl); - return Stream.of( - new DefaultedTestPopulator(new TestContext("regular_shaped_crafting", CraftingRecipe.shapedBuilder() + return Stream.of( + Stream.of(new TestContext("regular_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("S S", " P ") .where('S', stoneIngredient) .where('P', anyPearlIngredient) .result(result))) - - .expectCrafts(8, 16) - .expectInput(List.of( - stone, empty, stone, - empty, bigPearl, empty, - empty, empty, empty)) - - .partialInventory(List.of(stone64, bigPearl8, bigPearl8)) - .partialInput(List.of( - stone64, empty, empty, - empty, bigPearl4, empty, - empty, empty, empty)) - - .badInventory(Collections.nCopies(9, bedrock)) - .badInput(Collections.nCopies(9, bedrock)), - - new DefaultedTestPopulator(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(8, 16) + .expectInput(List.of( + stone, empty, stone, + empty, bigPearl, empty, + empty, empty, empty)) + + .partialInventory(List.of(stone64, bigPearl8, bigPearl8)) + .partialInput(List.of( + stone64, empty, empty, + empty, bigPearl4, empty, + empty, empty, empty)) + .badInput(Collections.nCopies(9, bedrock)), + new BadLayoutTestPopulator(test) + .badInventory(Collections.nCopies(9, bedrock)) + .badInput(Collections.nCopies(9, bedrock)) + )).flatMap(TestPopulator::populate), + + Stream.of(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("SSS", "BBB", "SSS") .where('S', smallPearlIngredient) .where('B', bigPearlIngredient) .result(result))) + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(4, 4) + .expectInput(List.of( + smallPearl, smallPearl, smallPearl, + bigPearl, bigPearl, bigPearl, + smallPearl, smallPearl, smallPearl)) + + .partialInventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) + .partialInput(List.of( + smallPearl4, empty, smallPearl4, + bigPearl4, empty, bigPearl4, + smallPearl, empty, empty)) + .badInput(Collections.nCopies(9, pearl)), + new BadLayoutTestPopulator(test) + .badInventory(List.of(pearl16)) + .badInput(Collections.nCopies(9, pearl)) + )) + .flatMap(TestPopulator::populate), - .expectCrafts(4, 4) - .expectInput(List.of( - smallPearl, smallPearl, smallPearl, - bigPearl, bigPearl, bigPearl, - smallPearl, smallPearl, smallPearl)) - - .partialInventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) - .partialInput(List.of( - smallPearl4, empty, smallPearl4, - bigPearl4, empty, bigPearl4, - smallPearl, empty, empty)) - - .badInventory(List.of(pearl16)) - .badInput(Collections.nCopies(9, pearl)), - - new DefaultedTestPopulator(new TestContext("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() - .addIngredients(anyPearlIngredient, stoneIngredient, anyPearlIngredient))) - - .expectCrafts(16, 16) - .expectInput(List.of( - smallPearl, stone, smallPearl, - empty, empty, empty, - empty, empty, empty)) - - .partialInventory(List.of(smallPearl32)) - .partialInput(List.of( - smallPearl4, stone64, empty, - empty, empty, empty, - empty, empty, empty)) - - .badInventory(Collections.nCopies(10, bedrock)) - .badInput(Collections.nCopies(3, bedrock))/*, + Stream.of(new TestContext("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() + .addIngredients( + anyPearlIngredient, stoneIngredient, anyPearlIngredient))) + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(16, 16) + .expectInput(List.of( + smallPearl, stone, smallPearl, + empty, empty, empty, + empty, empty, empty)) + + .partialInventory(List.of(smallPearl32)) + .partialInput(List.of( + smallPearl4, stone64, empty, + empty, empty, empty, + empty, empty, empty)) + .badInput(Collections.nCopies(3, bedrock)), + new BadLayoutTestPopulator(test) + .badInventory(Collections.nCopies(10, bedrock)) + .badInput(Collections.nCopies(3, bedrock)) + )) + .flatMap(TestPopulator::populate)/*, //TODO uncomment after shapeless recipe fix - new DefaultedTestPopulator(new TestContext("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() + Stream.of(new TestContext("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, bigPearlIngredient, bigPearlIngredient, bigPearlIngredient))) - - .expectCrafts(8, 16) - .expectInput(List.of( - smallPearl, smallPearl, smallPearl, - bigPearl, bigPearl, bigPearl, - empty, empty, empty)) - - .partialInventory(Collections.nCopies(5, bigPearl8)) - .partialInput(List.of( - smallPearl32, smallPearl32, smallPearl, - bigPearl4, bigPearl4, empty, - empty, empty, empty)) - - .badInventory(List.of(pearl16)) - .badInput(Collections.nCopies(6, pearl))*/ - ).flatMap(TestPopulator::populate); + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(8, 16) + .expectInput(List.of( + smallPearl, smallPearl, smallPearl, + bigPearl, bigPearl, bigPearl, + empty, empty, empty)) + + .partialInventory(Collections.nCopies(5, bigPearl8)) + .partialInput(List.of( + smallPearl32, smallPearl32, smallPearl, + bigPearl4, bigPearl4, empty, + empty, empty, empty)) + .badInput(Collections.nCopies(6, pearl)), + new BadLayoutTestPopulator(test) + .badInventory(List.of(pearl16)) + .badInput(Collections.nCopies(6, pearl)) + )) + .flatMap(TestPopulator::populate)*/ + ).flatMap(Function.identity()); } private static Stream streamSmeltingRecipes() { @@ -222,34 +238,43 @@ private static Stream streamSmeltingRecipes() { bigSnowball.offer(Keys.MAX_STACK_SIZE, 4); final ItemStack result = ItemStack.of(ItemTypes.BARRIER); - return Stream.of( - new DefaultedTestPopulator(new TestContext("regular_smelting", CookingRecipe.builder() + return Stream.of( + Stream.of(new TestContext("regular_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(snowball.type())) .result(result))) + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(4, 8) + .expectInput(List.of(bigSnowball)) + + .partialInventory(Collections.nCopies(8, bigSnowball)) + .badInput(List.of(bedrock)), + new BadLayoutTestPopulator(test) + .badInventory(List.of(bedrock)) + .badInput(List.of(bedrock)) + )) + .flatMap(TestPopulator::populate), - .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)) - - .partialInventory(Collections.nCopies(8, bigSnowball)) - .badInventory(List.of(bedrock)) - .badInput(List.of(bedrock)), - - new DefaultedTestPopulator(new TestContext("custom_smelting", CookingRecipe.builder() + Stream.of(new TestContext("custom_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(ResourceKey.sponge("big_snowball"), stack -> stack.type() == bigSnowball.type() && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), snowball)) .result(result))) - - .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)) - - .partialInventory(Collections.nCopies(8, bigSnowball)) - .badInventory(List.of(snowball4)) - .badInput(List.of(snowball4)) - ).flatMap(TestPopulator::populate); + .flatMap(test -> Stream.of( + new DefaultedTestPopulator(test) + .expectCrafts(4, 8) + .expectInput(List.of(bigSnowball)) + .partialInventory(Collections.nCopies(8, bigSnowball)) + .badInput(List.of(snowball4)), + new BadLayoutTestPopulator(test) + .badInventory(List.of(snowball4)) + .badInput(List.of(snowball4)) + )) + .flatMap(TestPopulator::populate) + ).flatMap(Function.identity()); } @TestFactory @@ -265,6 +290,10 @@ public Stream generateTests() { @FunctionalInterface private interface TestPopulator { + static TestPopulator composite(final Stream populators) { + return () -> populators.flatMap(TestPopulator::populate); + } + Stream populate(); } @@ -308,8 +337,6 @@ private static final class DefaultedTestPopulator implements TestPopulator { // All of them should be considered for expected crafts private List partialInventory = List.of(); private List partialInput = List.of(); - // "Bad" items exist to ensure placer clears input and doesn't pull wrong items if there is no other choice - private List badInventory = List.of(); private List badInput = List.of(); public DefaultedTestPopulator(final TestContext base) { @@ -337,11 +364,6 @@ public DefaultedTestPopulator partialInput(final List items) { return this; } - public DefaultedTestPopulator badInventory(final List items) { - this.badInventory = items; - return this; - } - public DefaultedTestPopulator badInput(final List items) { this.badInput = items; return this; @@ -351,22 +373,12 @@ public DefaultedTestPopulator badInput(final List items) { public Stream populate() { final List totalInitialInventory = Stream.concat(this.partialInventory.stream(), this.partialInput.stream()).toList(); final List expectedShiftInput = RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedShiftCrafts); - final List baseInputs = List.of( - this.base.name("Empty input").input(List.of()), - this.base.name("Bad input").input(this.badInput) - ); - final Stream testBadLayouts = baseInputs.stream() + final Stream testSingleClick = Stream.of(this.base) .flatMap(context -> Stream.of( - context.name("Empty inventory").inventory(List.of()), - context.name("Bad inventory").inventory(this.badInventory) + context.name("Empty input").input(List.of()), + context.name("Bad input").input(this.badInput) )) - .flatMap(context -> Stream.of(context, context.shift())) - .flatMap(context -> Stream.of(context, context.creative())) - // 2 clicks is enough to ensure we always get empty input - .map(context -> context.expectInputs(List.of(List.of(), List.of()))); - - final Stream testSingleClick = baseInputs.stream() .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) .flatMap(context -> Stream.of(context, context.creative())) .flatMap(context -> Stream.of( @@ -400,11 +412,49 @@ public Stream populate() { context.creative().expectInput(List.of()) )); - return Stream.of(testBadLayouts, testSingleClick, testMultipleClicks, testFullInventory) + return Stream.of(testSingleClick, testMultipleClicks, testFullInventory) .flatMap(Function.identity()); } } + private static final class BadLayoutTestPopulator implements TestPopulator { + + private final TestContext base; + private List badInventory = List.of(); + private List badInput = List.of(); + + public BadLayoutTestPopulator(final TestContext base) { + this.base = base; + } + + public BadLayoutTestPopulator badInventory(final List items) { + this.badInventory = items; + return this; + } + + public BadLayoutTestPopulator badInput(final List items) { + this.badInput = items; + return this; + } + + @Override + public Stream populate() { + return Stream.of(this.base) + .flatMap(context -> Stream.of( + context.name("Empty input").input(List.of()), + context.name("Bad input").input(this.badInput) + )) + .flatMap(context -> Stream.of( + context.name("Empty inventory").inventory(List.of()), + context.name("Bad inventory").inventory(this.badInventory) + )) + .flatMap(context -> Stream.of(context, context.shift())) + .flatMap(context -> Stream.of(context, context.creative())) + // 2 clicks is enough to ensure we always get empty input + .map(context -> context.expectInputs(List.of(List.of(), List.of()))); + } + } + private record TestContext( RecipeHolder recipe, String testName, boolean shiftClick, boolean creativeMode, @@ -466,7 +516,7 @@ public void testCrafting() { } public void testSmelting() { - this.test(MenuType.FURNACE, menu -> ((Inventory) ((AbstractFurnaceMenuAccessor) menu).accessor$container()).slot(0).get()); + this.test(MenuType.FURNACE, menu -> ((Inventory) ((AbstractFurnaceMenuAccessor) menu).accessor$container()).slot(0).orElseThrow()); } public void test( From 46cd7a322f35aebe00f76e388c8219bc9571c836 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Sun, 2 Nov 2025 22:32:05 +0300 Subject: [PATCH 14/20] Update RecipePlaceTest.java --- .../common/recipe/RecipePlaceTest.java | 215 +++++++----------- 1 file changed, 82 insertions(+), 133 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index e1a92125010..3645c17cc1b 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -102,6 +102,22 @@ private static ItemStack withQuantity(final ItemStack stack, final int quantity) return copy; } + private static TestPopulator badLayoutTests(final TestContext base) { + return () -> Stream.of(base) + .flatMap(context -> Stream.of( + context.name("Empty input").input(List.of()), + context.name("Bad input") + )) + .flatMap(context -> Stream.of( + context.name("Empty inventory").inventory(List.of()), + context.name("Bad inventory") + )) + .flatMap(context -> Stream.of(context, context.shift())) + .flatMap(context -> Stream.of(context, context.creative())) + // 2 clicks is enough to ensure we always get empty input + .map(context -> context.expectInputs(List.of(List.of(), List.of()))); + } + private static Stream streamCraftingRecipes() { final ItemStack empty = ItemStack.empty(); final ItemStack bedrock = ItemStack.of(ItemTypes.BEDROCK); @@ -137,23 +153,22 @@ private static Stream streamCraftingRecipes() { .where('P', anyPearlIngredient) .result(result))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) + new DefaultedTestPopulator(test + .inventory(List.of(stone64, bigPearl8, bigPearl8)) + .input(List.of( + stone64, empty, empty, + empty, bigPearl4, empty, + empty, empty, empty))) .expectCrafts(8, 16) .expectInput(List.of( stone, empty, stone, empty, bigPearl, empty, - empty, empty, empty)) - - .partialInventory(List.of(stone64, bigPearl8, bigPearl8)) - .partialInput(List.of( - stone64, empty, empty, - empty, bigPearl4, empty, - empty, empty, empty)) - .badInput(Collections.nCopies(9, bedrock)), - new BadLayoutTestPopulator(test) - .badInventory(Collections.nCopies(9, bedrock)) - .badInput(Collections.nCopies(9, bedrock)) - )).flatMap(TestPopulator::populate), + empty, empty, empty)), + RecipePlaceTest.badLayoutTests(test + .inventory(Collections.nCopies(9, bedrock)) + .input(Collections.nCopies(9, bedrock))) + )) + .flatMap(TestPopulator::populate), Stream.of(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("SSS", "BBB", "SSS") @@ -161,22 +176,20 @@ private static Stream streamCraftingRecipes() { .where('B', bigPearlIngredient) .result(result))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) + new DefaultedTestPopulator(test + .inventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) + .input(List.of( + smallPearl4, empty, smallPearl4, + bigPearl4, empty, bigPearl4, + smallPearl, empty, empty))) .expectCrafts(4, 4) .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - smallPearl, smallPearl, smallPearl)) - - .partialInventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) - .partialInput(List.of( - smallPearl4, empty, smallPearl4, - bigPearl4, empty, bigPearl4, - smallPearl, empty, empty)) - .badInput(Collections.nCopies(9, pearl)), - new BadLayoutTestPopulator(test) - .badInventory(List.of(pearl16)) - .badInput(Collections.nCopies(9, pearl)) + smallPearl, smallPearl, smallPearl)), + RecipePlaceTest.badLayoutTests(test + .inventory(List.of(pearl16)) + .input(Collections.nCopies(9, pearl))) )) .flatMap(TestPopulator::populate), @@ -184,22 +197,21 @@ private static Stream streamCraftingRecipes() { .addIngredients( anyPearlIngredient, stoneIngredient, anyPearlIngredient))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) + new DefaultedTestPopulator(test + .inventory(List.of(smallPearl32)) + .input(List.of( + smallPearl4, stone64, empty, + empty, empty, empty, + empty, empty, empty))) + .expectCrafts(16, 16) .expectInput(List.of( smallPearl, stone, smallPearl, empty, empty, empty, - empty, empty, empty)) - - .partialInventory(List.of(smallPearl32)) - .partialInput(List.of( - smallPearl4, stone64, empty, - empty, empty, empty, - empty, empty, empty)) - .badInput(Collections.nCopies(3, bedrock)), - new BadLayoutTestPopulator(test) - .badInventory(Collections.nCopies(10, bedrock)) - .badInput(Collections.nCopies(3, bedrock)) + empty, empty, empty)), + RecipePlaceTest.badLayoutTests(test + .inventory(Collections.nCopies(10, bedrock)) + .input(Collections.nCopies(3, bedrock))) )) .flatMap(TestPopulator::populate)/*, @@ -209,22 +221,21 @@ private static Stream streamCraftingRecipes() { smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, bigPearlIngredient, bigPearlIngredient, bigPearlIngredient))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) + new DefaultedTestPopulator(test + .inventory(Collections.nCopies(5, bigPearl8)) + .input(List.of( + smallPearl32, smallPearl32, smallPearl, + bigPearl4, bigPearl4, empty, + empty, empty, empty))) + .expectCrafts(8, 16) .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - empty, empty, empty)) - - .partialInventory(Collections.nCopies(5, bigPearl8)) - .partialInput(List.of( - smallPearl32, smallPearl32, smallPearl, - bigPearl4, bigPearl4, empty, - empty, empty, empty)) - .badInput(Collections.nCopies(6, pearl)), - new BadLayoutTestPopulator(test) - .badInventory(List.of(pearl16)) - .badInput(Collections.nCopies(6, pearl)) + empty, empty, empty)), + RecipePlaceTest.badLayoutTests(test + .inventory(List.of(pearl16)) + .input(Collections.nCopies(6, pearl))) )) .flatMap(TestPopulator::populate)*/ ).flatMap(Function.identity()); @@ -244,15 +255,14 @@ private static Stream streamSmeltingRecipes() { .ingredient(Ingredient.of(snowball.type())) .result(result))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) - .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)) + new DefaultedTestPopulator(test + .inventory(Collections.nCopies(8, bigSnowball))) - .partialInventory(Collections.nCopies(8, bigSnowball)) - .badInput(List.of(bedrock)), - new BadLayoutTestPopulator(test) - .badInventory(List.of(bedrock)) - .badInput(List.of(bedrock)) + .expectCrafts(4, 8) + .expectInput(List.of(bigSnowball)), + RecipePlaceTest.badLayoutTests(test + .inventory(List.of(bedrock)) + .input(List.of(bedrock))) )) .flatMap(TestPopulator::populate), @@ -264,14 +274,14 @@ private static Stream streamSmeltingRecipes() { snowball)) .result(result))) .flatMap(test -> Stream.of( - new DefaultedTestPopulator(test) + new DefaultedTestPopulator(test + .inventory(Collections.nCopies(8, bigSnowball))) + .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)) - .partialInventory(Collections.nCopies(8, bigSnowball)) - .badInput(List.of(snowball4)), - new BadLayoutTestPopulator(test) - .badInventory(List.of(snowball4)) - .badInput(List.of(snowball4)) + .expectInput(List.of(bigSnowball)), + RecipePlaceTest.badLayoutTests(test + .inventory(List.of(snowball4)) + .input(List.of(snowball4))) )) .flatMap(TestPopulator::populate) ).flatMap(Function.identity()); @@ -333,12 +343,6 @@ private static final class DefaultedTestPopulator implements TestPopulator { private int expectedShiftCrafts; private List expectedInput = List.of(); - // "Partial" items are used in actual crafting - // All of them should be considered for expected crafts - private List partialInventory = List.of(); - private List partialInput = List.of(); - private List badInput = List.of(); - public DefaultedTestPopulator(final TestContext base) { this.base = base; } @@ -354,32 +358,15 @@ public DefaultedTestPopulator expectInput(final List items) { return this; } - public DefaultedTestPopulator partialInventory(final List items) { - this.partialInventory = items; - return this; - } - - public DefaultedTestPopulator partialInput(final List items) { - this.partialInput = items; - return this; - } - - public DefaultedTestPopulator badInput(final List items) { - this.badInput = items; - return this; - } - @Override public Stream populate() { - final List totalInitialInventory = Stream.concat(this.partialInventory.stream(), this.partialInput.stream()).toList(); + final List totalInitialInventory = Stream.concat(this.base.inventory.stream(), this.base.input().stream()).toList(); final List expectedShiftInput = RecipePlaceTest.createExpectedInput(this.expectedInput, this.expectedShiftCrafts); final Stream testSingleClick = Stream.of(this.base) - .flatMap(context -> Stream.of( - context.name("Empty input").input(List.of()), - context.name("Bad input").input(this.badInput) - )) - .map(context -> context.name("Total inventory").inventory(totalInitialInventory)) + .map(context -> context + .name("Empty input").input(List.of()) + .name("Total inventory").inventory(totalInitialInventory)) .flatMap(context -> Stream.of(context, context.creative())) .flatMap(context -> Stream.of( context.expectInput(RecipePlaceTest.createExpectedInput(this.expectedInput, 1)), @@ -390,8 +377,8 @@ public Stream populate() { // So we can perform multiple-click tests on a single input. final Stream testMultipleClicks = Stream.of(this.base) .map(context -> context - .name("Partial input").input(this.partialInput) - .name("Partial inventory").inventory(this.partialInventory)) + .name("Partial input") + .name("Partial inventory")) .flatMap(context -> Stream.of(context, context.creative())) .flatMap(context -> Stream.of( context @@ -404,11 +391,11 @@ public Stream populate() { final Stream testFullInventory = Stream.of(this.base) .map(context -> context - .name("Partial input").input(this.partialInput) + .name("Partial input") .name("Full inventory").inventory(Collections.nCopies(36, ItemStack.of(ItemTypes.BARRIER, 64)))) .flatMap(context -> Stream.of(context, context.shift())) .flatMap(context -> Stream.of( - context.expectInput(this.partialInput), + context.expectInput(this.base.input()), context.creative().expectInput(List.of()) )); @@ -417,44 +404,6 @@ public Stream populate() { } } - private static final class BadLayoutTestPopulator implements TestPopulator { - - private final TestContext base; - private List badInventory = List.of(); - private List badInput = List.of(); - - public BadLayoutTestPopulator(final TestContext base) { - this.base = base; - } - - public BadLayoutTestPopulator badInventory(final List items) { - this.badInventory = items; - return this; - } - - public BadLayoutTestPopulator badInput(final List items) { - this.badInput = items; - return this; - } - - @Override - public Stream populate() { - return Stream.of(this.base) - .flatMap(context -> Stream.of( - context.name("Empty input").input(List.of()), - context.name("Bad input").input(this.badInput) - )) - .flatMap(context -> Stream.of( - context.name("Empty inventory").inventory(List.of()), - context.name("Bad inventory").inventory(this.badInventory) - )) - .flatMap(context -> Stream.of(context, context.shift())) - .flatMap(context -> Stream.of(context, context.creative())) - // 2 clicks is enough to ensure we always get empty input - .map(context -> context.expectInputs(List.of(List.of(), List.of()))); - } - } - private record TestContext( RecipeHolder recipe, String testName, boolean shiftClick, boolean creativeMode, From 9130a7149275cb5c99d11680ddafc265882e89d1 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:03:34 +0300 Subject: [PATCH 15/20] Update SpongeAPI --- SpongeAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpongeAPI b/SpongeAPI index 9475ca73e23..34622032e7b 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit 9475ca73e23b259f75c0b36aa981c0be347a5086 +Subproject commit 34622032e7bda623d9eff36cd00e7d1503cb2af6 From 6c684fba7b1dcd83f11bd900dc1313b27cd8f217 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:03:34 +0300 Subject: [PATCH 16/20] Update SpongeAPI --- SpongeAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpongeAPI b/SpongeAPI index 9475ca73e23..8b4f93e72fe 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit 9475ca73e23b259f75c0b36aa981c0be347a5086 +Subproject commit 8b4f93e72fec84dcfee8f322e48a6f71cee96654 From 5f40fb9c7158e697f0794288a9c72f7d5b60d264 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:14:45 +0300 Subject: [PATCH 17/20] Update SpongeAPI --- SpongeAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpongeAPI b/SpongeAPI index 8b4f93e72fe..bd65c38b7bb 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit 8b4f93e72fec84dcfee8f322e48a6f71cee96654 +Subproject commit bd65c38b7bb7e76033d9dc257a4dcd158e03333d From 0cf4640aaece7da42e31bf65f071c9e7be7c579d Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:20:43 +0300 Subject: [PATCH 18/20] enable custom shapelss recipe tests --- .../common/recipe/RecipePlaceTest.java | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index 3645c17cc1b..d3454d7be3a 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -102,8 +102,8 @@ private static ItemStack withQuantity(final ItemStack stack, final int quantity) return copy; } - private static TestPopulator badLayoutTests(final TestContext base) { - return () -> Stream.of(base) + private static Stream badLayoutTests(final TestContext base) { + return Stream.of(base) .flatMap(context -> Stream.of( context.name("Empty input").input(List.of()), context.name("Bad input") @@ -152,7 +152,7 @@ private static Stream streamCraftingRecipes() { .where('S', stoneIngredient) .where('P', anyPearlIngredient) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(stone64, bigPearl8, bigPearl8)) .input(List.of( @@ -163,19 +163,19 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( stone, empty, stone, empty, bigPearl, empty, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(Collections.nCopies(9, bedrock)) .input(Collections.nCopies(9, bedrock))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("SSS", "BBB", "SSS") .where('S', smallPearlIngredient) .where('B', bigPearlIngredient) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) .input(List.of( @@ -186,17 +186,17 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - smallPearl, smallPearl, smallPearl)), + smallPearl, smallPearl, smallPearl)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(pearl16)) .input(Collections.nCopies(9, pearl))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( anyPearlIngredient, stoneIngredient, anyPearlIngredient))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(smallPearl32)) .input(List.of( @@ -208,19 +208,18 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, stone, smallPearl, empty, empty, empty, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(Collections.nCopies(10, bedrock)) .input(Collections.nCopies(3, bedrock))) - )) - .flatMap(TestPopulator::populate)/*, + )), - //TODO uncomment after shapeless recipe fix Stream.of(new TestContext("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, bigPearlIngredient, bigPearlIngredient, bigPearlIngredient))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(5, bigPearl8)) .input(List.of( @@ -232,12 +231,12 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(pearl16)) .input(Collections.nCopies(6, pearl))) )) - .flatMap(TestPopulator::populate)*/ ).flatMap(Function.identity()); } @@ -254,17 +253,17 @@ private static Stream streamSmeltingRecipes() { .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(snowball.type())) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(8, bigSnowball))) .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)), + .expectInput(List.of(bigSnowball)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(bedrock)) .input(List.of(bedrock))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("custom_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) @@ -273,17 +272,17 @@ private static Stream streamSmeltingRecipes() { && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), snowball)) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(8, bigSnowball))) .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)), + .expectInput(List.of(bigSnowball)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(snowball4)) .input(List.of(snowball4))) )) - .flatMap(TestPopulator::populate) ).flatMap(Function.identity()); } @@ -300,10 +299,6 @@ public Stream generateTests() { @FunctionalInterface private interface TestPopulator { - static TestPopulator composite(final Stream populators) { - return () -> populators.flatMap(TestPopulator::populate); - } - Stream populate(); } From bf8b5f7ff9349eb66e62b571d9b68a069ff78de9 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:20:43 +0300 Subject: [PATCH 19/20] enable custom shapelss recipe tests --- .../common/recipe/RecipePlaceTest.java | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java index 3645c17cc1b..d3454d7be3a 100644 --- a/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java +++ b/src/test/java/org/spongepowered/common/recipe/RecipePlaceTest.java @@ -102,8 +102,8 @@ private static ItemStack withQuantity(final ItemStack stack, final int quantity) return copy; } - private static TestPopulator badLayoutTests(final TestContext base) { - return () -> Stream.of(base) + private static Stream badLayoutTests(final TestContext base) { + return Stream.of(base) .flatMap(context -> Stream.of( context.name("Empty input").input(List.of()), context.name("Bad input") @@ -152,7 +152,7 @@ private static Stream streamCraftingRecipes() { .where('S', stoneIngredient) .where('P', anyPearlIngredient) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(stone64, bigPearl8, bigPearl8)) .input(List.of( @@ -163,19 +163,19 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( stone, empty, stone, empty, bigPearl, empty, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(Collections.nCopies(9, bedrock)) .input(Collections.nCopies(9, bedrock))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("custom_shaped_crafting", CraftingRecipe.shapedBuilder() .aisle("SSS", "BBB", "SSS") .where('S', smallPearlIngredient) .where('B', bigPearlIngredient) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(smallPearl32, bigPearl, bigPearl, bigPearl, bigPearl, smallPearl32)) .input(List.of( @@ -186,17 +186,17 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - smallPearl, smallPearl, smallPearl)), + smallPearl, smallPearl, smallPearl)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(pearl16)) .input(Collections.nCopies(9, pearl))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("regular_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( anyPearlIngredient, stoneIngredient, anyPearlIngredient))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(List.of(smallPearl32)) .input(List.of( @@ -208,19 +208,18 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, stone, smallPearl, empty, empty, empty, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(Collections.nCopies(10, bedrock)) .input(Collections.nCopies(3, bedrock))) - )) - .flatMap(TestPopulator::populate)/*, + )), - //TODO uncomment after shapeless recipe fix Stream.of(new TestContext("custom_shapeless_crafting", CraftingRecipe.shapelessBuilder() .addIngredients( smallPearlIngredient, smallPearlIngredient, smallPearlIngredient, bigPearlIngredient, bigPearlIngredient, bigPearlIngredient))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(5, bigPearl8)) .input(List.of( @@ -232,12 +231,12 @@ private static Stream streamCraftingRecipes() { .expectInput(List.of( smallPearl, smallPearl, smallPearl, bigPearl, bigPearl, bigPearl, - empty, empty, empty)), + empty, empty, empty)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(pearl16)) .input(Collections.nCopies(6, pearl))) )) - .flatMap(TestPopulator::populate)*/ ).flatMap(Function.identity()); } @@ -254,17 +253,17 @@ private static Stream streamSmeltingRecipes() { .type(RecipeTypes.SMELTING) .ingredient(Ingredient.of(snowball.type())) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(8, bigSnowball))) .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)), + .expectInput(List.of(bigSnowball)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(bedrock)) .input(List.of(bedrock))) - )) - .flatMap(TestPopulator::populate), + )), Stream.of(new TestContext("custom_smelting", CookingRecipe.builder() .type(RecipeTypes.SMELTING) @@ -273,17 +272,17 @@ private static Stream streamSmeltingRecipes() { && stack.maxStackQuantity() == bigSnowball.maxStackQuantity(), snowball)) .result(result))) - .flatMap(test -> Stream.of( + .flatMap(test -> Stream.concat( new DefaultedTestPopulator(test .inventory(Collections.nCopies(8, bigSnowball))) .expectCrafts(4, 8) - .expectInput(List.of(bigSnowball)), + .expectInput(List.of(bigSnowball)) + .populate(), RecipePlaceTest.badLayoutTests(test .inventory(List.of(snowball4)) .input(List.of(snowball4))) )) - .flatMap(TestPopulator::populate) ).flatMap(Function.identity()); } @@ -300,10 +299,6 @@ public Stream generateTests() { @FunctionalInterface private interface TestPopulator { - static TestPopulator composite(final Stream populators) { - return () -> populators.flatMap(TestPopulator::populate); - } - Stream populate(); } From 2b0f447dc72dea40a1b0180003481521ba5efe18 Mon Sep 17 00:00:00 2001 From: MrHell228 Date: Fri, 28 Nov 2025 00:27:27 +0300 Subject: [PATCH 20/20] Update SpongeAPI --- SpongeAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpongeAPI b/SpongeAPI index bd65c38b7bb..9475ca73e23 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit bd65c38b7bb7e76033d9dc257a4dcd158e03333d +Subproject commit 9475ca73e23b259f75c0b36aa981c0be347a5086