From 89db87dbfce4e4bd130f5daccac7cac495002275 Mon Sep 17 00:00:00 2001 From: LexManos Date: Wed, 22 Nov 2017 13:26:02 -0800 Subject: [PATCH] Create custom shapeless recipe matching system. Should solve #4516 damageable items in shapeless recipes. --- .../item/crafting/Ingredient.java.patch | 27 ++- .../item/crafting/ShapelessRecipes.java.patch | 36 +++- .../common/crafting/CompoundIngredient.java | 12 ++ .../common/crafting/IngredientNBT.java | 6 + .../common/util/RecipeMatcher.java | 162 ++++++++++++++++++ .../minecraftforge/oredict/OreIngredient.java | 6 + .../oredict/ShapelessOreRecipe.java | 19 +- .../minecraftforge/debug/RecipeTestMod.java | 72 +++++++- .../forge/recipes/shapeless_damageable.json | 13 ++ .../recipetest/recipes/tool_complex.json | 21 +++ .../recipetest/recipes/tool_simple.json | 13 ++ 11 files changed, 368 insertions(+), 19 deletions(-) create mode 100644 src/main/java/net/minecraftforge/common/util/RecipeMatcher.java create mode 100644 src/test/resources/assets/forge/recipes/shapeless_damageable.json create mode 100644 src/test/resources/assets/recipetest/recipes/tool_complex.json create mode 100644 src/test/resources/assets/recipetest/recipes/tool_simple.json diff --git a/patches/minecraft/net/minecraft/item/crafting/Ingredient.java.patch b/patches/minecraft/net/minecraft/item/crafting/Ingredient.java.patch index 697a870b61..f9d344f2f3 100644 --- a/patches/minecraft/net/minecraft/item/crafting/Ingredient.java.patch +++ b/patches/minecraft/net/minecraft/item/crafting/Ingredient.java.patch @@ -1,6 +1,11 @@ --- ../src-base/minecraft/net/minecraft/item/crafting/Ingredient.java +++ ../src-work/minecraft/net/minecraft/item/crafting/Ingredient.java -@@ -13,6 +13,8 @@ +@@ -8,11 +8,11 @@ + import net.minecraft.client.util.RecipeItemHelper; + import net.minecraft.item.Item; + import net.minecraft.item.ItemStack; +-import net.minecraftforge.fml.relauncher.Side; +-import net.minecraftforge.fml.relauncher.SideOnly; public class Ingredient implements Predicate { @@ -9,12 +14,13 @@ public static final Ingredient field_193370_a = new Ingredient(new ItemStack[0]) { public boolean apply(@Nullable ItemStack p_apply_1_) -@@ -21,17 +23,34 @@ +@@ -21,17 +21,41 @@ } }; private final ItemStack[] field_193371_b; + private final ItemStack[] matchingStacksExploded; private IntList field_194140_c; ++ private final boolean isSimple; + protected Ingredient(int size) + { @@ -23,6 +29,7 @@ + protected Ingredient(ItemStack... p_i47503_1_) { ++ boolean simple = true; this.field_193371_b = p_i47503_1_; + net.minecraft.util.NonNullList lst = net.minecraft.util.NonNullList.func_191196_a(); + for (ItemStack s : p_i47503_1_) @@ -30,11 +37,16 @@ + if (s.func_190926_b()) + continue; + if (s.func_77960_j() == net.minecraftforge.oredict.OreDictionary.WILDCARD_VALUE) ++ { ++ if (s.func_77973_b().func_77645_m()) ++ simple = false; + s.func_77973_b().func_150895_a(net.minecraft.creativetab.CreativeTabs.field_78027_g, lst); ++ } + else + lst.add(s); + } + this.matchingStacksExploded = lst.toArray(new ItemStack[lst.size()]); ++ this.isSimple = simple && this.matchingStacksExploded.length > 0; + Ingredient.INSTANCES.add(this); } @@ -46,7 +58,7 @@ } public boolean apply(@Nullable ItemStack p_apply_1_) -@@ -63,9 +82,9 @@ +@@ -63,9 +87,9 @@ { if (this.field_194140_c == null) { @@ -58,7 +70,7 @@ { this.field_194140_c.add(RecipeItemHelper.func_194113_b(itemstack)); } -@@ -76,6 +95,18 @@ +@@ -76,6 +100,18 @@ return this.field_194140_c; } @@ -77,7 +89,7 @@ public static Ingredient func_193367_a(Item p_193367_0_) { return func_193369_a(new ItemStack(p_193367_0_, 1, 32767)); -@@ -108,4 +139,17 @@ +@@ -108,4 +144,22 @@ return field_193370_a; } @@ -93,5 +105,10 @@ + lst.add(stack); + } + return new Ingredient(lst.toArray(new ItemStack[lst.size()])); ++ } ++ ++ public boolean isSimple() ++ { ++ return isSimple || this == field_193370_a; + } } diff --git a/patches/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java.patch b/patches/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java.patch index df082d02b7..34e6831a17 100644 --- a/patches/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java.patch +++ b/patches/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java.patch @@ -1,6 +1,6 @@ --- ../src-base/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java +++ ../src-work/minecraft/net/minecraft/item/crafting/ShapelessRecipes.java -@@ -10,10 +10,8 @@ +@@ -10,23 +10,25 @@ import net.minecraft.util.JsonUtils; import net.minecraft.util.NonNullList; import net.minecraft.world.World; @@ -12,15 +12,25 @@ { private final ItemStack field_77580_a; public final NonNullList field_77579_b; -@@ -26,7 +24,6 @@ + private final String field_194138_c; ++ private final boolean isSimple; + + public ShapelessRecipes(String p_i47500_1_, ItemStack p_i47500_2_, NonNullList p_i47500_3_) + { + this.field_194138_c = p_i47500_1_; + this.field_77580_a = p_i47500_2_; this.field_77579_b = p_i47500_3_; ++ boolean simple = true; ++ for (Ingredient i : p_i47500_3_) ++ simple &= i.isSimple(); ++ this.isSimple = simple; } - @SideOnly(Side.CLIENT) public String func_193358_e() { return this.field_194138_c; -@@ -50,10 +47,7 @@ +@@ -50,10 +52,7 @@ { ItemStack itemstack = p_179532_1_.func_70301_a(i); @@ -32,17 +42,18 @@ } return nonnulllist; -@@ -61,7 +55,8 @@ +@@ -61,7 +60,9 @@ public boolean func_77569_a(InventoryCrafting p_77569_1_, World p_77569_2_) { - List list = Lists.newArrayList(this.field_77579_b); + int ingredientCount = 0; + net.minecraft.client.util.RecipeItemHelper recipeItemHelper = new net.minecraft.client.util.RecipeItemHelper(); ++ List inputs = Lists.newArrayList(); for (int i = 0; i < p_77569_1_.func_174923_h(); ++i) { -@@ -71,27 +66,13 @@ +@@ -71,27 +72,22 @@ if (!itemstack.func_190926_b()) { @@ -63,17 +74,26 @@ - return false; - } + ++ingredientCount; -+ recipeItemHelper.func_194112_a(itemstack); ++ if (this.isSimple) ++ recipeItemHelper.func_194112_a(itemstack); ++ else ++ inputs.add(itemstack); } } } - return list.isEmpty(); -+ return ingredientCount == this.field_77579_b.size() && recipeItemHelper.func_194116_a(this, null); ++ if (ingredientCount != this.field_77579_b.size()) ++ return false; ++ ++ if (this.isSimple) ++ return recipeItemHelper.func_194116_a(this, null); ++ ++ return net.minecraftforge.common.util.RecipeMatcher.findMatches(inputs, this.field_77579_b) != null; } public ItemStack func_77572_b(InventoryCrafting p_77572_1_) -@@ -136,7 +117,6 @@ +@@ -136,7 +132,6 @@ return nonnulllist; } diff --git a/src/main/java/net/minecraftforge/common/crafting/CompoundIngredient.java b/src/main/java/net/minecraftforge/common/crafting/CompoundIngredient.java index 32a2c02803..60909ddd45 100644 --- a/src/main/java/net/minecraftforge/common/crafting/CompoundIngredient.java +++ b/src/main/java/net/minecraftforge/common/crafting/CompoundIngredient.java @@ -20,11 +20,17 @@ public class CompoundIngredient extends Ingredient private Collection children; private ItemStack[] stacks; private IntList itemIds; + private final boolean isSimple; protected CompoundIngredient(Collection children) { super(0); this.children = children; + + boolean simple = true; + for (Ingredient child : children) + simple &= child.isSimple(); + this.isSimple = simple; } @Override @@ -78,4 +84,10 @@ public class CompoundIngredient extends Ingredient this.stacks = null; //Shouldn't need to invalidate children as this is only called form invalidateAll.. } + + @Override + public boolean isSimple() + { + return isSimple; + } } diff --git a/src/main/java/net/minecraftforge/common/crafting/IngredientNBT.java b/src/main/java/net/minecraftforge/common/crafting/IngredientNBT.java index 3f7c31edaf..9a85682335 100644 --- a/src/main/java/net/minecraftforge/common/crafting/IngredientNBT.java +++ b/src/main/java/net/minecraftforge/common/crafting/IngredientNBT.java @@ -40,4 +40,10 @@ public class IngredientNBT extends Ingredient //Can't use areItemStacksEqualUsingNBTShareTag because it compares stack size as well return this.stack.getItem() == input.getItem() && this.stack.getItemDamage() == input.getItemDamage() && ItemStack.areItemStackShareTagsEqual(this.stack, input); } + + @Override + public boolean isSimple() + { + return false; + } } diff --git a/src/main/java/net/minecraftforge/common/util/RecipeMatcher.java b/src/main/java/net/minecraftforge/common/util/RecipeMatcher.java new file mode 100644 index 0000000000..c8549ff1ed --- /dev/null +++ b/src/main/java/net/minecraftforge/common/util/RecipeMatcher.java @@ -0,0 +1,162 @@ +/* + * Minecraft Forge + * Copyright (c) 2016. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package net.minecraftforge.common.util; + +import java.util.BitSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import com.google.common.base.Predicate; + +public class RecipeMatcher +{ + /** + * Attempts to match inputs to the specified tests. In the best way that all inputs are used by one test. + * Will return null in any of these cases: + * input/test lengths don't match. This is only for matching paired outputs. + * any input doesn't match a test + * any test doesn't match a input + * If we are unable to determine a proper pair + * + * @return An array mapping inputs to tests. ret[x] = y means input[x] = test[y] + */ + public static int[] findMatches(List inputs, List> tests) + { + int elements = inputs.size(); + if (elements != tests.size()) + return null; // There will not be a 1:1 mapping of inputs -> tests + + int[] ret = new int[elements]; + for (int x = 0; x < elements; x++) + ret[x] = -1; + + // [UnusedInputs] [UnusedIngredients] [IngredientMatchMask]... + BitSet data = new BitSet((elements + 2) * elements); + for (int x = 0; x < elements; x++) + { + int matched = 0; + int offset = (x + 2) * elements; + Predicate test = tests.get(x); + + for (int y = 0; y < elements; y++) + { + if (data.get(y)) + continue; + + if (test.apply(inputs.get(y))) + { + data.set(offset + y); + matched++; + } + } + + if (matched == 0) + return null; //We have an test that matched non of the inputs + + if (matched == 1) + { + if (!claim(ret, data, x, elements)) + return null; //We failed to claim this index, which means it caused something else to go to 0 matches, which makes the whole thing fail + } + } + + if (data.nextClearBit(0) >= elements) //All items have been used, which means all tests have a match! + return ret; + + // We should be in a state where multiple tests are satified by multiple inputs. So we need to try a branching recursive test. + // However for performance reasons, we should probably make that check a sub-set of the entire graph. + if (backtrack(data, ret, 0, elements)) + return ret; + + return null; //Backtrack failed, no matches, we cry and go home now :( + } + + // This is bad... need to think of a better cascade, recursion instead of stack? + private static boolean claim(int[] ret, BitSet data, int claimed, int elements) + { + Queue pending = new LinkedList(); + pending.add(claimed); + + while (pending.peek() != null) + { + int test = pending.poll(); + int offset = (test + 2) * elements; + int used = data.nextSetBit(offset) - offset; + + if (used >= elements || used < 0) + throw new IllegalStateException("What? We matched something, but it wasn't set in the range of this test! Test: " + test + " Used: " + used); + + data.set(used); + data.set(elements + test); + ret[used] = test; + + for (int x = 0; x < elements; x++) + { + offset = (x + 2) * elements; + if (data.get(offset + used) && !data.get(elements + x)) + { + data.clear(offset + used); + int count = 0; + for (int y = offset; y < offset + elements; y++) + if (data.get(y)) + count++; + + if (count == 0) + return false; //Claiming this caused another test to lose its last match.. + + if (count == 1) + pending.add(x); + } + } + } + + return true; + } + + //We use recursion here, why? Because I feel like it. Also because we should only ever be working in data sets < 9 + private static boolean backtrack(BitSet data, int[] ret, int start, int elements) + { + int test = data.nextClearBit(elements + start) - elements; + if (test >= elements) + return true; //Could not find the next unused test. + + if (test < 0) + throw new IllegalStateException("This should never happen, negative test in backtrack!"); + + int offset = (test + 2) * elements; + for (int x = 0; x < elements; x++) + { + if (!data.get(offset + x) || data.get(x)) + continue; + + data.set(x); + + if (backtrack(data, ret, test + 1, elements)) + { + ret[x] = test; + return true; + } + + data.clear(x); + } + + return false; + } +} diff --git a/src/main/java/net/minecraftforge/oredict/OreIngredient.java b/src/main/java/net/minecraftforge/oredict/OreIngredient.java index 11ae517367..9a017b9258 100644 --- a/src/main/java/net/minecraftforge/oredict/OreIngredient.java +++ b/src/main/java/net/minecraftforge/oredict/OreIngredient.java @@ -114,4 +114,10 @@ public class OreIngredient extends Ingredient this.itemIds = null; this.array = null; } + + @Override + public boolean isSimple() + { + return true; + } } diff --git a/src/main/java/net/minecraftforge/oredict/ShapelessOreRecipe.java b/src/main/java/net/minecraftforge/oredict/ShapelessOreRecipe.java index 56d9485da2..db07357ba6 100644 --- a/src/main/java/net/minecraftforge/oredict/ShapelessOreRecipe.java +++ b/src/main/java/net/minecraftforge/oredict/ShapelessOreRecipe.java @@ -33,6 +33,7 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.world.World; import net.minecraftforge.common.crafting.CraftingHelper; import net.minecraftforge.common.crafting.JsonContext; +import net.minecraftforge.common.util.RecipeMatcher; import net.minecraftforge.registries.IForgeRegistryEntry; import javax.annotation.Nonnull; @@ -48,6 +49,7 @@ public class ShapelessOreRecipe extends IForgeRegistryEntry.Impl implem protected ItemStack output = ItemStack.EMPTY; protected NonNullList input = NonNullList.create(); protected ResourceLocation group; + protected boolean isSimple = true; public ShapelessOreRecipe(ResourceLocation group, Block result, Object... recipe){ this(group, new ItemStack(result), recipe); } public ShapelessOreRecipe(ResourceLocation group, Item result, Object... recipe){ this(group, new ItemStack(result), recipe); } @@ -56,6 +58,8 @@ public class ShapelessOreRecipe extends IForgeRegistryEntry.Impl implem this.group = group; output = result.copy(); this.input = input; + for (Ingredient i : input) + this.isSimple &= i.isSimple(); } public ShapelessOreRecipe(ResourceLocation group, @Nonnull ItemStack result, Object... recipe) { @@ -67,6 +71,7 @@ public class ShapelessOreRecipe extends IForgeRegistryEntry.Impl implem if (ing != null) { input.add(ing); + this.isSimple &= ing.isSimple(); } else { @@ -94,6 +99,7 @@ public class ShapelessOreRecipe extends IForgeRegistryEntry.Impl implem { int ingredientCount = 0; RecipeItemHelper recipeItemHelper = new RecipeItemHelper(); + List items = Lists.newArrayList(); for (int i = 0; i < inv.getSizeInventory(); ++i) { @@ -101,11 +107,20 @@ public class ShapelessOreRecipe extends IForgeRegistryEntry.Impl implem if (!itemstack.isEmpty()) { ++ingredientCount; - recipeItemHelper.accountStack(itemstack); + if (this.isSimple) + recipeItemHelper.accountStack(itemstack); + else + items.add(itemstack); } } - return ingredientCount == this.input.size() && recipeItemHelper.canCraft(this, null); + if (ingredientCount != this.input.size()) + return false; + + if (this.isSimple) + return recipeItemHelper.canCraft(this, null); + + return RecipeMatcher.findMatches(items, this.input) != null; } @Override diff --git a/src/test/java/net/minecraftforge/debug/RecipeTestMod.java b/src/test/java/net/minecraftforge/debug/RecipeTestMod.java index cc9c48c4a8..df3e1b8275 100644 --- a/src/test/java/net/minecraftforge/debug/RecipeTestMod.java +++ b/src/test/java/net/minecraftforge/debug/RecipeTestMod.java @@ -1,44 +1,108 @@ package net.minecraftforge.debug; +import java.util.Random; + +import net.minecraft.client.renderer.block.model.ModelResourceLocation; +import net.minecraft.creativetab.CreativeTabs; import net.minecraft.init.Blocks; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.IRecipe; import net.minecraft.item.crafting.ShapedRecipes; +import net.minecraft.util.EnumActionResult; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.ModelLoader; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.crafting.CraftingHelper; +import net.minecraftforge.debug.OnItemUseFirstTest.CommonProxy; +import net.minecraftforge.debug.OnItemUseFirstTest.ItemTest; import net.minecraftforge.event.RegistryEvent; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.SidedProxy; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.registry.GameRegistry.ObjectHolder; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.oredict.ShapedOreRecipe; -@Mod(modid = "recipetest", name = "Recipe test mod", version = "1.0", acceptableRemoteVersions = "*") +@Mod(modid = RecipeTestMod.MODID, name = "Recipe test mod", version = "1.0", acceptableRemoteVersions = "*") public class RecipeTestMod { + public static final String MODID = "recipetest"; + private static final boolean ENABLED = true; + @SidedProxy + public static CommonProxy proxy = null; + + @Mod.EventHandler public void preinit(FMLPreInitializationEvent event) { - MinecraftForge.EVENT_BUS.register(this); + if (ENABLED) + MinecraftForge.EVENT_BUS.register(this); } @SubscribeEvent public void registerRecipes(RegistryEvent.Register event) { - ResourceLocation location1 = new ResourceLocation("recipetest", "dirt"); + ResourceLocation location1 = new ResourceLocation(MODID, "dirt"); ShapedOreRecipe recipe1 = new ShapedOreRecipe(location1, new ItemStack(Blocks.DIAMOND_BLOCK), "DDD", 'D', new ItemStack(Blocks.DIRT)); recipe1.setRegistryName(location1); event.getRegistry().register(recipe1); if (FMLLaunchHandler.side() == Side.SERVER) { - ResourceLocation location2 = new ResourceLocation("recipetest", "stone"); + ResourceLocation location2 = new ResourceLocation(MODID, "stone"); CraftingHelper.ShapedPrimer primer1 = CraftingHelper.parseShaped("SSS", 'S', new ItemStack(Blocks.IRON_BLOCK)); ShapedRecipes recipe2 = new ShapedRecipes(location2.getResourcePath(), primer1.width, primer1.height, primer1.input, new ItemStack(Blocks.GOLD_BLOCK)); recipe2.setRegistryName(location2); event.getRegistry().register(recipe2); } } + + @SubscribeEvent + public void registerItems(RegistryEvent.Register event) + { + proxy.registerItem(event); + } + + public static abstract class CommonProxy + { + protected Item TOOL; + public void registerItem(RegistryEvent.Register event) + { + TOOL = new Item() + { + Random RAND = new Random(); + @Override + public ItemStack getContainerItem(ItemStack in) + { + ItemStack ret = in.copy(); + ret.attemptDamageItem(1, RAND, null); + return ret; + } + + @Override + public boolean hasContainerItem() + { + return true; + } + }.setRegistryName(MODID, "tool").setMaxDamage(10).setCreativeTab(CreativeTabs.MISC).setUnlocalizedName("recipetest.tool").setMaxStackSize(1); + event.getRegistry().register(TOOL); + } + } + + public static final class ServerProxy extends CommonProxy + { + } + + public static final class ClientProxy extends CommonProxy + { + @Override + public void registerItem(RegistryEvent.Register event) + { + super.registerItem(event); + ModelLoader.setCustomModelResourceLocation(TOOL, 0, new ModelResourceLocation("minecraft:stick#inventory")); + } + } } diff --git a/src/test/resources/assets/forge/recipes/shapeless_damageable.json b/src/test/resources/assets/forge/recipes/shapeless_damageable.json new file mode 100644 index 0000000000..b34590375a --- /dev/null +++ b/src/test/resources/assets/forge/recipes/shapeless_damageable.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { + "item": "minecraft:bow", + "data": 32767 + } + ], + "result": { + "item": "minecraft:wool", + "data": 9 + } +} \ No newline at end of file diff --git a/src/test/resources/assets/recipetest/recipes/tool_complex.json b/src/test/resources/assets/recipetest/recipes/tool_complex.json new file mode 100644 index 0000000000..f2d6c8178f --- /dev/null +++ b/src/test/resources/assets/recipetest/recipes/tool_complex.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { + "item": "tool", + "data": 32767 + }, + { + "item": "minecraft:stone", + "data": 32767 + }, + { + "item": "minecraft:stone", + "data": 0 + } + ], + "result": { + "item": "minecraft:wool", + "data": 0 + } +} diff --git a/src/test/resources/assets/recipetest/recipes/tool_simple.json b/src/test/resources/assets/recipetest/recipes/tool_simple.json new file mode 100644 index 0000000000..192c2fa77d --- /dev/null +++ b/src/test/resources/assets/recipetest/recipes/tool_simple.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { + "item": "tool", + "data": 32767 + } + ], + "result": { + "item": "minecraft:wool", + "data": 9 + } +}