diff --git a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java index 79b207b44..d94287320 100644 --- a/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java +++ b/src/main/java/org/cyclops/integratedcrafting/api/crafting/CraftingJobDependencyGraph.java @@ -1,5 +1,6 @@ package org.cyclops.integratedcrafting.api.crafting; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -16,9 +17,9 @@ import javax.annotation.Nullable; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; /** * A CraftingJobDependencyGraph stores dependencies between crafting jobs based on their unique ID. @@ -52,11 +53,7 @@ public CraftingJob getCraftingJob(int id) { } public Collection getDependencies(CraftingJob craftingJob) { - return dependencies.getOrDefault(craftingJob.getId(), new IntArrayList()) - .stream() - .map(craftingJobs::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + return getCraftingJobs(dependencies.get(craftingJob.getId())); } public boolean hasDependencies(CraftingJob craftingJob) { @@ -77,11 +74,27 @@ public boolean hasDependencies(int craftingJobId) { } public Collection getDependents(CraftingJob craftingJob) { - return dependents.getOrDefault(craftingJob.getId(), new IntArrayList()) - .stream() - .map(craftingJobs::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + return getCraftingJobs(dependents.get(craftingJob.getId())); + } + + /** + * Resolve the given crafting job ids into their crafting jobs, skipping the ids that are unknown. + * @param craftingJobIds Crafting job ids, may be null if no ids are stored. + * @return A new collection with the resolved crafting jobs. + */ + protected Collection getCraftingJobs(@Nullable IntCollection craftingJobIds) { + if (craftingJobIds == null || craftingJobIds.isEmpty()) { + return Collections.emptyList(); + } + List resolved = Lists.newArrayListWithCapacity(craftingJobIds.size()); + IntIterator it = craftingJobIds.iterator(); + while (it.hasNext()) { + CraftingJob craftingJob = this.craftingJobs.get(it.nextInt()); + if (craftingJob != null) { + resolved.add(craftingJob); + } + } + return resolved; } public void addCraftingJobId(CraftingJob craftingJob) { diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index 187a84dcc..35cccac3a 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -37,7 +37,6 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.IntStream; /** * Helpers related to handling crafting jobs. @@ -986,6 +985,45 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora extractionMemoryReusable, collectMissingIngredients, recipeOutputQuantity, false); } + /** + * Check if the given storage has no capacity at all, and can therefore never contain anything. + * + * {@link IIngredientComponentStorage#getMaxQuantity()} is not a cheap call for a network channel: + * it aggregates the capacity of every storage position in the channel, + * which costs a capability lookup and a slot enumeration per position. + * A channel that is indexed and holds at least one instance is guaranteed to have capacity, + * so its index can answer this question without touching any position. + * + * @param storage A storage. + * @param The instance type. + * @param The matching condition parameter. + * @return If the storage has no capacity. + */ + protected static boolean hasNoStorageCapacity(IIngredientComponentStorage storage) { + if (storage instanceof IngredientChannelIndexed + && !((IngredientChannelIndexed) storage).getIndex().isEmpty()) { + return false; + } + return storage.getMaxQuantity() == 0; + } + + /** + * @param recipe A recipe. + * @param ingredientComponent An ingredient component type. + * @param The instance type. + * @param The matching condition parameter. + * @return If at least one of the recipe's inputs of the given component type is reusable. + */ + protected static boolean hasReusableInput(IRecipeDefinition recipe, IngredientComponent ingredientComponent) { + int inputCount = recipe.getInputs(ingredientComponent).size(); + for (int i = 0; i < inputCount; i++) { + if (recipe.isInputReusable(ingredientComponent, i)) { + return true; + } + } + return false; + } + public static Pair, MissingIngredients> getIngredientRecipeInputs(IIngredientComponentStorage storage, IngredientComponent ingredientComponent, IRecipeDefinition recipe, boolean simulate, @@ -997,10 +1035,9 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora // Quickly return if the storage is empty // We can't take this shortcut if we have a reusable ingredient AND extractionMemoryReusable is not empty - if (storage.getMaxQuantity() == 0 && + if (hasNoStorageCapacity(storage) && extractionMemoryReusable.isEmpty() && - IntStream.range(0, recipe.getInputs(ingredientComponent).size()) - .noneMatch(i -> recipe.isInputReusable(ingredientComponent, i))) { + !hasReusableInput(recipe, ingredientComponent)) { if (collectMissingIngredients) { List> recipeInputs = recipe.getInputs(ingredientComponent); MissingIngredients missing = new MissingIngredients<>(recipeInputs.stream().map(IPrototypedIngredientAlternatives::getAlternatives) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index e72a8d591..f38d56de7 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -429,10 +429,10 @@ public void update(INetwork network, int channel, PartPos targetPos) { // Notify the network of finalized crafting jobs if (finishedCraftingJobs.size() > 0) { + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); for (CraftingJob finishedCraftingJob : finishedCraftingJobs.values()) { if (finishedCraftingJob.getAmount() == 0) { // If the job is fully finished, remove it from the network - ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); craftingNetwork.onCraftingJobFinished(finishedCraftingJob); allCraftingJobs.remove(finishedCraftingJob.getId()); nonBlockingJobsRunningAmount.remove(finishedCraftingJob.getId()); @@ -471,7 +471,9 @@ public void update(INetwork network, int channel, PartPos targetPos) { } } - if (processingJobs < this.maxProcessingJobs) { + // Only look for a job to start if this handler has room for one, and has something to start. + // Skipping this block for an idle handler avoids a crafting network lookup for every idle tick. + if (processingJobs < this.maxProcessingJobs && !this.pendingCraftingJobs.isEmpty()) { // Handle crafting jobs CraftingJob startingCraftingJob = null; ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java index d1310ca73..b3a8e1d8e 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobIndexDefault.java @@ -9,6 +9,7 @@ import org.cyclops.cyclopscore.datastructure.MultitransformIterator; import org.cyclops.cyclopscore.ingredient.collection.IIngredientMapMutable; import org.cyclops.cyclopscore.ingredient.collection.IngredientHashMap; +import org.cyclops.cyclopscore.ingredient.collection.IngredientMapSingleClassified; import org.cyclops.integratedcrafting.api.crafting.CraftingJob; import org.cyclops.integratedcrafting.api.recipe.ICraftingJobIndex; import org.cyclops.integratedcrafting.api.recipe.ICraftingJobIndexModifiable; @@ -57,7 +58,13 @@ public CraftingJob getCraftingJob(int craftingJobId) { @Nullable protected IIngredientMapMutable> initializeIndex(IngredientComponent recipeComponent) { - return new IngredientHashMap<>(recipeComponent); + // Classify by the component's primary category, just like RecipeIndexDefault does. + // Lookups in this index are done with quantity-less match conditions, + // which a plain hash map can only answer by filtering over every indexed crafting job. + if (recipeComponent.getCategoryTypes().size() == 1) { + return new IngredientHashMap<>(recipeComponent); + } + return new IngredientMapSingleClassified<>(recipeComponent, () -> new IngredientHashMap<>(recipeComponent), recipeComponent.getCategoryTypes().get(0)); } @Override diff --git a/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java b/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java index 484d76038..492d6c4c9 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/crafting/processoverride/CraftingProcessOverrideCraftingTable.java @@ -60,11 +60,14 @@ public boolean craft(Function, PartPos> targetGetter, CraftingInput gridInput = gridFull.asCraftInput(); Level level = target.getPos().getLevel(true); - return CraftingHelpers.findServerRecipe(RecipeType.CRAFTING, gridInput, level) + // Look the recipe up via the cache, as an uncached lookup is a linear scan over every crafting recipe. + // This method is called at least twice for every craft: + // once to simulate the craft, and once to actually perform it. + return CraftingHelpers.findRecipeCached(RecipeType.CRAFTING, gridInput, level, false) .or(() -> { try { CraftingGrid gridSmall = new CraftingGrid(ingredients, 2, 2); - return CraftingHelpers.findServerRecipe(RecipeType.CRAFTING, gridSmall.asCraftInput(), level); + return CraftingHelpers.findRecipeCached(RecipeType.CRAFTING, gridSmall.asCraftInput(), level, false); } catch (IllegalArgumentException e) { // This can occur if the ingredients don't fit in a 2x2 grid. return Optional.empty();