diff --git a/src/functionalTest/java/gregtech/test/api/recipe/store/RecipeTrieTest.java b/src/functionalTest/java/gregtech/test/api/recipe/store/RecipeTrieTest.java new file mode 100644 index 00000000000..5a4eb321894 --- /dev/null +++ b/src/functionalTest/java/gregtech/test/api/recipe/store/RecipeTrieTest.java @@ -0,0 +1,358 @@ +package gregtech.test.api.recipe.store; + +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidRegistry; +import net.minecraftforge.fluids.FluidStack; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableSet; + +import gregtech.api.recipe.store.RecipeTrie; +import gregtech.api.util.GT_Recipe; +import gregtech.api.util.GT_RecipeBuilder; + +public class RecipeTrieTest { + + @Test + public void testAdd() { + RecipeTrie trie = new RecipeTrie(); + + boolean result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertTrue(result); + Assertions.assertEquals(1, trie.size()); + + // adding the same recipe twice should fail + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertFalse(result); + Assertions.assertEquals(1, trie.size()); + + // add another valid recipe + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertTrue(result); + Assertions.assertEquals(2, trie.size()); + + // adding a conflicting recipe should fail + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertFalse(result); + Assertions.assertEquals(2, trie.size()); + + // add another valid recipe + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertTrue(result); + Assertions.assertEquals(3, trie.size()); + + // adding the same recipe with a different input ordering should fail + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.gold_ingot), new ItemStack(Items.iron_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertFalse(result); + Assertions.assertEquals(3, trie.size()); + + // adding a new unique recipe should succeed + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.flint)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertTrue(result); + Assertions.assertEquals(4, trie.size()); + + // empty recipe inputs should fail + + result = trie.add( + GT_RecipeBuilder.builder() + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertFalse(result); + Assertions.assertEquals(4, trie.size()); + + // empty outputs do not matter to the trie and should succeed + + result = trie.add( + GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.melon_seeds)) + .duration(1) + .eut(1) + .build() + .get()); + Assertions.assertTrue(result); + Assertions.assertEquals(5, trie.size()); + } + + @Test + public void testFind() { + RecipeTrie trie = new RecipeTrie(); + GT_Recipe recipe = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe); + GT_Recipe recipe2 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.LAVA, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe2); + GT_Recipe recipe3 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.diamond)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe3); + GT_Recipe recipe4 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.gold_ingot), new ItemStack(Items.diamond)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe4); + GT_Recipe recipe5 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.flint)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe5); + GT_Recipe recipe6 = GT_RecipeBuilder.builder() + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe6); + + GT_Recipe result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertEquals(recipe, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.LAVA, 1) }, + i -> true); + Assertions.assertEquals(recipe2, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.diamond) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertEquals(recipe3, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.diamond), new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertEquals(recipe4, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.flint), }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertEquals(recipe5, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.flint), new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertEquals(recipe5, result); + + result = trie.findMatching( + new ItemStack[] {}, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1) }, + i -> true); + Assertions.assertEquals(recipe6, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1) }, + i -> true); + Assertions.assertEquals(recipe6, result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.iron_ingot), }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertNull(result); + + result = trie + .findMatching(new ItemStack[] { new ItemStack(Items.iron_ingot), }, new FluidStack[] {}, i -> true); + Assertions.assertNull(result); + + result = trie.findMatching( + new ItemStack[] { new ItemStack(Items.gold_ingot) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, + i -> true); + Assertions.assertNull(result); + + result = trie.findMatching(new ItemStack[] { new ItemStack(Items.gold_ingot) }, new FluidStack[] {}, i -> true); + Assertions.assertNull(result); + + result = trie + .findMatching(new ItemStack[] {}, new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1) }, i -> true); + Assertions.assertNull(result); + } + + @Test + public void testFindAll() { + RecipeTrie trie = new RecipeTrie(); + GT_Recipe recipe = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe); + GT_Recipe recipe2 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot)) + .fluidInputs(new FluidStack(FluidRegistry.LAVA, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe2); + GT_Recipe recipe3 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.iron_ingot), new ItemStack(Items.diamond)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe3); + GT_Recipe recipe4 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.gold_ingot), new ItemStack(Items.diamond)) + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe4); + GT_Recipe recipe5 = GT_RecipeBuilder.builder() + .itemInputs(new ItemStack(Items.flint)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe5); + GT_Recipe recipe6 = GT_RecipeBuilder.builder() + .fluidInputs(new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1)) + .itemOutputs(new ItemStack(Items.diamond)) + .duration(1) + .eut(1) + .build() + .get(); + trie.add(recipe6); + var all = ImmutableSet.of(recipe, recipe2, recipe3, recipe4, recipe5, recipe6); + var subset4 = ImmutableSet.of(recipe, recipe2, recipe5, recipe6); + + var results = trie.findAll( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot), + new ItemStack(Items.diamond), new ItemStack(Items.flint) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1) }); + Assertions.assertEquals(6, results.size()); + Assertions.assertTrue(results.containsAll(all)); + Assertions.assertTrue(all.containsAll(results)); + + results = trie.findAll( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot), + new ItemStack(Items.flint) }, + new FluidStack[] { new FluidStack(FluidRegistry.WATER, 1), new FluidStack(FluidRegistry.LAVA, 1) }); + Assertions.assertEquals(4, results.size()); + Assertions.assertTrue(results.containsAll(subset4)); + Assertions.assertTrue(subset4.containsAll(results)); + + results = trie.findAll( + new ItemStack[] { new ItemStack(Items.iron_ingot), new ItemStack(Items.gold_ingot), + new ItemStack(Items.diamond), new ItemStack(Items.flint) }, + new FluidStack[] {}); + Assertions.assertEquals(1, results.size()); + Assertions.assertEquals(recipe5, results.toArray()[0]); + Assertions.assertTrue(all.containsAll(results)); + } + + @Test + public void testRemove() { + + } +} diff --git a/src/main/java/gregtech/api/recipe/RecipeMapBackend.java b/src/main/java/gregtech/api/recipe/RecipeMapBackend.java index 8ba0895a7f6..f1813d770ee 100644 --- a/src/main/java/gregtech/api/recipe/RecipeMapBackend.java +++ b/src/main/java/gregtech/api/recipe/RecipeMapBackend.java @@ -31,6 +31,7 @@ import gregtech.api.GregTech_API; import gregtech.api.interfaces.IRecipeMap; import gregtech.api.objects.GT_ItemStack; +import gregtech.api.recipe.store.RecipeTrie; import gregtech.api.util.GT_OreDictUnificator; import gregtech.api.util.GT_Recipe; import gregtech.api.util.GT_RecipeBuilder; @@ -73,6 +74,8 @@ public class RecipeMapBackend { */ protected final RecipeMapBackendProperties properties; + private final RecipeTrie trie = new RecipeTrie(); + public RecipeMapBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) { this.properties = propertiesBuilder.build(); GregTech_API.itemStackMultiMaps.add(itemIndex); @@ -122,6 +125,10 @@ public Map> getRecipeCategoryMap() { return Collections.unmodifiableMap(recipesByCategory); } + public RecipeTrie trie() { + return trie; + } + // region add recipe /** @@ -196,6 +203,12 @@ protected Collection doAdd(GT_RecipeBuilder builder) { downstream.doAdd(builder); } } + for (GT_Recipe recipe : ret) { + if (!trie.add(recipe)) { + // TODO uncomment when collisions are reduced + // handleCollision(recipe); + } + } return ret; } diff --git a/src/main/java/gregtech/api/recipe/store/RecipeTrie.java b/src/main/java/gregtech/api/recipe/store/RecipeTrie.java new file mode 100644 index 00000000000..ee177722e9a --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/RecipeTrie.java @@ -0,0 +1,656 @@ +package gregtech.api.recipe.store; + +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidRegistry; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.oredict.OreDictionary; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import gregtech.api.recipe.store.ingredient.*; +import gregtech.api.util.GT_Log; +import gregtech.api.util.GT_Recipe; +import gregtech.api.util.function.Either; +import gregtech.api.util.item.ItemHolder; +import gregtech.common.config.gregtech.ConfigGeneral; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +public final class RecipeTrie { + + // debug + private static final boolean CRASH_ON_EMPTY = false; + + private static final Map> ingredientCache = new WeakHashMap<>(); + + final TrieBranch rootBranch = new TrieBranch(); + + private boolean hasOreDictInputs; + private boolean hasNBTMatchInputs; + + private int size; + + /** + * @param recipe the recipe to add + * @return if addition was successful + */ + public boolean add(@NotNull GT_Recipe recipe) { + var ingredients = createIngredients(recipe); + if (ingredients == null) { + if (ConfigGeneral.loggingRecipes) { + GT_Log.recipe.printf("Recipe has no inputs: %s", recipe); + if (ConfigGeneral.loggingRecipesStackTrace) { + new Throwable().printStackTrace(GT_Log.recipe); + } + } + if (CRASH_ON_EMPTY) { + throw new IllegalArgumentException("Cannot add recipe with no inputs"); + } + return false; + } + + if (add(recipe, ingredients)) { + this.size++; + return true; + } + return false; + } + + /** + * @param recipe the recipe to add + * @param ingredients the ingredients the recipe contains + * @return if addition was successful + */ + private boolean add(@NotNull GT_Recipe recipe, @NotNull AbstractMapIngredient @NotNull [] ingredients) { + TrieBranch branch = rootBranch; + for (int i = 0; i < ingredients.length; i++) { + final boolean isLast = i == ingredients.length - 1; + + final AbstractMapIngredient ingredient = ingredients[i]; + var inserted = insertIntoBranch(recipe, branch, ingredient, isLast); + + if (inserted == null) { + // some kind of conflict + return false; + } + + var addedRecipe = inserted.left(); + if (addedRecipe != null) { + // successful if the added recipe was the one that is attempting to be added + return addedRecipe == recipe; + } + + var nextBranch = inserted.right(); + assert nextBranch != null; + + // tail recursion: add each ingredient on the right branch path, and insert the recipe on the left at the + // end + branch = nextBranch; + } + + return false; + } + + /** + * @param recipe the recipe to insert + * @param branch the current branch to insert into + * @param ingredient the ingredient to associate with the branch + * @param isLast if this is the final branch in the route + * @return the inserted element, or an existing one, or null if there is a conflict with many recipes + */ + private static @Nullable Either insertIntoBranch(@NotNull GT_Recipe recipe, + @NotNull TrieBranch branch, @NotNull AbstractMapIngredient ingredient, boolean isLast) { + var nodes = branch.getNodes(); + var value = nodes.get(ingredient); + if (isLast) { + if (value == null) { + // no recipe, so put one here + value = Either.left(recipe); + nodes.put(ingredient, value); + return value; + } + + GT_Recipe existing = value.left(); + if (existing != null) { + if (ConfigGeneral.loggingRecipes) { + if (existing.equals(recipe)) { + GT_Log.recipe.printf("Duplicate recipe: %s%n", recipe); + } else { + GT_Log.recipe.printf("Conflicting recipes:%nExisting: %s%nConflict: %s%n", existing, recipe); + } + + if (ConfigGeneral.loggingRecipesStackTrace) { + new Throwable().printStackTrace(GT_Log.recipe); + } + } + // return the existing recipes on conflicts so earlier recurses can use it + return value; + } + + // this recipe is a subset of one or more recipes, which is a conflict + var existingEdge = value.right(); + assert existingEdge != null; + if (ConfigGeneral.loggingRecipes) { + StringBuilder builder = new StringBuilder("Conflicting recipes:%n1: ").append(recipe); + int i = 2; + for (GT_Recipe r : existingEdge.getAll() + .toArray(GT_Recipe[]::new)) { + builder.append("%n") + .append(i++) + .append(": ") + .append(r); + } + GT_Log.recipe.printf(builder.toString()); + } + if (ConfigGeneral.loggingRecipesStackTrace) { + new Throwable().printStackTrace(GT_Log.recipe); + } + + return null; + } else if (value == null) { + // no existing ingredient is present, so use a new one + value = Either.right(new TrieBranch()); + nodes.put(ingredient, value); + return value; + } + + // an existing ingredient is present, so use it + return value; + } + + /** + * @param recipe the recipe + * @return the trie ingredients for the recipe + */ + private @NotNull AbstractMapIngredient @Nullable [] createIngredients(@NotNull GT_Recipe recipe) { + int length = recipe.mInputs.length + recipe.mFluidInputs.length; + if (length == 0) { + return null; + } + + ObjectArrayList list = new ObjectArrayList<>(length); + if (recipe.mInputs.length > 0) { + var unique = uniqueIngredients(recipe.mInputs); + for (var input : unique) { + list.add(deduplicateIngredient(ingredientCache, MapIngredientFactory.from(input))); + + // TODO partial nbt match inputs here + // this.hasNBTMatchInputs = true; + } + } + if (recipe instanceof GT_Recipe.GT_Recipe_WithAlt oreDictRecipe) { + // TODO oredict goes here + this.hasOreDictInputs = true; + } + if (recipe.mFluidInputs.length > 0) { + var unique = uniqueIngredients(recipe.mFluidInputs); + for (var input : unique) { + list.add(deduplicateIngredient(ingredientCache, new MapFluidStackIngredient(input))); + } + } + + list.sort(MapIngredientComparator.INSTANCE); + + return list.toArray(new AbstractMapIngredient[0]); + } + + /** + * Deduplicates all item ingredients + * + * @param stacks the stacks to deduplicate + * @return the deduplicated list of stacks + */ + private static @NotNull List<@NotNull ItemStack> uniqueIngredients(@Nullable ItemStack @NotNull [] stacks) { + List list = new ObjectArrayList<>(stacks.length); + for (var stack : stacks) { + if (stack == null) { + continue; + } + + boolean isEqual = false; + for (var seen : list) { + if (seen.getItem() != stack.getItem()) { + continue; + } + if (seen.getItemDamage() != stack.getItemDamage()) { + continue; + } + if (Objects.equals(seen.getTagCompound(), stack.getTagCompound())) { + isEqual = true; + break; + } + } + + if (isEqual) { + continue; + } + + list.add(stack); + } + + return list; + } + + /** + * Deduplicates all fluid ingredients + * + * @param stacks the stacks to deduplicate + * @return the deduplicated list of fluids + */ + private static @NotNull List<@NotNull Fluid> uniqueIngredients(@Nullable FluidStack @NotNull [] stacks) { + List list = new ObjectArrayList<>(stacks.length); + for (var stack : stacks) { + if (stack == null) { + continue; + } + + Fluid fluid = stack.getFluid(); + boolean isEqual = false; + for (var seen : list) { + if (FluidRegistry.getFluidID(seen) == FluidRegistry.getFluidID(fluid)) { + isEqual = true; + break; + } + } + if (isEqual) { + continue; + } + + list.add(fluid); + } + + return list; + } + + /** + * Deduplicates ingredients using a cache + * + * @param cache the cache to use + * @param ingredient the ingredient to deduplicate + * @return the deduplicated ingredient + */ + private static @NotNull AbstractMapIngredient deduplicateIngredient( + @NotNull Map> cache, + @NotNull AbstractMapIngredient ingredient) { + var cached = cache.get(ingredient); + if (cached != null) { + AbstractMapIngredient i = cached.get(); + if (i != null) { + return i; + } + } + + cache.put(ingredient, new WeakReference<>(ingredient)); + return ingredient; + } + + /** + * @param recipe the recipe to remove + * @return if removal was successful + */ + public boolean remove(@NotNull GT_Recipe recipe) { + var ingredients = createIngredients(recipe); + if (ingredients == null) { + throw new IllegalArgumentException("Cannot remove recipe with no inputs"); + } + + if (remove(recipe, ingredients, rootBranch, 0) != null) { + size--; + return true; + } + return false; + } + + /** + * @param recipe the recipe to remove + * @param ingredients the ingredients the recipe contains + * @param branch the branch to remove from + * @param index the ingredient index + * @return the removed recipe + */ + private static @Nullable GT_Recipe remove(@NotNull GT_Recipe recipe, + @NotNull AbstractMapIngredient @NotNull [] ingredients, @NotNull TrieBranch branch, int index) { + AbstractMapIngredient ingredient = ingredients[index]; + var nodes = branch.getNodes(); + + var node = nodes.get(ingredient); + if (node == null) { + return null; + } + + GT_Recipe found; + var left = node.left(); + if (left != null) { + // if a recipe is in this node, end immediately with it as found + found = left; + } else { + var right = node.right(); + assert right != null; + + // recursion: remove the next ingredient up the trie + found = remove(recipe, ingredients, right, index + 1); + } + + if (found == recipe) { + // disassociate this step in the route + undoInsert(nodes, ingredient, index == ingredients.length - 1); + return found; + } else if (found != null) { + if (ConfigGeneral.loggingRecipes) { + GT_Log.recipe.printf("Failed to remove recipe %s%n, found: %s%n", recipe, found); + if (ConfigGeneral.loggingRecipesStackTrace) { + new Throwable().printStackTrace(GT_Log.err); + } + } + return null; + } + + return null; + } + + /** + * Reverts an insertion operation into a branch + * + * @param nodes the nodes to remove from + * @param ingredient the added ingredient to undo + * @param isLast if the ingredient was the final ingredient in the route + */ + private static void undoInsert(@NotNull Map> nodes, + @NotNull AbstractMapIngredient ingredient, boolean isLast) { + // undo the changes made + if (isLast) { + // last ingredient needs ingredient->recipe mapping removed + nodes.remove(ingredient); + } else { + TrieBranch branch = nodes.get(ingredient) + .right(); + if (branch != null && branch.isEmpty()) { + // remove added empty branches + nodes.remove(ingredient); + } + } + } + + /** + * Clear the Trie + */ + public void clear() { + this.rootBranch.getNodes() + .clear(); + } + + /** + * @return the amount of values in the trie + */ + public int size() { + return this.size; + } + + public @NotNull Stream findStream(@NotNull ItemStack @NotNull [] items, + @NotNull FluidStack @NotNull [] fluids, @NotNull Predicate predicate, boolean parallel) { + if (items.length == 0 && fluids.length == 0) { + return Stream.empty(); + } + + predicate = predicate.and(r -> r.isRecipeInputEqual(false, false, fluids, items)); + return StreamSupport.stream(new RecipeTrieSpliterator(this, toIngredients(items, fluids), predicate), parallel); + } + + /** + * Finds a recipe where the input quantities must sufficiently match + * + * @param items the items potentially in the recipe inputs + * @param fluids the fluids potentially in the recipe inputs + * @param predicate the predicate to determine if the recipe matches + * @return the recipe + */ + public @Nullable GT_Recipe findMatching(@NotNull ItemStack @NotNull [] items, + @NotNull FluidStack @NotNull [] fluids, @NotNull Predicate predicate) { + if (items.length == 0 && fluids.length == 0) { + return null; + } + + var ingredients = toIngredients(items, fluids); + predicate = predicate.and(r -> r.isRecipeInputEqual(false, false, fluids, items)); + return find(ingredients, predicate); + } + + /** + * @param items the items potentially in the recipe inputs + * @param fluids the fluids potentially in the recipe inputs + * @param predicate the predicate to determine if the recipe matches + * @return the recipe + */ + public @Nullable GT_Recipe find(@NotNull ItemHolder @NotNull [] items, @NotNull FluidStack @NotNull [] fluids, + @NotNull Predicate predicate) { + if (items.length == 0 && fluids.length == 0) { + return null; + } + + var ingredients = toIngredients(items, fluids); + return find(ingredients, predicate); + } + + /** + * @param ingredients the ingredients potentially in the recipe inputs + * @param predicate the predicate to determine if the recipe matches + * @return the recipe + */ + public @Nullable GT_Recipe find(@NotNull AbstractMapIngredient @NotNull [] @NotNull [] ingredients, + @NotNull Predicate predicate) { + BitSet skipList = new BitSet(); + for (int i = 0; i < ingredients.length; i++) { + skipList.set(i); + GT_Recipe recipe = find(ingredients, rootBranch, predicate, i, 0, skipList); + if (recipe != null) { + return recipe; + } + + skipList.clear(i); + } + + return null; + } + + /** + * @param items the items to convert + * @param fluids the fluids to convert + * @return an array of ingredients + */ + private @NotNull AbstractMapIngredient @NotNull [] @NotNull [] toIngredients(@NotNull ItemHolder @NotNull [] items, + @NotNull FluidStack @NotNull [] fluids) { + List list = new ArrayList<>(items.length + fluids.length); + for (ItemHolder stack : items) { + List inner = new ObjectArrayList<>(1); + // regular input + inner.add(MapIngredientFactory.from(stack)); + + if (hasOreDictInputs) { + for (int i : stack.getOreDictTagIDs()) { + inner.add(new MapOreDictIngredient(i)); + } + } + + if (hasNBTMatchInputs) { + // TODO partial nbt match + // inner.add(new MapPartialNBTIngredient(stack)); + } + inner.sort(MapIngredientComparator.INSTANCE); + list.add(inner.toArray(new AbstractMapIngredient[0])); + } + + for (FluidStack stack : fluids) { + list.add(new AbstractMapIngredient[] { new MapFluidStackIngredient(stack) }); + } + + return list.toArray(new AbstractMapIngredient[0][]); + } + + /** + * @param items the items to convert + * @param fluids the fluids to convert + * @return an array of ingredients + */ + private @NotNull AbstractMapIngredient @NotNull [] @NotNull [] toIngredients(@NotNull ItemStack @NotNull [] items, + @NotNull FluidStack @NotNull [] fluids) { + List list = new ArrayList<>(items.length + fluids.length); + for (ItemStack stack : items) { + List inner = new ObjectArrayList<>(1); + // regular input + inner.add(MapIngredientFactory.from(stack)); + + if (hasOreDictInputs) { + for (int i : OreDictionary.getOreIDs(stack)) { + inner.add(new MapOreDictIngredient(i)); + } + } + + if (hasNBTMatchInputs) { + // TODO partial nbt match + // inner.add(new MapPartialNBTIngredient(stack)); + } + inner.sort(MapIngredientComparator.INSTANCE); + list.add(inner.toArray(new AbstractMapIngredient[0])); + } + + for (FluidStack stack : fluids) { + list.add(new AbstractMapIngredient[] { new MapFluidStackIngredient(stack) }); + } + + return list.toArray(new AbstractMapIngredient[0][]); + } + + /** + * @param ingredients the ingredients potentially leading to a recipe + * @param branch the branch to search + * @param predicate the predicate to determine if a found recipe is valid + * @param index the ingredient index + * @return the found recipe + */ + private static @Nullable GT_Recipe find(@NotNull AbstractMapIngredient @NotNull [] @NotNull [] ingredients, + @NotNull TrieBranch branch, @NotNull Predicate predicate, int index, int count, + @NotNull BitSet skipList) { + if (count == ingredients.length) { + // ingredients exhausted + return null; + } + + for (var ingredient : ingredients[index]) { + var result = branch.getNodes() + .get(ingredient); + if (result == null) { + // no branch to continue with, try the next possible ingredient + continue; + } + + GT_Recipe recipe = result.left(); + if (recipe != null) { + if (predicate.test(recipe)) { + return recipe; + } + + // found a recipe, but the predicate fails, so look for more + continue; + } + + TrieBranch nextBranch = result.right(); + assert nextBranch != null; + + int i = (index + 1) % ingredients.length; + while (i != index) { + if (skipList.get(i)) { + i = (i + 1) % ingredients.length; + continue; + } + skipList.set(i); + + // recursion: try every unused ingredient as the next branch in the route + recipe = find(ingredients, nextBranch, predicate, i, count + 1, skipList); + skipList.clear(i); + + if (recipe != null) { + return recipe; + } + + i = (i + 1) % ingredients.length; + } + } + + return null; + } + + /** + * Exhaustively gathers all recipes that can be crafted with the given ingredients into a set. + * + * @param items the input items + * @param fluids the input fluids + * @return all the recipes that can be run with these inputs + */ + public @NotNull Set findAll(@NotNull ItemStack @NotNull [] items, + @NotNull FluidStack @NotNull [] fluids) { + Set set = new ObjectOpenHashSet<>(); + var ingredients = toIngredients(items, fluids); + BitSet skipList = new BitSet(); + + for (int i = 0; i < ingredients.length; i++) { + skipList.set(i); + findAll(ingredients, rootBranch, set, i, 0, skipList); + skipList.clear(i); + } + return set; + } + + /** + * @param ingredients the ingredients potentially leading to a recipe + * @param branch the branch to search + * @param set the set to store recipes in + * @param index the ingredient index + */ + private static void findAll(@NotNull AbstractMapIngredient @NotNull [] @NotNull [] ingredients, + @NotNull TrieBranch branch, @NotNull Set set, int index, int count, @NotNull BitSet skipList) { + if (count == ingredients.length) { + // ingredients exhausted + return; + } + + for (var ingredient : ingredients[index]) { + var result = branch.getNodes() + .get(ingredient); + if (result == null) { + continue; + } + + GT_Recipe recipe = result.left(); + if (recipe != null) { + set.add(recipe); + continue; + } + + TrieBranch nextBranch = result.right(); + assert nextBranch != null; + + int i = (index + 1) % ingredients.length; + while (i != index) { + if (skipList.get(i)) { + i = (i + 1) % ingredients.length; + continue; + } + skipList.set(i); + + // recursion: try every unused ingredient as the next branch in the route + findAll(ingredients, nextBranch, set, i, count + 1, skipList); + skipList.clear(i); + + i = (i + 1) % ingredients.length; + } + } + } +} diff --git a/src/main/java/gregtech/api/recipe/store/RecipeTrieCollisionFinder.java b/src/main/java/gregtech/api/recipe/store/RecipeTrieCollisionFinder.java new file mode 100644 index 00000000000..7c5af05fae7 --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/RecipeTrieCollisionFinder.java @@ -0,0 +1,156 @@ +package gregtech.api.recipe.store; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import gregtech.api.recipe.RecipeMap; +import gregtech.api.util.GT_Recipe; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +public final class RecipeTrieCollisionFinder { + + private static final Logger LOGGER = LogManager.getLogger("GregTech Recipe Checker"); + + private RecipeTrieCollisionFinder() {} + + /** + * Finds collisions in each RecipeMap's RecipeTrie + */ + public static void findCollisions() { + Object2ObjectMap, Object2ObjectMap>> mismatchedRecipes = new Object2ObjectOpenHashMap<>(); + Object2ObjectMap, Set> emptyInputRecipes = new Object2ObjectOpenHashMap<>(); + + LOGGER.info("Starting recipe issue checks..."); + for (var recipeMap : RecipeMap.ALL_RECIPE_MAPS.values()) { + mismatchedRecipes.put(recipeMap, new Object2ObjectOpenHashMap<>()); + emptyInputRecipes.put(recipeMap, new ObjectOpenHashSet<>()); + LOGGER.info("Checking Recipe Map: {}", recipeMap.unlocalizedName); + for (GT_Recipe currentRecipe : recipeMap.getAllRecipes()) { + List itemInputs = new ArrayList<>(); + for (ItemStack input : currentRecipe.mInputs) { + if (input == null) { + // check for any empty/null inputs + emptyInputRecipes.get(recipeMap) + .add(currentRecipe); + } else { + // set count to Integer.MAX_VALUE to detect conflicts which only occur in large batches + ItemStack stack = input.copy(); + stack.stackSize = Integer.MAX_VALUE; + itemInputs.add(stack); + } + } + + List fluidInputs = new ArrayList<>(); + for (FluidStack input : currentRecipe.mFluidInputs) { + if (input == null) { + // check for any empty/null inputs + emptyInputRecipes.get(recipeMap) + .add(currentRecipe); + } else { + // set count to Integer.MAX_VALUE to detect conflicts which only occur in large batches + fluidInputs.add(new FluidStack(input, Integer.MAX_VALUE)); + } + } + + Set collidingRecipes = recipeMap.getBackend() + .trie() + .findAll(itemInputs.toArray(new ItemStack[0]), fluidInputs.toArray(new FluidStack[0])); + + if (collidingRecipes.isEmpty()) { + LOGGER.error("Recipe had no matches for findRecipeCollisions: {}", currentRecipe); + continue; + } + + if (collidingRecipes.size() > 1) { + // remove the current recipe from the list of recipes, as it's not a conflict + collidingRecipes.remove(currentRecipe); + var conflicting = mismatchedRecipes.get(recipeMap); + + // if the conflict was iterated over before, and the current recipe is in the list, remove it + collidingRecipes.removeIf( + cf -> conflicting.get(cf) != null && conflicting.get(cf) + .contains(currentRecipe)); + + if (!collidingRecipes.isEmpty()) { + mismatchedRecipes.get(recipeMap) + .put(currentRecipe, collidingRecipes); + } + } + } + + if (mismatchedRecipes.get(recipeMap) + .isEmpty()) { + LOGGER.info("No mismatched recipes found for recipe map: {}", recipeMap.unlocalizedName); + mismatchedRecipes.remove(recipeMap); + } else { + LOGGER.error("Mismatched recipes found for recipe map: {}", recipeMap.unlocalizedName); + } + + if (emptyInputRecipes.get(recipeMap) + .isEmpty()) { + emptyInputRecipes.remove(recipeMap); + } else { + LOGGER.error("Recipes with empty inputs found in recipe map: {}", recipeMap.unlocalizedName); + } + } + + LOGGER.info("Completed recipe issue checks!"); + + int count = 0; + if (mismatchedRecipes.isEmpty()) { + LOGGER.info("No recipe conflicts found in all recipe maps!"); + } else { + count = (int) mismatchedRecipes.values() + .stream() + .mapToLong( + s -> s.values() + .stream() + .mapToLong(Set::size) + .sum()) + .sum(); + LOGGER.info("Found {} potential conflicts...", count); + for (var entry : mismatchedRecipes.entrySet()) { + LOGGER.error("\n[In Recipe map] : \"{}\"", entry.getKey().unlocalizedName); + for (var mismatch : mismatchedRecipes.get(entry.getKey()) + .entrySet()) { + StringBuilder conflictingRecipes = new StringBuilder(); + conflictingRecipes.append("\n[Tried matching]: ") + .append(mismatch.getKey()); + for (var additional : mismatch.getValue()) { + conflictingRecipes.append("\n[Also Found]: ") + .append(additional); + } + + LOGGER.error(conflictingRecipes.toString()); + } + } + } + + int emptyCount = 0; + if (!emptyInputRecipes.isEmpty()) { + emptyCount = (int) emptyInputRecipes.values() + .stream() + .mapToLong(Set::size) + .sum(); + LOGGER.info("Found {} recipes with empty inputs", emptyCount); + for (var recipeMap : emptyInputRecipes.entrySet()) { + LOGGER.error("\n[In Recipe map] : \"{}\"", recipeMap.getKey().unlocalizedName); + for (var recipe : recipeMap.getValue()) { + LOGGER.error("\n{}", recipe); + } + } + } + + LOGGER.info("Found {} conflicts", count); + LOGGER.info("Found {} empty inputs", emptyCount); + } +} diff --git a/src/main/java/gregtech/api/recipe/store/RecipeTrieSpliterator.java b/src/main/java/gregtech/api/recipe/store/RecipeTrieSpliterator.java new file mode 100644 index 00000000000..0d645f2c8ea --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/RecipeTrieSpliterator.java @@ -0,0 +1,227 @@ +package gregtech.api.recipe.store; + +import java.util.ArrayDeque; +import java.util.BitSet; +import java.util.Deque; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import gregtech.api.recipe.store.ingredient.AbstractMapIngredient; +import gregtech.api.util.GT_Recipe; + +// TODO not functional +final class RecipeTrieSpliterator implements Spliterator { + + private static final int CHARACTERISTICS = Spliterator.NONNULL | Spliterator.IMMUTABLE; + + private final RecipeTrie trie; + private final AbstractMapIngredient[][] ingredients; + private final Predicate predicate; + + private final Deque posStack = new ArrayDeque<>(); + private final Deque endStack = new ArrayDeque<>(); + private final Deque branchStack = new ArrayDeque<>(); + private final Deque depthStack = new ArrayDeque<>(); + private final Deque indexStack = new ArrayDeque<>(); + private final Deque skipListStack = new ArrayDeque<>(); + + RecipeTrieSpliterator(@NotNull RecipeTrie trie, @NotNull AbstractMapIngredient @NotNull [][] ingredients, + @NotNull Predicate predicate) { + this.trie = trie; + this.ingredients = ingredients; + this.predicate = predicate; + this.posStack.push(0); + this.endStack.push(ingredients.length == 0 ? 0 : ingredients[0].length); + this.branchStack.push(trie.rootBranch); + this.depthStack.push(0); + this.indexStack.push(0); + BitSet skipList = new BitSet(); + skipList.set(0); + this.skipListStack.push(skipList); + } + + private RecipeTrieSpliterator(@NotNull RecipeTrieSpliterator spliterator, int start, int end) { + this.trie = spliterator.trie; + this.ingredients = spliterator.ingredients; + this.predicate = spliterator.predicate; + this.posStack.addLast(start); + this.endStack.addLast(end); + this.branchStack.addLast(spliterator.branchStack.getFirst()); + this.depthStack.addLast(spliterator.depthStack.getFirst()); + this.indexStack.addLast(spliterator.indexStack.getFirst()); + this.skipListStack.addLast( + BitSet.valueOf( + spliterator.skipListStack.getFirst() + .toLongArray())); + } + + @Override + public boolean tryAdvance(Consumer action) { + GT_Recipe recipe = seek(); + if (recipe != null) { + action.accept(recipe); + return true; + } + + return false; + } + + /** + * @return the next recipe + */ + private @Nullable GT_Recipe seek() { + int depth = depthStack.getLast(); + if (depth == ingredients.length) { + return null; + } + + int index = indexStack.getLast(); + int end = endStack.getLast(); + TrieBranch branch = branchStack.getLast(); + var arr = ingredients[index]; + for (int i = posStack.getLast(); i < end; i++) { + var ingredient = arr[i]; + var result = branch.getNodes() + .get(ingredient); + if (result == null) { + // keep searching at this depth + continue; + } + + GT_Recipe recipe = result.left(); + if (recipe != null) { + if (predicate.test(recipe)) { + if (i + 1 < end) { + // store the position and branch to resume from next time + indexStack.addLast(index); + depthStack.addLast(depth); + endStack.addLast(end); + posStack.addLast(i + 1); // need to advance to the next position + branchStack.addLast(branch); + } else { + depthStack.removeLast(); + indexStack.removeLast(); + endStack.removeLast(); + branchStack.removeLast(); + posStack.removeLast(); + } + return recipe; + } + + // found a recipe, but the predicate fails, so look for more + continue; + } + + TrieBranch nextBranch = result.right(); + assert nextBranch != null; + + BitSet skipList = skipListStack.getLast(); + int j = (index + 1) % ingredients.length; + while (j != index) { + if (skipList.get(j)) { + j = (j + 1) % ingredients.length; + continue; + } + skipList.set(j); + + indexStack.addLast(j); + depthStack.addLast(depth + 1); + endStack.addLast(ingredients[j].length); + posStack.addLast(0); + branchStack.addLast(nextBranch); + skipListStack.addLast(BitSet.valueOf(skipList.toLongArray())); + + // recursion: try every unused ingredient as the next branch in the route + recipe = seek(); + skipList.clear(j); + + if (recipe != null) { + return recipe; + } + + j = (j + 1) % ingredients.length; + } + skipListStack.removeLast(); + } + + depthStack.removeLast(); + indexStack.removeLast(); + endStack.removeLast(); + branchStack.removeLast(); + posStack.removeLast(); + + return seek(); + } + + @Override + public @Nullable Spliterator trySplit() { + if (isEmpty()) { + return null; + } + + return trySplit( + new ArrayDeque<>(posStack), + new ArrayDeque<>(endStack), + new ArrayDeque<>(branchStack), + new ArrayDeque<>(depthStack), + new ArrayDeque<>(indexStack), + new ArrayDeque<>(skipListStack)); + } + + /** + * Recursively attempt to split the workload in half and give the other half to a new spliterator. + * + * @param poses the starting positions + * @param ends the end positions + * @param branches the branches + * @return a spliterator with half of the current one's work. + */ + private @Nullable Spliterator trySplit(@NotNull Deque poses, @NotNull Deque ends, + @NotNull Deque branches, @NotNull Deque depths, @NotNull Deque indices, + @NotNull Deque skipLists) { + if (isEmpty()) { + return null; + } + + int pos = poses.getFirst(); + int end = ends.getFirst(); + + int mid = (pos + end) >>> 1; + if (pos > mid) { + // cannot split here, split higher up + poses.removeFirst(); + ends.removeFirst(); + branches.removeFirst(); + depths.removeFirst(); + indices.removeFirst(); + skipLists.removeFirst(); + return trySplit(poses, ends, branches, depths, indices, skipLists); + } + + Spliterator spliterator = new RecipeTrieSpliterator(this, pos, mid); + this.posStack.addFirst(mid); // current starts in the middle now + return spliterator; + } + + private boolean isEmpty() { + return posStack.isEmpty() || endStack.isEmpty() + || branchStack.isEmpty() + || depthStack.isEmpty() + || indexStack.isEmpty() + || skipListStack.isEmpty(); + } + + @Override + public long estimateSize() { + return (long) (endStack.getLast() - posStack.getLast()) * depthStack.size(); + } + + @Override + public int characteristics() { + return CHARACTERISTICS; + } +} diff --git a/src/main/java/gregtech/api/recipe/store/TrieBranch.java b/src/main/java/gregtech/api/recipe/store/TrieBranch.java new file mode 100644 index 00000000000..2127825e59d --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/TrieBranch.java @@ -0,0 +1,36 @@ +package gregtech.api.recipe.store; + +import java.util.Map; +import java.util.stream.Stream; + +import org.jetbrains.annotations.NotNull; + +import gregtech.api.recipe.store.ingredient.AbstractMapIngredient; +import gregtech.api.util.GT_Recipe; +import gregtech.api.util.function.Either; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +final class TrieBranch { + + private Map> nodes; + + public @NotNull Stream getAll() { + if (nodes == null) { + return Stream.empty(); + } + return nodes.values() + .stream() + .flatMap(e -> e.map(Stream::of, TrieBranch::getAll)); + } + + public boolean isEmpty() { + return nodes == null || nodes.isEmpty(); + } + + public @NotNull Map> getNodes() { + if (nodes == null) { + nodes = new Object2ObjectOpenHashMap<>(2); + } + return nodes; + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/AbstractMapIngredient.java b/src/main/java/gregtech/api/recipe/store/ingredient/AbstractMapIngredient.java new file mode 100644 index 00000000000..f16f40ca6eb --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/AbstractMapIngredient.java @@ -0,0 +1,28 @@ +package gregtech.api.recipe.store.ingredient; + +public abstract class AbstractMapIngredient { + + private int hash; + private boolean hashed; + + protected AbstractMapIngredient() {} + + /** + * @return the sorting priority of this ingredient class + */ + public abstract int sortingPriority(); + + /** + * @return the hashcode of this ingredient + */ + protected abstract int hash(); + + @Override + public final int hashCode() { + if (!hashed) { + this.hash = hash(); + this.hashed = true; + } + return hash; + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapFluidStackIngredient.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapFluidStackIngredient.java new file mode 100644 index 00000000000..22b711e350f --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapFluidStackIngredient.java @@ -0,0 +1,49 @@ +package gregtech.api.recipe.store.ingredient; + +import java.util.Comparator; + +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidRegistry; +import net.minecraftforge.fluids.FluidStack; + +import org.jetbrains.annotations.NotNull; + +public final class MapFluidStackIngredient extends AbstractMapIngredient { + + public static final Comparator FLUID_COMPARATOR = Comparator.comparingInt(FluidRegistry::getFluidID); + public static final Comparator COMPARATOR = (a, b) -> FLUID_COMPARATOR + .compare(a.fluid, b.fluid); + + private final Fluid fluid; + + public MapFluidStackIngredient(@NotNull Fluid fluid) { + this.fluid = fluid; + } + + public MapFluidStackIngredient(@NotNull FluidStack stack) { + this(stack.getFluid()); + } + + @Override + public int sortingPriority() { + return 100; + } + + @Override + protected int hash() { + return fluid.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapFluidStackIngredient that)) return false; + + return fluid.equals(that.fluid); + } + + @Override + public String toString() { + return "MapFluidStackIngredient{" + FluidRegistry.getFluidName(fluid) + '}'; + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientComparator.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientComparator.java new file mode 100644 index 00000000000..1d900a7860a --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientComparator.java @@ -0,0 +1,39 @@ +package gregtech.api.recipe.store.ingredient; + +import java.util.Comparator; + +import org.jetbrains.annotations.NotNull; + +public final class MapIngredientComparator implements Comparator { + + public static final Comparator INSTANCE = new MapIngredientComparator(); + + private MapIngredientComparator() {} + + @Override + public int compare(@NotNull AbstractMapIngredient o1, @NotNull AbstractMapIngredient o2) { + int result = Integer.compare(o1.sortingPriority(), o2.sortingPriority()); + if (result != 0) { + return result; + } + + return compareSubClass(o1, o2); + } + + private static int compareSubClass(@NotNull AbstractMapIngredient o1, @NotNull AbstractMapIngredient o2) { + if (o1 instanceof MapItemStackIngredient ingredient) { + return MapItemStackIngredient.COMPARATOR.compare(ingredient, (MapItemStackIngredient) o2); + } + if (o1 instanceof MapItemStackNBTIngredient ingredient) { + return MapItemStackNBTIngredient.COMPARATOR.compare(ingredient, (MapItemStackNBTIngredient) o2); + } + if (o1 instanceof MapOreDictIngredient ingredient) { + return MapOreDictIngredient.COMPARATOR.compare(ingredient, (MapOreDictIngredient) o2); + } + if (o1 instanceof MapFluidStackIngredient ingredient) { + return MapFluidStackIngredient.COMPARATOR.compare(ingredient, (MapFluidStackIngredient) o2); + } + + throw new IllegalArgumentException("AbstractMapIngredient is of unknown class: " + o1.getClass()); + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientFactory.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientFactory.java new file mode 100644 index 00000000000..07890feffc4 --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapIngredientFactory.java @@ -0,0 +1,43 @@ +package gregtech.api.recipe.store.ingredient; + +import net.minecraft.item.ItemStack; + +import org.jetbrains.annotations.NotNull; + +import gregtech.api.util.item.ItemHolder; + +public final class MapIngredientFactory { + + private MapIngredientFactory() {} + + public static @NotNull AbstractMapIngredient from(@NotNull ItemStack stack) { + if (stack.hasTagCompound()) { + return new MapItemStackNBTIngredient(stack); + } + return new MapItemStackIngredient(stack); + } + + public static @NotNull AbstractMapIngredient from(@NotNull ItemHolder holder) { + if (holder.getNBT() != null) { + return new MapItemStackNBTIngredient(holder); + } + return new MapItemStackIngredient(holder); + } + + public static @NotNull AbstractMapIngredient from(@NotNull String oreDict) { + return new MapOreDictIngredient(oreDict); + } + + public static @NotNull AbstractMapIngredient from(@NotNull Object object) { + if (object instanceof ItemStack stack) { + return from(stack); + } + if (object instanceof ItemHolder holder) { + return from(holder); + } + if (object instanceof String string) { + return from(string); + } + throw new IllegalArgumentException("Ingredient " + object + " is of unknown type"); + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackIngredient.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackIngredient.java new file mode 100644 index 00000000000..3726b3e8df7 --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackIngredient.java @@ -0,0 +1,75 @@ +package gregtech.api.recipe.store.ingredient; + +import java.util.Comparator; +import java.util.Objects; + +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import org.jetbrains.annotations.NotNull; + +import gregtech.api.util.GT_Utility; +import gregtech.api.util.item.ItemHolder; + +/** + * Basic ingredient for Item and Metadata + */ +public final class MapItemStackIngredient extends AbstractMapIngredient { + + private static final Comparator CIRCUIT_COMPARATOR = (a, b) -> { + // circuits get priority as a trie search depth optimization + if (GT_Utility.isAnyIntegratedCircuit(a.item, a.meta)) { + if (!GT_Utility.isAnyIntegratedCircuit(b.item, b.meta)) { + return -1; + } + } else if (GT_Utility.isAnyIntegratedCircuit(b.item, b.meta)) { + return 1; + } + + return 0; + }; + + public static final Comparator COMPARATOR = CIRCUIT_COMPARATOR + .thenComparingInt((MapItemStackIngredient i) -> Item.getIdFromItem(i.item)) + .thenComparingInt((MapItemStackIngredient i) -> i.meta); + + private final Item item; + private final int meta; + + public MapItemStackIngredient(@NotNull ItemHolder holder) { + this(holder.getItem(), holder.getMeta()); + } + + public MapItemStackIngredient(@NotNull ItemStack stack) { + this(Objects.requireNonNull(stack.getItem()), Items.feather.getDamage(stack)); + } + + public MapItemStackIngredient(@NotNull Item item, int meta) { + this.item = item; + this.meta = meta; + } + + @Override + public int sortingPriority() { + return 10; + } + + @Override + protected int hash() { + return GT_Utility.stackHashCode(item, meta); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapItemStackIngredient that)) return false; + + return meta == that.meta && item.equals(that.item); + } + + @Override + public String toString() { + return "MapItemStackIngredient{" + new ItemStack(item, 1, meta) + '}'; + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackNBTIngredient.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackNBTIngredient.java new file mode 100644 index 00000000000..58a754e56de --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapItemStackNBTIngredient.java @@ -0,0 +1,63 @@ +package gregtech.api.recipe.store.ingredient; + +import java.util.Comparator; +import java.util.Objects; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import org.jetbrains.annotations.NotNull; + +import gregtech.api.util.GT_Utility; +import gregtech.api.util.item.ItemHolder; + +/** + * Basic ingredient for matching Item, Meta, and NBT tags + */ +public final class MapItemStackNBTIngredient extends AbstractMapIngredient { + + public static final Comparator COMPARATOR = Comparator + .comparingInt((MapItemStackNBTIngredient i) -> Item.getIdFromItem(i.item)) + .thenComparingInt((MapItemStackNBTIngredient i) -> i.meta) + .thenComparing((MapItemStackNBTIngredient i) -> i.nbt.toString()); + + private final Item item; + private final int meta; + private final NBTTagCompound nbt; + + public MapItemStackNBTIngredient(@NotNull ItemHolder holder) { + this(holder.getItem(), holder.getMeta(), Objects.requireNonNull(holder.getNBT(), "ItemHolder had no NBT")); + } + + public MapItemStackNBTIngredient(@NotNull ItemStack stack) { + this( + Objects.requireNonNull(stack.getItem()), + stack.getItemDamage(), + Objects.requireNonNull(stack.getTagCompound(), "ItemHolder had no NBT")); + } + + public MapItemStackNBTIngredient(@NotNull Item item, int meta, @NotNull NBTTagCompound nbt) { + this.item = item; + this.meta = meta; + this.nbt = nbt; + } + + @Override + public int sortingPriority() { + return 20; + } + + @Override + protected int hash() { + return GT_Utility.stackHashCode(item, meta) * 31 + nbt.hashCode(); // TODO something better? + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapItemStackNBTIngredient that)) return false; + + return meta == that.meta && item.equals(that.item) && nbt.equals(that.nbt); + } +} diff --git a/src/main/java/gregtech/api/recipe/store/ingredient/MapOreDictIngredient.java b/src/main/java/gregtech/api/recipe/store/ingredient/MapOreDictIngredient.java new file mode 100644 index 00000000000..219dfea0f25 --- /dev/null +++ b/src/main/java/gregtech/api/recipe/store/ingredient/MapOreDictIngredient.java @@ -0,0 +1,48 @@ +package gregtech.api.recipe.store.ingredient; + +import java.util.Comparator; + +import net.minecraftforge.oredict.OreDictionary; + +import org.jetbrains.annotations.NotNull; + +/** + * Basic ingredient for matching oredict + */ +public final class MapOreDictIngredient extends AbstractMapIngredient { + + public static final Comparator COMPARATOR = Comparator.comparingInt(i -> i.oreDict); + + private final int oreDict; + + public MapOreDictIngredient(int oreDict) { + this.oreDict = oreDict; + } + + public MapOreDictIngredient(@NotNull String oreDict) { + this(OreDictionary.getOreID(oreDict)); + } + + @Override + public int sortingPriority() { + return 50; + } + + @Override + protected int hash() { + return oreDict; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapOreDictIngredient that)) return false; + + return oreDict == that.oreDict; + } + + @Override + public String toString() { + return "MapOreDictIngredient{" + "oreDict=" + OreDictionary.getOreName(oreDict) + ", id=" + oreDict + '}'; + } +} diff --git a/src/main/java/gregtech/api/util/GT_Log.java b/src/main/java/gregtech/api/util/GT_Log.java index 2d00c2e061e..dc0715646ec 100644 --- a/src/main/java/gregtech/api/util/GT_Log.java +++ b/src/main/java/gregtech/api/util/GT_Log.java @@ -18,10 +18,12 @@ public class GT_Log { public static PrintStream ore = new LogBuffer(); public static PrintStream pal = null; public static PrintStream exp = new LogBuffer(); + public static PrintStream recipe = null; public static File mLogFile; public static File mOreDictLogFile; public static File mPlayerActivityLogFile; public static File mExplosionLog; + public static File mRecipeLog; public static class LogBuffer extends PrintStream { diff --git a/src/main/java/gregtech/api/util/GT_Recipe.java b/src/main/java/gregtech/api/util/GT_Recipe.java index c0812dcaef9..abb4dceb26c 100644 --- a/src/main/java/gregtech/api/util/GT_Recipe.java +++ b/src/main/java/gregtech/api/util/GT_Recipe.java @@ -912,6 +912,55 @@ public GT_Recipe setEUt(int aEUt) { return this; } + @Override + public String toString() { + return "GT_Recipe{" + "inputs=" + + Arrays.toString(mInputs) + + ", outputs=" + + Arrays.toString(mOutputs) + + ", fluidInputs=" + + Arrays.toString( + Arrays.stream(mFluidInputs) + .map( + fluidStack -> fluidStack.amount + "x" + + fluidStack.getUnlocalizedName() + + (fluidStack.tag != null ? "{" + fluidStack.tag + "}" : "")) + .toArray()) + + ", fluidOutputs=" + + Arrays.toString( + Arrays.stream(mFluidOutputs) + .map( + fluidStack -> fluidStack.amount + "x" + + fluidStack.getUnlocalizedName() + + (fluidStack.tag != null ? "{" + fluidStack.tag + "}" : "")) + .toArray()) + + ", chances=" + + Arrays.toString(mChances) + + ", specialItems=" + + mSpecialItems + + ", duration=" + + mDuration + + ", EUt=" + + mEUt + + ", specialValue=" + + mSpecialValue + + ", enabled=" + + mEnabled + + ", hidden=" + + mHidden + + ", isNBTSensitive=" + + isNBTSensitive + + ", metadataStorage=" + + metadataStorage + + ", recipeCategory=" + + recipeCategory + + ", owners=" + + owners + + ", fakeRecipe=" + + mFakeRecipe + + '}'; + } + public static class GT_Recipe_AssemblyLine { public static final ArrayList sAssemblylineRecipes = new ArrayList<>(); diff --git a/src/main/java/gregtech/api/util/GT_Utility.java b/src/main/java/gregtech/api/util/GT_Utility.java index 84e67ab7281..3673397d4c8 100644 --- a/src/main/java/gregtech/api/util/GT_Utility.java +++ b/src/main/java/gregtech/api/util/GT_Utility.java @@ -2349,6 +2349,10 @@ public static int itemToInt(Item aItem, int aMeta) { return Item.getIdFromItem(aItem) | (aMeta << 16); } + public static int stackHashCode(Item item, int meta) { + return item.hashCode() * 38197 + meta; + } + public static int stackToWildcard(ItemStack aStack) { if (isStackInvalid(aStack)) return 0; return Item.getIdFromItem(aStack.getItem()) | (W << 16); @@ -4622,8 +4626,11 @@ public static FluidStack convertCellToFluid(ItemStack itemStack) { public static boolean isAnyIntegratedCircuit(ItemStack itemStack) { if (itemStack == null) return false; - return itemStack.getItem() == ItemList.Circuit_Integrated.getItem() && 0 <= itemStack.getItemDamage() - && itemStack.getItemDamage() < 25; + return isAnyIntegratedCircuit(itemStack.getItem(), itemStack.getItemDamage()); + } + + public static boolean isAnyIntegratedCircuit(Item item, int meta) { + return item != null && item == ItemList.Circuit_Integrated.getItem() && 0 <= meta && meta < 25; } public static byte convertRatioToRedstone(long used, long max, int threshold, boolean inverted) { diff --git a/src/main/java/gregtech/api/util/function/Either.java b/src/main/java/gregtech/api/util/function/Either.java new file mode 100644 index 00000000000..03970d8259b --- /dev/null +++ b/src/main/java/gregtech/api/util/function/Either.java @@ -0,0 +1,99 @@ +package gregtech.api.util.function; + +import java.util.function.Function; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class Either { + + private Either() {} + + public abstract T map(@NotNull Function l, @NotNull Function r); + + public abstract @Nullable L left(); + + public abstract @Nullable R right(); + + public static @NotNull Either left(@NotNull L value) { + return new Left<>(value); + } + + public static @NotNull Either right(@NotNull R value) { + return new Right<>(value); + } + + private static final class Left extends Either { + + private final L value; + + private Left(@NotNull L value) { + this.value = value; + } + + @Override + public T map(@NotNull Function l, @NotNull Function r) { + return l.apply(value); + } + + @Override + public @NotNull L left() { + return value; + } + + @Override + public @Nullable R right() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Leftleft)) return false; + + return value.equals(left.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + private static final class Right extends Either { + + private final R value; + + private Right(@NotNull R value) { + this.value = value; + } + + @Override + public T map(@NotNull Function l, @NotNull Function r) { + return r.apply(value); + } + + @Override + public @Nullable L left() { + return null; + } + + @Override + public @NotNull R right() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Rightright)) return false; + + return value.equals(right.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } +} diff --git a/src/main/java/gregtech/api/util/item/ItemHolder.java b/src/main/java/gregtech/api/util/item/ItemHolder.java index 4675d0ba0e4..ab0229a8afa 100644 --- a/src/main/java/gregtech/api/util/item/ItemHolder.java +++ b/src/main/java/gregtech/api/util/item/ItemHolder.java @@ -47,14 +47,12 @@ public int[] getOreDictTagIDs() { public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof ItemHolder otherIH)) return false; - if (Arrays.stream(oreIDs) - .anyMatch(id -> { - for (int i = 0; i < otherIH.getOreDictTagIDs().length; i++) { - if (id == otherIH.getOreDictTagIDs()[i]) return true; + for (int id : oreIDs) { + for (int i = 0; i < otherIH.getOreDictTagIDs().length; i++) { + if (id == otherIH.getOreDictTagIDs()[i]) { + return true; } - return false; - })) { - return true; + } } if (item != otherIH.getItem() || meta != otherIH.getMeta()) { @@ -62,12 +60,12 @@ public boolean equals(Object other) { } if (this.tag == null && otherIH.getNBT() == null) return true; if (this.tag == null || otherIH.getNBT() == null) return false; - return this.tag.equals(otherIH); + return this.tag.equals(otherIH.tag); } @Override public int hashCode() { - return GT_Utility.stackToInt(toStack()); + return GT_Utility.itemToInt(item, meta); } @Nonnull @@ -76,4 +74,17 @@ private ItemStack toStack() { item.stackTagCompound = tag; return item; } + + @Override + public String toString() { + return "ItemHolder{" + "item=" + + item + + ", meta=" + + meta + + ", tag=" + + tag + + ", oreIDs=" + + Arrays.toString(oreIDs) + + '}'; + } } diff --git a/src/main/java/gregtech/common/config/gregtech/ConfigGeneral.java b/src/main/java/gregtech/common/config/gregtech/ConfigGeneral.java index e2ac011d39a..e5ec25d023e 100644 --- a/src/main/java/gregtech/common/config/gregtech/ConfigGeneral.java +++ b/src/main/java/gregtech/common/config/gregtech/ConfigGeneral.java @@ -251,4 +251,14 @@ public class ConfigGeneral { @Config.DefaultBoolean(true) @Config.RequiresMcRestart public static boolean loggingPlayerActicity; + + @Config.Comment("if true, log all the recipes in logs/GTRecipes.log.") + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public static boolean loggingRecipes; + + @Config.Comment("if true, log all the recipe stacktraces in logs/GTRecipes.log.") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean loggingRecipesStackTrace; } diff --git a/src/main/java/gregtech/loaders/preload/GT_PreLoad.java b/src/main/java/gregtech/loaders/preload/GT_PreLoad.java index a2e6137cff9..93be9ec8139 100644 --- a/src/main/java/gregtech/loaders/preload/GT_PreLoad.java +++ b/src/main/java/gregtech/loaders/preload/GT_PreLoad.java @@ -207,6 +207,18 @@ public static void createLogFiles(File parentFile) { GT_Log.pal = new PrintStream(GT_Log.mPlayerActivityLogFile); } catch (Throwable ignored) {} } + + if (ConfigGeneral.loggingRecipes) { + GT_Log.mRecipeLog = new File(parentFile, "logs/GTRecipes.log"); + if (!GT_Log.mRecipeLog.exists()) { + try { + GT_Log.mRecipeLog.createNewFile(); + } catch (Throwable ignored) {} + } + try { + GT_Log.recipe = new PrintStream(GT_Log.mRecipeLog); + } catch (Throwable ignored) {} + } } public static void runMineTweakerCompat() {