• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyclopsMC / IntegratedCrafting / #479011831

31 Dec 2025 10:58AM UTC coverage: 23.802% (-0.2%) from 24.029%
#479011831

push

github

rubensworks
Avoid need for running synchronous observers

Instead, we now run crafting results through crafting job completion
logic, before flushing it to the network and relying on observer logic.

This used to cause performance issues.

Related to #112

0 of 73 new or added lines in 3 files covered. (0.0%)

71 existing lines in 4 files now uncovered.

755 of 3172 relevant lines covered (23.8%)

0.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.86
/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java
1
package org.cyclops.integratedcrafting.core;
2

3
import com.google.common.collect.Iterables;
4
import com.google.common.collect.Lists;
5
import com.google.common.collect.Maps;
6
import com.google.common.collect.Sets;
7
import net.minecraft.core.Direction;
8
import net.minecraft.world.level.block.entity.BlockEntity;
9
import net.minecraftforge.common.capabilities.ICapabilityProvider;
10
import net.minecraftforge.common.util.LazyOptional;
11
import org.apache.commons.lang3.tuple.Pair;
12
import org.apache.logging.log4j.Level;
13
import org.cyclops.commoncapabilities.api.capability.recipehandler.IPrototypedIngredientAlternatives;
14
import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition;
15
import org.cyclops.commoncapabilities.api.ingredient.*;
16
import org.cyclops.commoncapabilities.api.ingredient.storage.IIngredientComponentStorage;
17
import org.cyclops.commoncapabilities.api.ingredient.storage.IngredientComponentStorageEmpty;
18
import org.cyclops.cyclopscore.helper.BlockEntityHelpers;
19
import org.cyclops.cyclopscore.ingredient.collection.*;
20
import org.cyclops.cyclopscore.ingredient.storage.IngredientComponentStorageSlottedCollectionWrapper;
21
import org.cyclops.integratedcrafting.Capabilities;
22
import org.cyclops.integratedcrafting.IntegratedCrafting;
23
import org.cyclops.integratedcrafting.api.crafting.*;
24
import org.cyclops.integratedcrafting.api.network.ICraftingNetwork;
25
import org.cyclops.integratedcrafting.api.recipe.IRecipeIndex;
26
import org.cyclops.integratedcrafting.capability.network.CraftingNetworkConfig;
27
import org.cyclops.integrateddynamics.IntegratedDynamics;
28
import org.cyclops.integrateddynamics.api.PartStateException;
29
import org.cyclops.integrateddynamics.api.ingredient.capability.IPositionedAddonsNetworkIngredientsHandler;
30
import org.cyclops.integrateddynamics.api.network.INetwork;
31
import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients;
32
import org.cyclops.integrateddynamics.api.part.PartPos;
33
import org.cyclops.integrateddynamics.core.helper.NetworkHelpers;
34
import org.cyclops.integrateddynamics.core.network.IngredientChannelAdapter;
35
import org.cyclops.integrateddynamics.core.network.IngredientChannelIndexed;
36

37
import javax.annotation.Nullable;
38
import java.util.*;
39
import java.util.function.Function;
40
import java.util.stream.Collectors;
41
import java.util.stream.IntStream;
42

43
/**
44
 * Helpers related to handling crafting jobs.
45
 * @author rubensworks
46
 */
47
public class CraftingHelpers {
×
48

49
    /**
50
     * Get the network at the given position,
51
     * or throw a PartStateException if it is null.
52
     * @param pos A position.
53
     * @return A network.
54
     * @throws PartStateException If the network could not be found.
55
     */
56
    public static INetwork getNetworkChecked(PartPos pos) throws PartStateException {
57
        INetwork network = NetworkHelpers.getNetwork(pos.getPos().getLevel(true), pos.getPos().getBlockPos(), pos.getSide()).orElse(null);
×
58
        if (network == null) {
×
59
            IntegratedDynamics.clog(Level.ERROR, "Could not get the network for transfer as no network was found.");
×
60
            throw new PartStateException(pos.getPos(), pos.getSide());
×
61
        }
62
        return network;
×
63
    }
64

65
    /**
66
     * Get the crafting network in the given network.
67
     * @param network A network.
68
     * @return The crafting network.
69
     */
70
    public static LazyOptional<ICraftingNetwork> getCraftingNetwork(@Nullable INetwork network) {
71
        if (network != null) {
×
72
            return network.getCapability(CraftingNetworkConfig.CAPABILITY);
×
73
        }
74
        return null;
×
75
    }
76

77
    /**
78
     * Get the crafting network in the given network.
79
     * @param network A network.
80
     * @return The crafting network.
81
     */
82
    public static ICraftingNetwork getCraftingNetworkChecked(@Nullable INetwork network) {
83
        return getCraftingNetwork(network)
×
84
                .orElseThrow(() -> new IllegalStateException("Could not find a crafting network"));
×
85
    }
86

87
    /**
88
     * Get the storage network of the given type in the given network.
89
     * @param network A network.
90
     * @param ingredientComponent The ingredient component type of the network.
91
     * @param <T> The instance type.
92
     * @param <M> The matching condition parameter.
93
     * @return The storage network.
94
     */
95
    public static <T, M> LazyOptional<IPositionedAddonsNetworkIngredients<T, M>> getIngredientsNetwork(INetwork network,
96
                                                                                                       IngredientComponent<T, M> ingredientComponent) {
97
        IPositionedAddonsNetworkIngredientsHandler<T, M> ingredientsHandler = ingredientComponent.getCapability(Capabilities.POSITIONED_ADDONS_NETWORK_INGREDIENTS_HANDLER).orElse(null);
×
98
        if (ingredientsHandler != null) {
×
99
            return ingredientsHandler.getStorage(network);
×
100
        }
101
        return LazyOptional.empty();
×
102
    }
103

104
    /**
105
     * Get the storage network of the given type in the given network.
106
     * @param network A network.
107
     * @param ingredientComponent The ingredient component type of the network.
108
     * @param <T> The instance type.
109
     * @param <M> The matching condition parameter.
110
     * @return The storage network.
111
     */
112
    public static <T, M> IPositionedAddonsNetworkIngredients<T, M> getIngredientsNetworkChecked(INetwork network,
113
                                                                                                IngredientComponent<T, M> ingredientComponent) {
114
        return getIngredientsNetwork(network, ingredientComponent)
×
115
                .orElseThrow(() -> new IllegalStateException("Could not find an ingredients network"));
×
116
    }
117

118
    /**
119
     * Get the storage of the given ingredient component type from the network.
120
     * @param network The network.
121
     * @param channel A network channel.
122
     * @param ingredientComponent The ingredient component type of the network.
123
     * @param scheduleObservation If an observation inside the ingredients network should be scheduled.
124
     * @param <T> The instance type.
125
     * @param <M> The matching condition parameter.
126
     * @return The storage.
127
     */
128
    public static <T, M> IIngredientComponentStorage<T, M> getNetworkStorage(INetwork network, int channel,
129
                                                                             IngredientComponent<T, M> ingredientComponent,
130
                                                                             boolean scheduleObservation) {
131
        IPositionedAddonsNetworkIngredients<T, M> ingredientsNetwork = getIngredientsNetwork(network, ingredientComponent).orElse(null);
×
132
        if (ingredientsNetwork != null) {
×
133
            if (scheduleObservation) {
×
134
                ingredientsNetwork.scheduleObservation();
×
135
            }
136
            return ingredientsNetwork.getChannel(channel);
×
137
        }
138
        return new IngredientComponentStorageEmpty<>(ingredientComponent);
×
139
    }
140

141
    /**
142
     * Calculate the required crafting jobs and their dependencies for the given instance in the given network.
143
     * @param network The target network.
144
     * @param channel The target channel.
145
     * @param ingredientComponent The ingredient component type of the instance.
146
     * @param instance The instance to craft.
147
     * @param matchCondition The match condition of the instance.
148
     * @param craftMissing If the missing required ingredients should also be crafted.
149
     * @param identifierGenerator identifierGenerator An ID generator for crafting jobs.
150
     * @param craftingJobsGraph The target graph where all dependencies will be stored.
151
     * @param collectMissingRecipes If the missing recipes should be collected inside
152
     *                              {@link UnknownCraftingRecipeException}.
153
     *                              This may slow down calculation for deeply nested recipe graphs.
154
     * @param <T> The instance type.
155
     * @param <M> The matching condition parameter.
156
     * @return The crafting job for the given instance.
157
     * @throws UnknownCraftingRecipeException If the recipe for a (sub)ingredient is unavailable.
158
     * @throws RecursiveCraftingRecipeException If an infinite recursive recipe was detected.
159
     */
160
    public static <T, M> CraftingJob calculateCraftingJobs(INetwork network, int channel,
161
                                                           IngredientComponent<T, M> ingredientComponent,
162
                                                           T instance, M matchCondition, boolean craftMissing,
163
                                                           IIdentifierGenerator identifierGenerator,
164
                                                           CraftingJobDependencyGraph craftingJobsGraph,
165
                                                           boolean collectMissingRecipes)
166
            throws UnknownCraftingRecipeException, RecursiveCraftingRecipeException {
167
        ICraftingNetwork craftingNetwork = getCraftingNetworkChecked(network);
×
168
        IRecipeIndex recipeIndex = craftingNetwork.getRecipeIndex(channel);
×
169
        Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter = getNetworkStorageGetter(network, channel, true);
×
170

UNCOV
171
        CraftingJob craftingJob = calculateCraftingJobs(recipeIndex, channel, storageGetter, ingredientComponent, instance, matchCondition,
×
172
                craftMissing, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), identifierGenerator, craftingJobsGraph, Sets.newHashSet(),
×
173
                collectMissingRecipes);
174
        craftingJobsGraph.addCraftingJobId(craftingJob);
×
175
        return craftingJob;
×
176
    }
177

178
    /**
179
     * Calculate the required crafting jobs and their dependencies for the given instance in the given network.
180
     * @param network The target network.
181
     * @param channel The target channel.
182
     * @param recipe The recipe to calculate a job for.
183
     * @param amount The amount of times the recipe should be crafted.
184
     * @param craftMissing If the missing required ingredients should also be crafted.
185
     * @param identifierGenerator identifierGenerator An ID generator for crafting jobs.
186
     * @param craftingJobsGraph The target graph where all dependencies will be stored.
187
     * @param collectMissingRecipes If the missing recipes should be collected inside
188
     *                              {@link FailedCraftingRecipeException}.
189
     *                              This may slow down calculation for deeply nested recipe graphs.
190
     * @return The crafting job for the given instance.
191
     * @throws FailedCraftingRecipeException If the recipe could not be crafted due to missing sub-dependencies.
192
     * @throws RecursiveCraftingRecipeException If an infinite recursive recipe was detected.
193
     */
194
    public static CraftingJob calculateCraftingJobs(INetwork network, int channel,
195
                                                    IRecipeDefinition recipe, int amount, boolean craftMissing,
196
                                                    IIdentifierGenerator identifierGenerator,
197
                                                    CraftingJobDependencyGraph craftingJobsGraph,
198
                                                    boolean collectMissingRecipes)
199
            throws FailedCraftingRecipeException, RecursiveCraftingRecipeException {
200
        ICraftingNetwork craftingNetwork = getCraftingNetworkChecked(network);
×
201
        IRecipeIndex recipeIndex = craftingNetwork.getRecipeIndex(channel);
×
202
        Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter = getNetworkStorageGetter(network, channel, true);
×
203

UNCOV
204
        PartialCraftingJobCalculation result = calculateCraftingJobs(recipeIndex, channel, storageGetter, recipe, amount,
×
205
                craftMissing, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), identifierGenerator, craftingJobsGraph, Sets.newHashSet(),
×
206
                collectMissingRecipes);
207
        if (result.getCraftingJob() == null) {
×
208
            throw new FailedCraftingRecipeException(recipe, amount, result.getMissingDependencies(),
×
209
                    compressMixedIngredients(new MixedIngredients(result.getIngredientsStorage())), result.getPartialCraftingJobs());
×
210
        } else {
211
            craftingJobsGraph.addCraftingJobId(result.getCraftingJob());
×
212
            return result.getCraftingJob();
×
213
        }
214
    }
215

216
    /**
217
     * @return An identifier generator for crafting jobs.
218
     */
219
    public static IIdentifierGenerator getGlobalCraftingJobIdentifier() {
220
        return () -> IntegratedCrafting.globalCounters.getNext("craftingJob");
×
221
    }
222

223
    /**
224
     * Calculate the effective quantity for the given instance in the output of the given recipe.
225
     * @param recipe A recipe.
226
     * @param ingredientComponent The ingredient component.
227
     * @param instance An instance.
228
     * @param matchCondition A match condition.
229
     * @param <T> The instance type.
230
     * @param <M> The matching condition parameter.
231
     * @return The effective quantity.
232
     */
233
    public static <T, M> long getOutputQuantityForRecipe(IRecipeDefinition recipe,
234
                                                         IngredientComponent<T, M> ingredientComponent,
235
                                                         T instance, M matchCondition) {
236
        IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
1✔
237
        return recipe.getOutput().getInstances(ingredientComponent)
1✔
238
                .stream()
1✔
239
                .filter(i -> matcher.matches(i, instance, matchCondition))
1✔
240
                .mapToLong(matcher::getQuantity)
1✔
241
                .sum();
1✔
242
    }
243

244
    /**
245
     * Calculate a crafting job for the given instance.
246
     *
247
     * This method is merely an easily-stubbable implementation for the method above,
248
     * to simplify unit testing.
249
     *
250
     * @param recipeIndex The recipe index.
251
     * @param channel The target channel that will be stored in created crafting jobs.
252
     * @param storageGetter A callback function to get a storage for the given ingredient component.
253
     * @param ingredientComponent The ingredient component type of the instance.
254
     * @param instance The instance to craft.
255
     * @param matchCondition The match condition of the instance.
256
     * @param craftMissing If the missing required ingredients should also be crafted.
257
     * @param simulatedExtractionMemory This map remembers all extracted instances in simulation mode.
258
     *                                  This is to make sure that instances can not be extracted multiple times
259
     *                                  when simulating.
260
     * @param extractionMemoryReusable Like simulatedExtractionMemory, but it stores the reusable ingredients.
261
     * @param identifierGenerator An ID generator for crafting jobs.
262
     * @param craftingJobsGraph The target graph where all dependencies will be stored.
263
     * @param parentDependencies A set of parent recipe dependencies that are pending.
264
     *                           This is used to check for infinite recursion in recipes.
265
     * @param collectMissingRecipes If the missing recipes should be collected inside
266
     *                              {@link UnknownCraftingRecipeException}.
267
     *                              This may slow down calculation for deeply nested recipe graphs.
268
     * @param <T> The instance type.
269
     * @param <M> The matching condition parameter.
270
     * @return The crafting job for the given instance.
271
     * @throws UnknownCraftingRecipeException If the recipe for a (sub)ingredient is unavailable.
272
     * @throws RecursiveCraftingRecipeException If an infinite recursive recipe was detected.
273
     */
274
    protected static <T, M> CraftingJob calculateCraftingJobs(IRecipeIndex recipeIndex, int channel,
275
                                                              Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
276
                                                              IngredientComponent<T, M> ingredientComponent,
277
                                                              T instance, M matchCondition, boolean craftMissing,
278
                                                              Map<IngredientComponent<?, ?>,
279
                                                                      IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemory,
280
                                                              Map<IngredientComponent<?, ?>,
281
                                                                      IIngredientCollectionMutable<?, ?>> extractionMemoryReusable,
282
                                                              IIdentifierGenerator identifierGenerator,
283
                                                              CraftingJobDependencyGraph craftingJobsGraph,
284
                                                              Set<IPrototypedIngredient> parentDependencies,
285
                                                              boolean collectMissingRecipes)
286
            throws UnknownCraftingRecipeException, RecursiveCraftingRecipeException {
287
        IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
1✔
288
        // This matching condition makes it so that the recipe output does not have to match with the requested input by quantity.
289
        M quantifierlessCondition = matcher.withoutCondition(matchCondition,
1✔
290
                ingredientComponent.getPrimaryQuantifier().getMatchCondition());
1✔
291
        long instanceQuantity = matcher.getQuantity(instance);
1✔
292

293
        // Loop over all available recipes, and return the first valid one.
294
        Iterator<IRecipeDefinition> recipes = recipeIndex.getRecipes(ingredientComponent, instance, quantifierlessCondition);
1✔
295
        List<UnknownCraftingRecipeException> firstMissingDependencies = Lists.newArrayList();
1✔
296
        Map<IngredientComponent<?, ?>, List<?>> firstIngredientsStorage = Collections.emptyMap();
1✔
297
        List<CraftingJob> firstPartialCraftingJobs = Lists.newArrayList();
1✔
298
        RecursiveCraftingRecipeException firstRecursiveException = null;
1✔
299
        while (recipes.hasNext()) {
1✔
300
            IRecipeDefinition recipe = recipes.next();
1✔
301

302
            // Calculate the quantity for the given instance that the recipe outputs
303
            long recipeOutputQuantity = getOutputQuantityForRecipe(recipe, ingredientComponent, instance, quantifierlessCondition);
1✔
304
            // Based on the quantity of the recipe output, calculate the amount of required recipe jobs.
305
            int amount = (int) Math.ceil(((float) instanceQuantity) / (float) recipeOutputQuantity);
1✔
306

307
            // Calculate jobs for the given recipe
308
            try {
309
                PartialCraftingJobCalculation result = calculateCraftingJobs(recipeIndex, channel,
1✔
310
                        storageGetter, recipe, amount, craftMissing,
311
                        simulatedExtractionMemory, extractionMemoryReusable, identifierGenerator, craftingJobsGraph, parentDependencies,
312
                        collectMissingRecipes && firstMissingDependencies.isEmpty());
1✔
313
                if (result.getCraftingJob() == null) {
1✔
314
                    firstMissingDependencies = result.getMissingDependencies();
1✔
315
                    firstIngredientsStorage = result.getIngredientsStorage();
1✔
316
                    if (result.getPartialCraftingJobs() != null) {
1✔
317
                        firstPartialCraftingJobs = result.getPartialCraftingJobs();
1✔
318
                    }
319
                } else {
320
                    return result.getCraftingJob();
1✔
321
                }
322
            } catch (RecursiveCraftingRecipeException e) {
1✔
323
                if (firstRecursiveException == null) {
1✔
324
                    firstRecursiveException = e;
1✔
325
                }
326
                continue;
1✔
327
            }
1✔
328
        }
1✔
329

330
        if (firstRecursiveException != null) {
1✔
331
            throw firstRecursiveException;
1✔
332
        }
333

334
        // No valid recipes were available, so we error or collect the missing instance.
335
        throw new UnknownCraftingRecipeException(new PrototypedIngredient<>(ingredientComponent, instance, matchCondition),
1✔
336
                matcher.getQuantity(instance), firstMissingDependencies, compressMixedIngredients(new MixedIngredients(firstIngredientsStorage)),
1✔
337
                firstPartialCraftingJobs);
338
    }
339

340
    /**
341
     * Calculate a crafting job for the given recipe.
342
     *
343
     * This method is merely an easily-stubbable implementation for the method above,
344
     * to simplify unit testing.
345
     *
346
     * @param recipeIndex The recipe index.
347
     * @param channel The target channel that will be stored in created crafting jobs.
348
     * @param storageGetter A callback function to get a storage for the given ingredient component.
349
     * @param recipe The recipe to calculate a job for.
350
     * @param amount The amount of times the recipe should be crafted.
351
     * @param craftMissing If the missing required ingredients should also be crafted.
352
     * @param simulatedExtractionMemory This map remembers all extracted instances in simulation mode.
353
     *                                  This is to make sure that instances can not be extracted multiple times
354
     *                                  when simulating.
355
     * @param extractionMemoryReusable Like simulatedExtractionMemory, but it stores the reusable ingredients.
356
     * @param identifierGenerator An ID generator for crafting jobs.
357
     * @param craftingJobsGraph The target graph where all dependencies will be stored.
358
     * @param parentDependencies A set of parent recipe dependencies that are pending.
359
     *                           This is used to check for infinite recursion in recipes.
360
     * @param collectMissingRecipes If the missing recipes should be collected inside
361
     *                              {@link UnknownCraftingRecipeException}.
362
     *                              This may slow down calculation for deeply nested recipe graphs.
363
     * @return The crafting job for the given instance.
364
     * @throws RecursiveCraftingRecipeException If an infinite recursive recipe was detected.
365
     */
366
    protected static PartialCraftingJobCalculation calculateCraftingJobs(
367
            IRecipeIndex recipeIndex, int channel,
368
            Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
369
            IRecipeDefinition recipe, int amount, boolean craftMissing,
370
            Map<IngredientComponent<?, ?>,
371
                    IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemory,
372
            Map<IngredientComponent<?, ?>,
373
                    IIngredientCollectionMutable<?, ?>> extractionMemoryReusable,
374
            IIdentifierGenerator identifierGenerator,
375
            CraftingJobDependencyGraph craftingJobsGraph,
376
            Set<IPrototypedIngredient> parentDependencies,
377
            boolean collectMissingRecipes)
378
            throws RecursiveCraftingRecipeException {
379
        List<UnknownCraftingRecipeException> missingDependencies = Lists.newArrayList();
1✔
380
        List<CraftingJob> partialCraftingJobs = Lists.newArrayList();
1✔
381

382
        // Check if all requirements are met for this recipe, if so return directly (don't schedule yet)
383
        Pair<Map<IngredientComponent<?, ?>, List<?>>, Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>>> simulation =
1✔
384
                getRecipeInputs(storageGetter, recipe, true, simulatedExtractionMemory, extractionMemoryReusable,
1✔
385
                        true, amount);
386
        Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>> missingIngredients = simulation.getRight();
1✔
387
        if (!craftMissing && !missingIngredients.isEmpty()) {
1✔
388
            if (collectMissingRecipes) {
1✔
389
                // Collect missing ingredients as missing recipes when we don't want to craft sub-components,
390
                // but they are missing.
391
                for (Map.Entry<IngredientComponent<?, ?>, MissingIngredients<?, ?>> entry : missingIngredients.entrySet()) {
1✔
392
                    for (MissingIngredients.Element<?, ?> element : entry.getValue().getElements()) {
1✔
393
                        MissingIngredients.PrototypedWithRequested<?, ?> alternative = element.getAlternatives().get(0);
1✔
394

395
                        // Calculate the instance that was available in storage
396
                        IngredientComponent<?, ?> component = alternative.getRequestedPrototype().getComponent();
1✔
397
                        IIngredientMatcher matcher = component.getMatcher();
1✔
398
                        long storedQuantity = matcher.getQuantity(alternative.getRequestedPrototype().getPrototype()) - alternative.getQuantityMissing();
1✔
399
                        Map<IngredientComponent<?, ?>, List<?>> storageMap;
400
                        if (storedQuantity > 0) {
1✔
401
                            storageMap = Maps.newIdentityHashMap();
×
402
                            storageMap.put(component, Collections.singletonList(
×
403
                                    matcher.withQuantity(alternative.getRequestedPrototype().getPrototype(), storedQuantity)
×
404
                            ));
405
                        } else {
406
                            storageMap = Collections.emptyMap();
1✔
407
                        }
408

409
                        missingDependencies.add(new UnknownCraftingRecipeException(
1✔
410
                                alternative.getRequestedPrototype(), alternative.getQuantityMissing(),
1✔
411
                                Collections.emptyList(), compressMixedIngredients(new MixedIngredients(storageMap)), Lists.newArrayList()));
1✔
412
                    }
1✔
413
                }
1✔
414
            }
415
            return new PartialCraftingJobCalculation(null, missingDependencies, simulation.getLeft(), null);
1✔
416
        }
417

418
        // For all missing ingredients, recursively call this method for all missing items, and add as dependencies
419
        // We store dependencies as a mapping from recipe to job,
420
        // so that only one job exist per unique recipe,
421
        // so that a job amount can be incremented once another equal recipe is found.
422
        Map<IRecipeDefinition, CraftingJob> dependencies = Maps.newHashMapWithExpectedSize(missingIngredients.size());
1✔
423
        Map<IngredientComponent<?, ?>, IngredientCollectionPrototypeMap<?, ?>> dependenciesOutputSurplus = Maps.newIdentityHashMap();
1✔
424
        // We must be able to find crafting jobs for all dependencies
425
        for (IngredientComponent dependencyComponent : missingIngredients.keySet()) {
1✔
426
            try {
427
                // TODO: if we run into weird simulated extraction bugs, we may have to scope simulatedExtractionMemory, but I'm not sure about this (yet)
428
                PartialCraftingJobCalculationDependency resultDependency = calculateCraftingJobDependencyComponent(
1✔
429
                        dependencyComponent, dependenciesOutputSurplus, missingIngredients.get(dependencyComponent), parentDependencies,
1✔
430
                        dependencies, recipeIndex, channel, storageGetter, simulatedExtractionMemory, extractionMemoryReusable,
431
                        identifierGenerator, craftingJobsGraph, collectMissingRecipes);
432
                // Don't check the other components once we have an invalid dependency.
433
                if (!resultDependency.isValid()) {
1✔
434
                    missingDependencies.addAll(resultDependency.getUnknownCrafingRecipes());
1✔
435
                    partialCraftingJobs.addAll(resultDependency.getPartialCraftingJobs());
1✔
436
                    if (!collectMissingRecipes) {
1✔
437
                        break;
1✔
438
                    }
439
                }
440
            } catch (RecursiveCraftingRecipeException e) {
1✔
441
                e.addRecipe(recipe);
1✔
442
                throw e;
1✔
443
            }
1✔
444
        }
1✔
445
        // Add remaining surplus as negatives to simulated extraction
446
        for (IngredientComponent<?, ?> surplusComponent : dependenciesOutputSurplus.keySet()) {
1✔
447
            IngredientCollectionPrototypeMap<?, ?> surplusInstances = dependenciesOutputSurplus.get(surplusComponent);
1✔
448
            if (surplusInstances != null) {
1✔
449
                for (Object instance : surplusInstances) {
1✔
450
                    IngredientCollectionPrototypeMap<?, ?> simulatedExtractionMemoryInstances = simulatedExtractionMemory.get(surplusComponent);
1✔
451
                    if (simulatedExtractionMemoryInstances == null) {
1✔
452
                        simulatedExtractionMemoryInstances = new IngredientCollectionPrototypeMap<>(surplusComponent, true);
1✔
453
                        simulatedExtractionMemory.put(surplusComponent, simulatedExtractionMemoryInstances);
1✔
454
                    }
455
                    ((IngredientCollectionPrototypeMap) simulatedExtractionMemoryInstances).remove(instance);
1✔
456
                }
1✔
457
            }
458
        }
1✔
459

460
        // If at least one of our dependencies does not have a valid recipe or is not available,
461
        // go check the next recipe.
462
        if (!missingDependencies.isEmpty()) {
1✔
463
            return new PartialCraftingJobCalculation(null, missingDependencies, simulation.getLeft(), partialCraftingJobs);
1✔
464
        }
465

466
        CraftingJob craftingJob = new CraftingJob(identifierGenerator.getNext(), channel, recipe, amount,
1✔
467
                compressMixedIngredients(new MixedIngredients(simulation.getLeft())));
1✔
468
        for (CraftingJob dependency : dependencies.values()) {
1✔
469
            craftingJob.addDependency(dependency);
1✔
470
            craftingJobsGraph.addDependency(craftingJob, dependency);
1✔
471
        }
1✔
472
        return new PartialCraftingJobCalculation(craftingJob, null, simulation.getLeft(), null);
1✔
473
    }
474

475
    // Helper function for calculateCraftingJobs, returns a list of non-craftable ingredients and craftable ingredients.
476
    protected static <T, M> PartialCraftingJobCalculationDependency calculateCraftingJobDependencyComponent(
477
            IngredientComponent<T, M> dependencyComponent,
478
            Map<IngredientComponent<?, ?>, IngredientCollectionPrototypeMap<?, ?>> dependenciesOutputSurplus,
479
            MissingIngredients<T, M> missingIngredients,
480
            Set<IPrototypedIngredient> parentDependencies,
481
            Map<IRecipeDefinition, CraftingJob> dependencies,
482
            IRecipeIndex recipeIndex,
483
            int channel,
484
            Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
485
            Map<IngredientComponent<?, ?>,
486
                    IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemory,
487
            Map<IngredientComponent<?, ?>,
488
                    IIngredientCollectionMutable<?, ?>> extractionMemoryReusable,
489
            IIdentifierGenerator identifierGenerator,
490
            CraftingJobDependencyGraph craftingJobsGraph,
491
            boolean collectMissingRecipes)
492
            throws RecursiveCraftingRecipeException {
493
        IIngredientMatcher<T, M> dependencyMatcher = dependencyComponent.getMatcher();
1✔
494
        List<UnknownCraftingRecipeException> missingDependencies = Lists.newArrayList();
1✔
495
        for (MissingIngredients.Element<T, M> missingElement : missingIngredients.getElements()) {
1✔
496
            CraftingJob dependency = null;
1✔
497
            T dependencyInstance = null;
1✔
498
            boolean skipDependency = false;
1✔
499
            UnknownCraftingRecipeException firstError = null;
1✔
500
            // Loop over all prototype alternatives, at least one has to match.
501
            for (MissingIngredients.PrototypedWithRequested<T, M> prototypedAlternative : missingElement.getAlternatives()) {
1✔
502
                // Check if the missing element is reusable, and was triggered for craft earlier.
503
                if (missingElement.isInputReusable() && ((IIngredientCollectionMutable<T, M>) extractionMemoryReusable.get(dependencyComponent))
1✔
504
                        .contains(prototypedAlternative.getRequestedPrototype().getPrototype())) {
1✔
505
                    // Nothing has to be crafted anymore, jump to next dependency
506
                    skipDependency = true;
×
507
                    break;
×
508
                }
509

510
                IPrototypedIngredient<T, M> prototype = new PrototypedIngredient<>(
1✔
511
                        dependencyComponent,
512
                        dependencyMatcher.withQuantity(prototypedAlternative.getRequestedPrototype().getPrototype(), prototypedAlternative.getQuantityMissing()),
1✔
513
                        prototypedAlternative.getRequestedPrototype().getCondition()
1✔
514
                );
515
                // First check if we can grab it from previous surplus
516
                IngredientCollectionPrototypeMap<T, M> dependencyComponentSurplusOld = (IngredientCollectionPrototypeMap<T, M>) dependenciesOutputSurplus.get(dependencyComponent);
1✔
517
                IngredientCollectionPrototypeMap<T, M> dependencyComponentSurplus = null;
1✔
518
                if (dependencyComponentSurplusOld != null) {
1✔
519
                    // First create a copy of the given surplus store,
520
                    // and only once we see that the prototype is valid,
521
                    // save this copy again.
522
                    // This is because if this prototype is invalid,
523
                    // then we don't want these invalid surpluses.
524
                    dependencyComponentSurplus = new IngredientCollectionPrototypeMap<>(dependencyComponentSurplusOld.getComponent(), true);
1✔
525
                    dependencyComponentSurplus.addAll(dependencyComponentSurplusOld);
1✔
526

527
                    long remainingQuantity = dependencyMatcher.getQuantity(prototype.getPrototype());
1✔
528
                    IIngredientMatcher<T, M> prototypeMatcher = prototype.getComponent().getMatcher();
1✔
529
                    // Check all instances in the surplus that match with the given prototype
530
                    // For each match, we subtract its quantity from the required quantity.
531
                    Iterator<T> surplusIt = dependencyComponentSurplus.iterator(prototype.getPrototype(),
1✔
532
                            prototypeMatcher.withoutCondition(prototype.getCondition(), prototype.getComponent().getPrimaryQuantifier().getMatchCondition()));
1✔
533
                    boolean updatedRemainingQuantity = false;
1✔
534
                    while (remainingQuantity > 0 && surplusIt.hasNext()) {
1✔
535
                        updatedRemainingQuantity = true;
1✔
536
                        T matchingInstance = surplusIt.next();
1✔
537
                        long matchingInstanceQuantity = dependencyMatcher.getQuantity(matchingInstance);
1✔
538
                        if (matchingInstanceQuantity <= remainingQuantity) {
1✔
539
                            // This whole surplus instance can be consumed
540
                            remainingQuantity -= matchingInstanceQuantity;
1✔
541
                            surplusIt.remove();
1✔
542
                        } else {
543
                            // Only part of this surplus instance can be consumed.
544
                            matchingInstanceQuantity -= remainingQuantity;
×
545
                            remainingQuantity = 0;
×
546
                            surplusIt.remove();
×
547
                            dependencyComponentSurplus.setQuantity(matchingInstance, matchingInstanceQuantity);
×
548
                        }
549
                    }
1✔
550
                    if (updatedRemainingQuantity) {
1✔
551
                        if (remainingQuantity == 0) {
1✔
552
                            // The prototype is valid,
553
                            // so we can finally store our temporary surplus
554
                            dependenciesOutputSurplus.put(dependencyComponent, dependencyComponentSurplus);
×
555

556
                            // Nothing has to be crafted anymore, jump to next dependency
557
                            skipDependency = true;
×
558
                            break;
×
559
                        } else {
560
                            // Partial availability, other part needs to be crafted still.
561
                            prototype = new PrototypedIngredient<>(dependencyComponent,
1✔
562
                                    dependencyMatcher.withQuantity(prototype.getPrototype(), remainingQuantity),
1✔
563
                                    prototype.getCondition());
1✔
564
                        }
565
                    }
566
                }
567

568
                // Try to craft the given prototype
569
                try {
570
                    Set<IPrototypedIngredient> childDependencies = Sets.newHashSet(parentDependencies);
1✔
571
                    IPrototypedIngredient<T, M> dependencyPrototype = prototype;
1✔
572
                    if (dependencyMatcher.getQuantity(dependencyPrototype.getPrototype()) != 1) {
1✔
573
                        // Ensure 1-quantity is stored, for proper comparisons in future calls.
574
                        dependencyPrototype = new PrototypedIngredient<>(dependencyComponent,
1✔
575
                                dependencyMatcher.withQuantity(prototype.getPrototype(), 1),
1✔
576
                                prototype.getCondition());
1✔
577
                    }
578
                    if (!childDependencies.add(dependencyPrototype)) {
1✔
579
                        throw new RecursiveCraftingRecipeException(prototype);
1✔
580
                    }
581

582
                    dependency = calculateCraftingJobs(recipeIndex, channel, storageGetter,
1✔
583
                            dependencyComponent, prototype.getPrototype(),
1✔
584
                            prototype.getCondition(), true, simulatedExtractionMemory, extractionMemoryReusable,
1✔
585
                            identifierGenerator, craftingJobsGraph, childDependencies, collectMissingRecipes);
586
                    dependencyInstance = prototype.getPrototype();
1✔
587

588
                    // The prototype is valid,
589
                    // so we can finally store our temporary surplus
590
                    if (dependencyComponentSurplus != null) {
1✔
591
                        dependenciesOutputSurplus.put(dependencyComponent, dependencyComponentSurplus);
1✔
592
                    }
593

594
                    // Add the auxiliary recipe outputs that are not requested to the surplus
595
                    Object dependencyQuantifierlessCondition = dependencyMatcher.withoutCondition(prototype.getCondition(),
1✔
596
                            dependencyComponent.getPrimaryQuantifier().getMatchCondition());
1✔
597
                    long requestedQuantity = dependencyMatcher.getQuantity(prototype.getPrototype());
1✔
598
                    for (IngredientComponent outputComponent : dependency.getRecipe().getOutput().getComponents()) {
1✔
599
                        IngredientCollectionPrototypeMap<?, ?> componentSurplus = dependenciesOutputSurplus.get(outputComponent);
1✔
600
                        if (componentSurplus == null) {
1✔
601
                            componentSurplus = new IngredientCollectionPrototypeMap<>(outputComponent, true);
1✔
602
                            dependenciesOutputSurplus.put(outputComponent, componentSurplus);
1✔
603
                        }
604
                        List<Object> instances = dependency.getRecipe().getOutput().getInstances(outputComponent);
1✔
605
                        long recipeAmount = dependency.getAmount();
1✔
606
                        if (recipeAmount > 1) {
1✔
607
                            IIngredientMatcher matcher = outputComponent.getMatcher();
1✔
608
                            // If more than one recipe amount was crafted, correctly multiply the outputs to calculate the effective surplus.
609
                            instances = instances
1✔
610
                                    .stream()
1✔
611
                                    .map(instance -> matcher.withQuantity(instance, matcher.getQuantity(instance) * recipeAmount))
1✔
612
                                    .collect(Collectors.toList());
1✔
613
                        }
614
                        addRemainderAsSurplusForComponent(outputComponent, (List) instances, componentSurplus,
1✔
615
                                (IngredientComponent) prototype.getComponent(), prototype.getPrototype(), dependencyQuantifierlessCondition,
1✔
616
                                requestedQuantity);
617
                    }
1✔
618

619
                    break;
1✔
620
                } catch (UnknownCraftingRecipeException e) {
1✔
621
                    // Save the first error, and check the next prototype
622
                    if (firstError == null) {
1✔
623
                        // Modify the error so that the correct missing quantity is stored
624
                        firstError = new UnknownCraftingRecipeException(
1✔
625
                                e.getIngredient(), prototypedAlternative.getQuantityMissing(),
1✔
626
                                e.getMissingChildRecipes(), compressMixedIngredients(e.getIngredientsStorage()), e.getPartialCraftingJobs());
1✔
627
                    }
628
                }
629
            }
1✔
630

631
            // Check if this dependency can be skipped
632
            if (skipDependency) {
1✔
633
                continue;
×
634
            }
635

636
            // If no valid crafting recipe was found for the current sub-instance, re-throw its error
637
            if (dependency == null) {
1✔
638
                missingDependencies.add(firstError);
1✔
639
                if (collectMissingRecipes) {
1✔
640
                    continue;
1✔
641
                } else {
642
                    break;
643
                }
644
            }
645

646
            // --- When we reach this point, a valid sub-recipe was found ---
647

648
            // Update our simulatedExtractionMemory to indicate that the dependency's
649
            // instance should not be extracted anymore.
650
            ((IngredientCollectionPrototypeMap<T, M>) simulatedExtractionMemory.get(dependencyComponent))
1✔
651
                    .remove(dependencyInstance);
1✔
652

653
            // If the dependency instance is reusable, mark it as available in our reusable extraction memory
654
            if (missingElement.isInputReusable()) {
1✔
655
                ((IIngredientCollectionMutable<T, M>) extractionMemoryReusable.get(dependencyComponent)).add(dependencyInstance);
1✔
656
            }
657

658
            // Add the valid sub-recipe it to our dependencies
659
            // If the recipe was already present at this level, just increment the amount of the existing job.
660
            CraftingJob existingJob = dependencies.get(dependency.getRecipe());
1✔
661
            if (existingJob == null) {
1✔
662
                dependencies.put(dependency.getRecipe(), dependency);
1✔
663
            } else {
664
                // Remove the standalone dependency job by merging it into the existing job
665
                craftingJobsGraph.mergeCraftingJobs(existingJob, dependency, true);
1✔
666
            }
667
        }
1✔
668

669
        return new PartialCraftingJobCalculationDependency(missingDependencies, dependencies.values());
1✔
670
    }
671

672
    // Helper function for calculateCraftingJobDependencyComponent
673
    protected static <T1, M1, T2, M2> void addRemainderAsSurplusForComponent(IngredientComponent<T1, M1> ingredientComponent,
674
                                                                             List<T1> instances,
675
                                                                             IngredientCollectionPrototypeMap<T1, M1> simulatedExtractionMemory,
676
                                                                             IngredientComponent<T2, M2> blackListComponent,
677
                                                                             T2 blacklistInstance, M2 blacklistCondition,
678
                                                                             long blacklistQuantity) {
679
        IIngredientMatcher<T2, M2> blacklistMatcher = blackListComponent.getMatcher();
1✔
680
        for (T1 instance : instances) {
1✔
681
            IIngredientMatcher<T1, M1> outputMatcher = ingredientComponent.getMatcher();
1✔
682
            long reduceQuantity = 0;
1✔
683
            if (blackListComponent == ingredientComponent
1✔
684
                    && blacklistMatcher.matches(blacklistInstance, (T2) instance, blacklistCondition)) {
1✔
685
                reduceQuantity = blacklistQuantity;
1✔
686
            }
687
            long quantity = simulatedExtractionMemory.getQuantity(instance) + (outputMatcher.getQuantity(instance) - reduceQuantity);
1✔
688
            if (quantity > 0) {
1✔
689
                simulatedExtractionMemory.setQuantity(instance, quantity);
1✔
690
            }
691
        }
1✔
692
    }
1✔
693

694
    /**
695
     * Schedule all crafting jobs in the given dependency graph in the given network.
696
     *
697
     * @param craftingNetwork            The target crafting network.
698
     * @param storageGetter              The storage getter.
699
     * @param craftingJobDependencyGraph The crafting job dependency graph.
700
     * @param allowDistribution          If the crafting jobs are allowed to be split over multiple crafting interfaces.
701
     * @param initiator                  Optional UUID of the initiator.
702
     * @throws UnavailableCraftingInterfacesException If no crafting interfaces were available.
703
     */
704
    public static void scheduleCraftingJobs(ICraftingNetwork craftingNetwork,
705
                                            Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
706
                                            CraftingJobDependencyGraph craftingJobDependencyGraph,
707
                                            boolean allowDistribution,
708
                                            @Nullable UUID initiator) throws UnavailableCraftingInterfacesException {
709
        List<CraftingJob> startedJobs = Lists.newArrayList();
×
710
        craftingNetwork.getCraftingJobDependencyGraph().importDependencies(craftingJobDependencyGraph);
×
711
        for (CraftingJob craftingJob : craftingJobDependencyGraph.getCraftingJobs()) {
×
712
            try {
713
                craftingNetwork.scheduleCraftingJob(craftingJob, allowDistribution, storageGetter);
×
714
            } catch (UnavailableCraftingInterfacesException e) {
×
715
                // First, cancel all jobs that were already started
716
                for (CraftingJob startedJob : startedJobs) {
×
717
                    CraftingHelpers.insertIngredientsGuaranteed(startedJob.getIngredientsStorageBuffer(), storageGetter, (ICraftingResultsSink) Iterables.getFirst(craftingNetwork.getCraftingInterfaces(startedJob.getChannel()), null));
×
718
                    craftingNetwork.cancelCraftingJob(startedJob.getChannel(), startedJob.getId());
×
719
                }
×
720

721
                // Then, throw an exception for all jobs in this dependency graph
722
                throw new UnavailableCraftingInterfacesException(craftingJobDependencyGraph.getCraftingJobs());
×
723
            }
×
724
            startedJobs.add(craftingJob);
×
725
            if (initiator != null) {
×
726
                craftingJob.setInitiatorUuid(initiator.toString());
×
727
            }
728
        }
×
729
    }
×
730

731
    /**
732
     * Schedule the given crafting job  in the given network.
733
     *
734
     * @param craftingNetwork   The target crafting network.
735
     * @param storageGetter     The storage getter.
736
     * @param craftingJob       The crafting job to schedule.
737
     * @param allowDistribution If the crafting job is allowed to be split over multiple crafting interfaces.
738
     * @param initiator         Optional UUID of the initiator.
739
     * @return The scheduled crafting job.
740
     * @throws UnavailableCraftingInterfacesException If no crafting interfaces were available.
741
     */
742
    public static CraftingJob scheduleCraftingJob(ICraftingNetwork craftingNetwork,
743
                                                  Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
744
                                                  CraftingJob craftingJob,
745
                                                  boolean allowDistribution,
746
                                                  @Nullable UUID initiator) throws UnavailableCraftingInterfacesException {
747
        craftingNetwork.scheduleCraftingJob(craftingJob, allowDistribution, storageGetter);
×
748
        if (initiator != null) {
×
749
            craftingJob.setInitiatorUuid(initiator.toString());
×
750
        }
751
        return craftingJob;
×
752
    }
753

754
    /**
755
     * Schedule a crafting job for the given instance in the given network.
756
     * @param network The target network.
757
     * @param channel The target channel.
758
     * @param ingredientComponent The ingredient component type of the instance.
759
     * @param instance The instance to craft.
760
     * @param matchCondition The match condition of the instance.
761
     * @param craftMissing If the missing required ingredients should also be crafted.
762
     * @param allowDistribution If the crafting job is allowed to be split over multiple crafting interfaces.
763
     * @param identifierGenerator An ID generator for crafting jobs.
764
     * @param initiator Optional UUID of the initiator.
765
     * @param <T> The instance type.
766
     * @param <M> The matching condition parameter.
767
     * @return The scheduled crafting job, or null if no recipe was found.
768
     */
769
    @Nullable
770
    public static <T, M> CraftingJob calculateAndScheduleCraftingJob(INetwork network, int channel,
771
                                                                     IngredientComponent<T, M> ingredientComponent,
772
                                                                     T instance, M matchCondition,
773
                                                                     boolean craftMissing, boolean allowDistribution,
774
                                                                     IIdentifierGenerator identifierGenerator,
775
                                                                     @Nullable UUID initiator) {
776
        try {
777
            CraftingJobDependencyGraph dependencyGraph = new CraftingJobDependencyGraph();
×
778
            CraftingJob craftingJob = calculateCraftingJobs(network, channel, ingredientComponent, instance,
×
779
                    matchCondition, craftMissing, identifierGenerator, dependencyGraph, false);
780

781
            ICraftingNetwork craftingNetwork = getCraftingNetworkChecked(network);
×
782

783
            scheduleCraftingJobs(craftingNetwork, getNetworkStorageGetter(network, channel, false), dependencyGraph, allowDistribution, initiator);
×
784

785
            return craftingJob;
×
786
        } catch (UnknownCraftingRecipeException | RecursiveCraftingRecipeException | UnavailableCraftingInterfacesException e) {
×
787
            return null;
×
788
        }
789
    }
790

791
    /**
792
     * Schedule a crafting job for the given recipe in the given network.
793
     * @param network The target network.
794
     * @param channel The target channel.
795
     * @param recipe The recipe to craft.
796
     * @param amount The amount to craft.
797
     * @param craftMissing If the missing required ingredients should also be crafted.
798
     * @param allowDistribution If the crafting job is allowed to be split over multiple crafting interfaces.
799
     * @param identifierGenerator An ID generator for crafting jobs.
800
     * @param initiator Optional UUID of the initiator.
801
     * @return The scheduled crafting job, or null if no recipe was found.
802
     */
803
    @Nullable
804
    public static CraftingJob calculateAndScheduleCraftingJob(INetwork network, int channel,
805
                                                              IRecipeDefinition recipe, int amount,
806
                                                              boolean craftMissing, boolean allowDistribution,
807
                                                              IIdentifierGenerator identifierGenerator,
808
                                                              @Nullable UUID initiator) {
809
        try {
810
            CraftingJobDependencyGraph dependencyGraph = new CraftingJobDependencyGraph();
×
811
            CraftingJob craftingJob = calculateCraftingJobs(network, channel, recipe, amount, craftMissing,
×
812
                    identifierGenerator, dependencyGraph, false);
813

814
            ICraftingNetwork craftingNetwork = getCraftingNetworkChecked(network);
×
815

816
            scheduleCraftingJobs(craftingNetwork, getNetworkStorageGetter(network, channel, false), dependencyGraph, allowDistribution, initiator);
×
817

818
            return craftingJob;
×
819
        } catch (RecursiveCraftingRecipeException | FailedCraftingRecipeException | UnavailableCraftingInterfacesException e) {
×
820
            return null;
×
821
        }
822
    }
823

824
    /**
825
     * Check if the given network contains the given instance in any of its storages.
826
     * @param network The target network.
827
     * @param channel The target channel.
828
     * @param ingredientComponent The ingredient component type of the instance.
829
     * @param instance The instance to check.
830
     * @param matchCondition The match condition of the instance.
831
     * @param <T> The instance type.
832
     * @param <M> The matching condition parameter.
833
     * @return If the instance is present in the network.
834
     */
835
    public static <T, M> boolean hasStorageInstance(INetwork network, int channel,
836
                                                    IngredientComponent<T, M> ingredientComponent,
837
                                                    T instance, M matchCondition) {
838
        IIngredientComponentStorage<T, M> storage = getNetworkStorage(network, channel, ingredientComponent, true);
×
839
        if (storage instanceof IngredientChannelAdapter) ((IngredientChannelAdapter) storage).disableLimits();
×
840
        boolean contains;
841
        if (storage instanceof IngredientChannelIndexed) {
×
842
            IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
×
843
            long quantityPresent = ((IngredientChannelIndexed<T, M>) storage).getIndex().getQuantity(instance);
×
844
            contains = matcher.hasCondition(matchCondition, ingredientComponent.getPrimaryQuantifier().getMatchCondition()) ? quantityPresent >= matcher.getQuantity(instance) : quantityPresent > 0;
×
845
        } else {
×
846
            contains = !ingredientComponent.getMatcher().isEmpty(storage.extract(instance, matchCondition, true));
×
847
        }
848
        if (storage instanceof IngredientChannelAdapter) ((IngredientChannelAdapter) storage).enableLimits();
×
849
        return contains;
×
850
    }
851

852
    /**
853
     * Check the quantity of the given instance in the network.
854
     * @param network The target network.
855
     * @param channel The target channel.
856
     * @param ingredientComponent The ingredient component type of the instance.
857
     * @param instance The instance to check.
858
     * @param matchCondition The match condition of the instance.
859
     * @param <T> The instance type.
860
     * @param <M> The matching condition parameter.
861
     * @return The quantity in the network.
862
     */
863
    public static <T, M> long getStorageInstanceQuantity(INetwork network, int channel,
864
                                                         IngredientComponent<T, M> ingredientComponent,
865
                                                         T instance, M matchCondition) {
866
        IIngredientComponentStorage<T, M> storage = getNetworkStorage(network, channel, ingredientComponent, true);
×
867
        if (storage instanceof IngredientChannelAdapter) ((IngredientChannelAdapter) storage).disableLimits();
×
868
        long quantityPresent;
869
        if (storage instanceof IngredientChannelIndexed) {
×
870
            quantityPresent = ((IngredientChannelIndexed<T, M>) storage).getIndex().getQuantity(instance);
×
871
        } else {
872
            quantityPresent = ingredientComponent.getMatcher().getQuantity(storage.extract(instance, matchCondition, true));
×
873
        }
874
        if (storage instanceof IngredientChannelAdapter) ((IngredientChannelAdapter) storage).enableLimits();
×
875
        return quantityPresent;
×
876
    }
877

878
    /**
879
     * Check if there is a scheduled crafting job for the given instance.
880
     * @param craftingNetwork The target crafting network.
881
     * @param channel The target channel.
882
     * @param ingredientComponent The ingredient component type of the instance.
883
     * @param instance The instance to check.
884
     * @param matchCondition The match condition of the instance.
885
     * @param <T> The instance type.
886
     * @param <M> The matching condition parameter.
887
     * @return If the instance has a crafting job.
888
     */
889
    public static <T, M> boolean isCrafting(ICraftingNetwork craftingNetwork, int channel,
890
                                            IngredientComponent<T, M> ingredientComponent,
891
                                            T instance, M matchCondition) {
892
        Iterator<CraftingJob> craftingJobs = craftingNetwork.getCraftingJobs(channel, ingredientComponent,
×
893
                instance, matchCondition);
894
        return craftingJobs.hasNext();
×
895
    }
896

897
    /**
898
     * Check if there is a scheduled crafting job for the given recipe.
899
     * @param craftingNetwork The target crafting network.
900
     * @param channel The target channel.
901
     * @param recipe The recipe to check.
902
     * @return If the instance has a crafting job.
903
     */
904
    public static boolean isCrafting(ICraftingNetwork craftingNetwork, int channel,
905
                                     IRecipeDefinition recipe) {
906
        Iterator<CraftingJob> it = craftingNetwork.getCraftingJobs(channel);
×
907
        while (it.hasNext()) {
×
908
            if (it.next().getRecipe().equals(recipe)) {
×
909
                return true;
×
910
            }
911
        }
912
        return false;
×
913
    }
914

915
    /**
916
     * Get all required recipe input ingredients from the network for the given ingredient component.
917
     *
918
     * If multiple alternative inputs are possible,
919
     * then only the first possible match will be taken.
920
     *
921
     * Note: Make sure that you first call in simulation-mode
922
     * to see if the ingredients are available.
923
     * If you immediately call this non-simulated,
924
     * then there might be a chance that ingredients are lost
925
     * from the network.
926
     *
927
     * @param storage The target storage.
928
     * @param ingredientComponent The ingredient component to get the ingredients for.
929
     * @param recipe The recipe to get the inputs from.
930
     * @param simulate If true, then the ingredients will effectively be removed from the network, not when false.
931
     * @param recipeOutputQuantity The number of times the given recipe should be applied.
932
     * @param <T> The instance type.
933
     * @param <M> The matching condition parameter, may be Void.
934
     * @return A list of slot-based ingredients, or null if no valid inputs could be found.
935
     */
936
    @Nullable
937
    public static <T, M> List<T> getIngredientRecipeInputs(IIngredientComponentStorage<T, M> storage,
938
                                                           IngredientComponent<T, M> ingredientComponent,
939
                                                           IRecipeDefinition recipe, boolean simulate,
940
                                                           long recipeOutputQuantity) {
941
        return getIngredientRecipeInputs(storage, ingredientComponent, recipe, simulate,
1✔
942
                simulate ? new IngredientCollectionPrototypeMap<>(ingredientComponent, true) : null,
1✔
943
                new IngredientHashSet<>(ingredientComponent),
944
                false, recipeOutputQuantity).getLeft();
1✔
945
    }
946

947
    /**
948
     * Get all required recipe input ingredients from the network for the given ingredient component,
949
     * and optionally, explicitly calculate the missing ingredients.
950
     *
951
     * If multiple alternative inputs are possible,
952
     * then only the first possible match will be taken.
953
     *
954
     * Note: Make sure that you first call in simulation-mode
955
     * to see if the ingredients are available.
956
     * If you immediately call this non-simulated,
957
     * then there might be a chance that ingredients are lost
958
     * from the network.
959
     *
960
     * @param storage The target storage.
961
     * @param ingredientComponent The ingredient component to get the ingredients for.
962
     * @param recipe The recipe to get the inputs from.
963
     * @param simulate If true, then the ingredients will effectively be removed from the network, not when false.
964
     * @param simulatedExtractionMemory This map remembers all extracted instances in simulation mode.
965
     *                                  This is to make sure that instances can not be extracted multiple times
966
     *                                  when simulating.
967
     *                                  The quantities can also go negatives,
968
     *                                  which means that a surplus of the given instance is present,
969
     *                                  which will be used up first before a call to the storage.
970
     * @param extractionMemoryReusable Like simulatedExtractionMemory, but it stores the reusable ingredients.
971
     * @param collectMissingIngredients If missing ingredients should be collected.
972
     *                                  If false, then the first returned list may be null
973
     *                                  if no valid matches can be found,
974
     *                                  and the second returned list is always null,
975
     * @param recipeOutputQuantity The number of times the given recipe should be applied.
976
     * @param <T> The instance type.
977
     * @param <M> The matching condition parameter, may be Void.
978
     * @return A pair with two lists:
979
     *           1. A list of available slot-based ingredients.
980
     *           2. A missing ingredients object.
981
     */
982
    public static <T, M> Pair<List<T>, MissingIngredients<T, M>>
983
    getIngredientRecipeInputs(IIngredientComponentStorage<T, M> storage, IngredientComponent<T, M> ingredientComponent,
984
                              IRecipeDefinition recipe, boolean simulate,
985
                              IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemory,
986
                              IIngredientCollectionMutable<T, M> extractionMemoryReusable,
987
                              boolean collectMissingIngredients, long recipeOutputQuantity) {
988
        IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
1✔
989

990
        // Quickly return if the storage is empty
991
        // We can't take this shortcut if we have a reusable ingredient AND extractionMemoryReusable is not empty
992
        if (storage.getMaxQuantity() == 0 &&
1✔
993
                extractionMemoryReusable.isEmpty() &&
1✔
994
                IntStream.range(0, recipe.getInputs(ingredientComponent).size())
1✔
995
                        .noneMatch(i -> recipe.isInputReusable(ingredientComponent, i))) {
1✔
996
            if (collectMissingIngredients) {
1✔
997
                List<IPrototypedIngredientAlternatives<T, M>> recipeInputs = recipe.getInputs(ingredientComponent);
1✔
998
                MissingIngredients<T, M> missing = new MissingIngredients<>(recipeInputs.stream().map(IPrototypedIngredientAlternatives::getAlternatives)
1✔
999
                        .map(l -> multiplyPrototypedIngredients(l, recipeOutputQuantity)) // If the input is reusable, don't multiply the expected input quantity
1✔
1000
                        .map(ps -> new MissingIngredients.Element<>(ps
1✔
1001
                                .stream()
1✔
1002
                                .map(p -> new MissingIngredients.PrototypedWithRequested<>(p, matcher.getQuantity(p.getPrototype())))
1✔
1003
                                .collect(Collectors.toList()), false)
1✔
1004
                        )
1005
                        .collect(Collectors.toList()));
1✔
1006
                return Pair.of(
1✔
1007
                        Lists.newArrayList(Collections.nCopies(recipe.getInputs(ingredientComponent).size(),
1✔
1008
                                ingredientComponent.getMatcher().getEmptyInstance())),
1✔
1009
                        missing);
1010
            } else {
1011
                return Pair.of(null, null);
1✔
1012
            }
1013
        }
1014

1015
        // Iterate over all input slots
1016
        List<IPrototypedIngredientAlternatives<T, M>> inputAlternativePrototypes = recipe.getInputs(ingredientComponent);
1✔
1017
        List<T> inputInstances = Lists.newArrayList();
1✔
1018
        List<MissingIngredients.Element<T, M>> missingElements =
1019
                collectMissingIngredients ? Lists.newArrayList() : null;
1✔
1020
        for (int inputIndex = 0; inputIndex < inputAlternativePrototypes.size(); inputIndex++) {
1✔
1021
            IPrototypedIngredientAlternatives<T, M> inputPrototypes = inputAlternativePrototypes.get(inputIndex);
1✔
1022
            T firstInputInstance = null;
1✔
1023
            boolean setFirstInputInstance = false;
1✔
1024
            T inputInstance = null;
1✔
1025
            boolean hasInputInstance = false;
1✔
1026
            IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemoryBufferFirst = null;
1✔
1027
            IIngredientCollectionMutable<T, M> extractionMemoryReusableBufferFirst = null;
1✔
1028

1029
            // Iterate over all alternatives for this input slot, and take the first matching ingredient.
1030
            List<MissingIngredients.PrototypedWithRequested<T, M>> missingAlternatives = Lists.newArrayList();
1✔
1031
            IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemoryAlternative = simulate ? new IngredientCollectionPrototypeMap<>(ingredientComponent, true) : null;
1✔
1032
            if (simulate) {
1✔
1033
                simulatedExtractionMemoryAlternative.addAll(simulatedExtractionMemory);
1✔
1034
            }
1035
            for (IPrototypedIngredient<T, M> inputPrototype : inputPrototypes.getAlternatives()) {
1✔
1036
                boolean inputReusable = recipe.isInputReusable(ingredientComponent, inputIndex);
1✔
1037
                IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemoryBuffer = simulate ? new IngredientCollectionPrototypeMap<>(ingredientComponent, true) : null;
1✔
1038
                IIngredientCollectionMutable<T, M> extractionMemoryReusableBuffer = inputReusable ? new IngredientHashSet<>(ingredientComponent) : null;
1✔
1039
                boolean shouldBreak = false;
1✔
1040

1041
                // Multiply required prototype if recipe quantity is higher than one, AND if the input is NOT reusable.
1042
                if (recipeOutputQuantity > 1 && !inputReusable) {
1✔
1043
                    inputPrototype = multiplyPrototypedIngredient(inputPrototype, recipeOutputQuantity);
1✔
1044
                }
1045

1046
                // If the prototype is empty, we can skip network extraction
1047
                if (matcher.isEmpty(inputPrototype.getPrototype())) {
1✔
1048
                    inputInstance = inputPrototype.getPrototype();
1✔
1049
                    hasInputInstance = true;
1✔
1050
                    break;
1✔
1051
                }
1052

1053
                long prototypeQuantity = matcher.getQuantity(inputPrototype.getPrototype());
1✔
1054
                if (inputReusable && extractionMemoryReusable.contains(inputPrototype.getPrototype())) {
1✔
1055
                    // If the reusable item has been extracted before, mark as valid, and don't extract again.
1056
                    inputInstance = inputPrototype.getComponent().getMatcher().getEmptyInstance();
1✔
1057
                    hasInputInstance = true;
1✔
1058
                    shouldBreak = true;
1✔
1059
                } else {
1060
                    long memoryQuantity;
1061
                    if (simulate && (memoryQuantity = simulatedExtractionMemoryAlternative
1✔
1062
                            .getQuantity(inputPrototype.getPrototype())) != 0) {
1✔
1063
                        long newQuantity = memoryQuantity + prototypeQuantity;
1✔
1064
                        if (newQuantity > 0) {
1✔
1065
                            // Part of our quantity can be provided via simulatedExtractionMemory,
1066
                            // but not all of it,
1067
                            // so we need to extract from storage as well.
1068
                            T newInstance = matcher.withQuantity(inputPrototype.getPrototype(), newQuantity);
1✔
1069
                            M matchCondition = matcher.withoutCondition(inputPrototype.getCondition(),
1✔
1070
                                    ingredientComponent.getPrimaryQuantifier().getMatchCondition());
1✔
1071
                            if (storage instanceof IngredientChannelAdapter)
1✔
1072
                                ((IngredientChannelAdapter) storage).disableLimits();
×
1073
                            T extracted = storage.extract(newInstance, matchCondition, true);
1✔
1074
                            if (storage instanceof IngredientChannelAdapter)
1✔
1075
                                ((IngredientChannelAdapter) storage).enableLimits();
×
1076
                            long quantityExtracted = matcher.getQuantity(extracted);
1✔
1077
                            if (quantityExtracted == newQuantity) {
1✔
1078
                                // All remaining could be extracted from storage, all is fine now
1079
                                inputInstance = inputPrototype.getPrototype();
1✔
1080
                                simulatedExtractionMemoryAlternative.add(inputInstance);
1✔
1081
                                simulatedExtractionMemoryBuffer.add(inputInstance);
1✔
1082
                                if (inputReusable) {
1✔
1083
                                    extractionMemoryReusableBuffer.add(inputInstance);
1✔
1084
                                }
1085
                                hasInputInstance = true;
1✔
1086
                                shouldBreak = true;
1✔
1087
                            } else if (collectMissingIngredients) {
1✔
1088
                                // Not everything could be extracted from storage, we *miss* the remaining ingredient.
1089
                                long quantityMissingPrevious = Math.max(0, memoryQuantity - quantityExtracted);
1✔
1090
                                long quantityMissingTotal = newQuantity - quantityExtracted;
1✔
1091
                                long quantityMissingRelative = quantityMissingTotal - quantityMissingPrevious;
1✔
1092

1093
                                missingAlternatives.add(new MissingIngredients.PrototypedWithRequested<>(inputPrototype, quantityMissingRelative));
1✔
1094
                                inputInstance = matcher.withQuantity(inputPrototype.getPrototype(), prototypeQuantity - quantityMissingRelative);
1✔
1095
                                simulatedExtractionMemoryAlternative.setQuantity(inputPrototype.getPrototype(), quantityMissingTotal);
1✔
1096
                                // Original prototype quantity because we what can be extracted from storage and what could NOT be extracted from storage should be added to the simulation extraction memory.
1097
                                // The part that could NOT be extracted will be removed later when crafting jobs are calculated for the missing elements. (not doing so would lead to over-estimation of what is in storage)
1098
                                simulatedExtractionMemoryBuffer.add(matcher.withQuantity(inputPrototype.getPrototype(), prototypeQuantity));
1✔
1099
                            }
1100
                        } else {
1✔
1101
                            // All of our quantity can be provided via our surplus in simulatedExtractionMemory
1102
                            simulatedExtractionMemoryAlternative.add(inputPrototype.getPrototype());
1✔
1103
                            simulatedExtractionMemoryBuffer.add(inputPrototype.getPrototype());
1✔
1104
                            if (inputReusable) {
1✔
1105
                                extractionMemoryReusableBuffer.add(inputPrototype.getPrototype());
1✔
1106
                            }
1107
                            inputInstance = inputPrototype.getComponent().getMatcher().getEmptyInstance();
1✔
1108
                            hasInputInstance = true;
1✔
1109
                            shouldBreak = true;
1✔
1110
                        }
1111
                    } else {
1✔
1112
                        M matchCondition = matcher.withoutCondition(inputPrototype.getCondition(),
1✔
1113
                                ingredientComponent.getPrimaryQuantifier().getMatchCondition());
1✔
1114
                        if (storage instanceof IngredientChannelAdapter)
1✔
1115
                            ((IngredientChannelAdapter) storage).disableLimits();
×
1116
                        T extracted = storage.extract(inputPrototype.getPrototype(), matchCondition, simulate);
1✔
1117
                        if (storage instanceof IngredientChannelAdapter)
1✔
1118
                            ((IngredientChannelAdapter) storage).enableLimits();
×
1119
                        long quantityExtracted = matcher.getQuantity(extracted);
1✔
1120
                        inputInstance = extracted;
1✔
1121
                        if (simulate) {
1✔
1122
                            simulatedExtractionMemoryAlternative.add(extracted);
1✔
1123
                            simulatedExtractionMemoryBuffer.add(inputPrototype.getPrototype());
1✔
1124
                        }
1125
                        if (prototypeQuantity == quantityExtracted) {
1✔
1126
                            hasInputInstance = true;
1✔
1127
                            shouldBreak = true;
1✔
1128
                            if (inputReusable) {
1✔
1129
                                extractionMemoryReusableBuffer.add(inputPrototype.getPrototype());
1✔
1130
                            }
1131
                        } else if (collectMissingIngredients) {
1✔
1132
                            long quantityMissing = prototypeQuantity - quantityExtracted;
1✔
1133
                            missingAlternatives.add(new MissingIngredients.PrototypedWithRequested<>(inputPrototype, quantityMissing));
1✔
1134
                        }
1135
                    }
1136
                }
1137

1138
                if (!setFirstInputInstance || shouldBreak) {
1✔
1139
                    setFirstInputInstance = true;
1✔
1140
                    firstInputInstance = inputInstance;
1✔
1141
                    simulatedExtractionMemoryBufferFirst = simulatedExtractionMemoryBuffer;
1✔
1142
                    if (inputReusable) {
1✔
1143
                        extractionMemoryReusableBufferFirst = extractionMemoryReusableBuffer;
1✔
1144
                    } else {
1145
                        extractionMemoryReusableBufferFirst = null;
1✔
1146
                    }
1147
                }
1148

1149
                if (shouldBreak) {
1✔
1150
                    break;
1✔
1151
                }
1152
            }
1✔
1153

1154
            if (simulatedExtractionMemoryBufferFirst != null) {
1✔
1155
                for (T instance : simulatedExtractionMemoryBufferFirst) {
1✔
1156
                    simulatedExtractionMemory.add(instance);
1✔
1157
                }
1✔
1158
            }
1159
            if (extractionMemoryReusableBufferFirst != null) {
1✔
1160
                for (T instance : extractionMemoryReusableBufferFirst) {
1✔
1161
                    extractionMemoryReusable.add(instance);
1✔
1162
                }
1✔
1163
            }
1164

1165
            // If none of the alternatives were found, fail immediately
1166
            if (!hasInputInstance) {
1✔
1167
                if (!simulate && !collectMissingIngredients) {
1✔
1168
                    // But first, re-insert all already-extracted instances
1169
                    for (T instance : inputInstances) {
1✔
1170
                        T remaining = storage.insert(instance, false);
1✔
1171
                        if (!matcher.isEmpty(remaining)) {
1✔
1172
                            throw new IllegalStateException("Extraction for a crafting recipe failed" +
×
1173
                                    "due to inconsistent insertion behaviour by destination in simulation " +
1174
                                    "and non-simulation: " + storage + ". Lost: " + remaining);
1175
                        }
1176
                    }
1✔
1177
                }
1178

1179
                if (!collectMissingIngredients) {
1✔
1180
                    // This input failed, return immediately
1181
                    return Pair.of(null, null);
1✔
1182
                } else {
1183
                    // Multiply missing collection if recipe quantity is higher than one
1184
                    if (missingAlternatives.size() > 0) {
1✔
1185
                        missingElements.add(new MissingIngredients.Element<>(missingAlternatives, recipe.isInputReusable(ingredientComponent, inputIndex)));
1✔
1186
                    }
1187
                }
1188
            }
1189

1190
            // Otherwise, append it to the list and carry on.
1191
            // If none of the instances were valid, we add the first partially valid instance.
1192
            if (hasInputInstance) {
1✔
1193
                inputInstances.add(inputInstance);
1✔
1194
            } else if (setFirstInputInstance && !matcher.isEmpty(firstInputInstance)) {
1✔
1195
                inputInstances.add(firstInputInstance);
1✔
1196
            }
1197
        }
1198

1199
        return Pair.of(
1✔
1200
                inputInstances,
1201
                collectMissingIngredients ? new MissingIngredients<>(missingElements) : null
1✔
1202
        );
1203
    }
1204

1205
    /**
1206
     * Get all required recipe input ingredients from the network.
1207
     *
1208
     * If multiple alternative inputs are possible,
1209
     * then only the first possible match will be taken.
1210
     *
1211
     * Note: Make sure that you first call in simulation-mode
1212
     * to see if the ingredients are available.
1213
     * If you immediately call this non-simulated,
1214
     * then there might be a chance that ingredients are lost
1215
     * from the network.
1216
     *
1217
     * @param network The target network.
1218
     * @param channel The target channel.
1219
     * @param recipe The recipe to get the inputs from.
1220
     * @param simulate If true, then the ingredients will effectively be removed from the network, not when false.
1221
     * @param recipeOutputQuantity The number of times the given recipe should be applied.
1222
     * @return The found ingredients or null.
1223
     */
1224
    @Nullable
1225
    public static IMixedIngredients getRecipeInputs(INetwork network, int channel,
1226
                                                    IRecipeDefinition recipe, boolean simulate,
1227
                                                    long recipeOutputQuantity) {
1228
        Map<IngredientComponent<?, ?>, List<?>> inputs = getRecipeInputs(getNetworkStorageGetter(network, channel, true),
×
1229
                recipe, simulate, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), false, recipeOutputQuantity).getLeft();
×
1230
        return inputs == null ? null : new MixedIngredients(inputs);
×
1231
    }
1232

1233
    /**
1234
     * Get all required recipe input ingredients from the crafting job's buffer.
1235
     *
1236
     * If multiple alternative inputs are possible,
1237
     * then only the first possible match will be taken.
1238
     *
1239
     * Note: Make sure that you first call in simulation-mode
1240
     * to see if the ingredients are available.
1241
     * If you immediately call this non-simulated,
1242
     * then there might be a chance that ingredients are lost
1243
     * from the buffer.
1244
     *
1245
     * @param craftingJob The crafting job.
1246
     * @param recipe The recipe to get the inputs from.
1247
     * @param simulate If true, then the ingredients will effectively be removed from the buffer, not when false.
1248
     * @param recipeOutputQuantity The number of times the given recipe should be applied.
1249
     * @return The found ingredients or null.
1250
     */
1251
    @Nullable
1252
    public static IMixedIngredients getRecipeInputsFromCraftingJobBuffer(CraftingJob craftingJob,
1253
                                                    IRecipeDefinition recipe, boolean simulate,
1254
                                                    long recipeOutputQuantity) {
1255
        Map<IngredientComponent<?, ?>, List<?>> inputs = getRecipeInputs(getCraftingJobBufferStorageGetter(craftingJob),
×
1256
                recipe, simulate, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), false, recipeOutputQuantity).getLeft();
×
1257
        return inputs == null ? null : new MixedIngredients(inputs);
×
1258
    }
1259

1260
    /**
1261
     * Create a callback function for getting a storage for an ingredient component from the given network channel.
1262
     * @param network The target network.
1263
     * @param channel The target channel.
1264
     * @param scheduleObservation If an observation inside the ingredients network should be scheduled.
1265
     * @return A callback function for getting a storage for an ingredient component.
1266
     */
1267
    public static Function<IngredientComponent<?, ?>, IIngredientComponentStorage> getNetworkStorageGetter(INetwork network, int channel, boolean scheduleObservation) {
1268
        return ingredientComponent -> getNetworkStorage(network, channel, ingredientComponent, scheduleObservation);
×
1269
    }
1270

1271
    /**
1272
     * Create a callback function for getting a storage for an ingredient component from the given crafting job buffer.
1273
     * @param craftingJob The crafting job.
1274
     * @return A callback function for getting a storage for an ingredient component.
1275
     */
1276
    public static Function<IngredientComponent<?, ?>, IIngredientComponentStorage> getCraftingJobBufferStorageGetter(CraftingJob craftingJob) {
1277
        return ingredientComponent -> {
×
1278
            List<?> list = craftingJob.getIngredientsStorageBuffer().getInstances(ingredientComponent);
×
1279
            return new IngredientComponentStorageSlottedCollectionWrapper<>(new IngredientList(ingredientComponent, list), Integer.MAX_VALUE, Integer.MAX_VALUE);
×
1280
        };
1281
    }
1282

1283
    /**
1284
     * Get all required recipe input ingredients based on a given storage callback.
1285
     *
1286
     * If multiple alternative inputs are possible,
1287
     * then only the first possible match will be taken.
1288
     *
1289
     * Note: Make sure that you first call in simulation-mode
1290
     * to see if the ingredients are available.
1291
     * If you immediately call this non-simulated,
1292
     * then there might be a chance that ingredients are lost
1293
     * from the network.
1294
     *
1295
     * @param storageGetter A callback function to get a storage for the given ingredient component.
1296
     * @param recipe The recipe to get the inputs from.
1297
     * @param simulate If true, then the ingredients will effectively be removed from the network, not when false.
1298
     * @param simulatedExtractionMemories This map remembers all extracted instances in simulation mode.
1299
     *                                    This is to make sure that instances can not be extracted multiple times
1300
     *                                    when simulating.
1301
     * @param extractionMemoriesReusable Like simulatedExtractionMemories, but it stores the reusable ingredients.
1302
     * @param collectMissingIngredients If missing ingredients should be collected.
1303
     *                                  If false, then the first returned mixed ingredients may be null
1304
     *                                  if no valid matches can be found,
1305
     *                                  and the second returned list is always null,
1306
     * @param recipeOutputQuantity The number of times the given recipe should be applied.
1307
     * @return A pair with two objects:
1308
     *           1. The found ingredients or null.
1309
     *           2. A mapping from ingredient component to missing ingredients (non-slot-based).
1310
     */
1311
    public static Pair<Map<IngredientComponent<?, ?>, List<?>>, Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>>>
1312
    getRecipeInputs(Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter, IRecipeDefinition recipe, boolean simulate,
1313
                    Map<IngredientComponent<?, ?>, IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemories,
1314
                    Map<IngredientComponent<?, ?>, IIngredientCollectionMutable<?, ?>> extractionMemoriesReusable,
1315
                    boolean collectMissingIngredients, long recipeOutputQuantity) {
1316
        // Determine available and missing ingredients
1317
        Map<IngredientComponent<?, ?>, List<?>> ingredientsAvailable = Maps.newIdentityHashMap();
1✔
1318
        Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>> ingredientsMissing = Maps.newIdentityHashMap();
1✔
1319
        for (IngredientComponent<?, ?> ingredientComponent : recipe.getInputComponents()) {
1✔
1320
            IIngredientComponentStorage storage = storageGetter.apply(ingredientComponent);
1✔
1321
            IngredientCollectionPrototypeMap<?, ?> simulatedExtractionMemory = simulatedExtractionMemories.get(ingredientComponent);
1✔
1322
            if (simulatedExtractionMemory == null) {
1✔
1323
                simulatedExtractionMemory = new IngredientCollectionPrototypeMap<>(ingredientComponent, true);
1✔
1324
                simulatedExtractionMemories.put(ingredientComponent, simulatedExtractionMemory);
1✔
1325
            }
1326
            IIngredientCollectionMutable extractionMemoryReusable = extractionMemoriesReusable.get(ingredientComponent);
1✔
1327
            if (extractionMemoryReusable == null) {
1✔
1328
                extractionMemoryReusable = new IngredientHashSet<>(ingredientComponent);
1✔
1329
                extractionMemoriesReusable.put(ingredientComponent, extractionMemoryReusable);
1✔
1330
            }
1331
            Pair<List<?>, MissingIngredients<?, ?>> subIngredients = getIngredientRecipeInputs(storage,
1✔
1332
                    (IngredientComponent) ingredientComponent, recipe, simulate, simulatedExtractionMemory, extractionMemoryReusable,
1333
                    collectMissingIngredients, recipeOutputQuantity);
1334
            List<?> subIngredientAvailable = subIngredients.getLeft();
1✔
1335
            MissingIngredients<?, ?> subIngredientsMissing = subIngredients.getRight();
1✔
1336
            if (subIngredientAvailable == null && !collectMissingIngredients) {
1✔
1337
                return Pair.of(null, null);
×
1338
            } else {
1339
                if (subIngredientAvailable != null && !subIngredientAvailable.isEmpty()) {
1✔
1340
                    ingredientsAvailable.put(ingredientComponent, subIngredientAvailable);
1✔
1341
                }
1342
                if (collectMissingIngredients && !subIngredientsMissing.getElements().isEmpty()) {
1✔
1343
                    ingredientsMissing.put(ingredientComponent, subIngredientsMissing);
1✔
1344
                }
1345
            }
1346
        }
1✔
1347

1348
        // Compress missing ingredients
1349
        // We do this to ensure that instances missing multiple times can be easily combined
1350
        // when triggering a crafting job for them.
1351
        Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>> ingredientsMissingCompressed = Maps.newIdentityHashMap();
1✔
1352
        for (IngredientComponent<?, ?> ingredientComponent : ingredientsMissing.keySet()) {
1✔
1353
            ingredientsMissingCompressed.put(ingredientComponent, compressMissingIngredients(ingredientsMissing.get(ingredientComponent)));
1✔
1354
        }
1✔
1355

1356
        return Pair.of(ingredientsAvailable, ingredientsMissingCompressed);
1✔
1357
    }
1358

1359
    /**
1360
     * Create a list of prototyped ingredients from the instances
1361
     * of the given ingredient component type in the given mixed ingredients.
1362
     *
1363
     * Equal prototypes will be stacked.
1364
     *
1365
     * @param ingredientComponent The ingredient component type.
1366
     * @param mixedIngredients The mixed ingredients.
1367
     * @param <T> The instance type.
1368
     * @param <M> The matching condition parameter.
1369
     * @return A list of prototypes.
1370
     */
1371
    public static <T, M> List<IPrototypedIngredient<T, M>> getCompressedIngredients(IngredientComponent<T, M> ingredientComponent,
1372
                                                                                    IMixedIngredients mixedIngredients) {
1373
        List<IPrototypedIngredient<T, M>> outputs = Lists.newArrayList();
1✔
1374

1375
        IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
1✔
1376
        for (T instance : mixedIngredients.getInstances(ingredientComponent)) {
1✔
1377
            // Try to stack this instance with an existing prototype
1378
            boolean stacked = false;
1✔
1379
            ListIterator<IPrototypedIngredient<T, M>> existingIt = outputs.listIterator();
1✔
1380
            while(existingIt.hasNext()) {
1✔
1381
                IPrototypedIngredient<T, M> prototypedIngredient = existingIt.next();
1✔
1382
                if (matcher.matches(instance, prototypedIngredient.getPrototype(),
1✔
1383
                        prototypedIngredient.getCondition())) {
1✔
1384
                    T stackedInstance = matcher.withQuantity(prototypedIngredient.getPrototype(),
1✔
1385
                            matcher.getQuantity(prototypedIngredient.getPrototype())
1✔
1386
                                    + matcher.getQuantity(instance));
1✔
1387
                    existingIt.set(new PrototypedIngredient<>(ingredientComponent, stackedInstance,
1✔
1388
                            prototypedIngredient.getCondition()));
1✔
1389
                    stacked = true;
1✔
1390
                    break;
1✔
1391
                }
1392
            }
1✔
1393

1394
            // If not possible, just append it to the list
1395
            if (!stacked) {
1✔
1396
                outputs.add(new PrototypedIngredient<>(ingredientComponent, instance,
1✔
1397
                        matcher.getExactMatchNoQuantityCondition()));
1✔
1398
            }
1399
        }
1✔
1400

1401
        return outputs;
1✔
1402
    }
1403

1404
    /**
1405
     * Compress the given missing ingredients so that equal instances just have an incremented quantity.
1406
     *
1407
     * @param missingIngredients The missing ingredients.
1408
     * @param <T> The instance type.
1409
     * @param <M> The matching condition parameter.
1410
     * @return A new missing ingredients object.
1411
     */
1412
    public static <T, M> MissingIngredients<T, M> compressMissingIngredients(MissingIngredients<T, M> missingIngredients) {
1413
        // Index identical missing ingredients in a map, to group them by quantity
1414
        Map<MissingIngredients.Element<T, M>, Long> elementsCompressedMap = Maps.newLinkedHashMap(); // Must be a linked map to maintain our order!!!
1✔
1415
        for (MissingIngredients.Element<T, M> element : missingIngredients.getElements()) {
1✔
1416
            elementsCompressedMap.merge(element, 1L, Long::sum);
1✔
1417
        }
1✔
1418

1419
        // Create a new missing ingredients list where we multiply the missing quantities
1420
        List<MissingIngredients.Element<T, M>> elementsCompressed = Lists.newArrayList();
1✔
1421
        for (Map.Entry<MissingIngredients.Element<T, M>, Long> entry : elementsCompressedMap.entrySet()) {
1✔
1422
            Long quantity = entry.getValue();
1✔
1423
            if (quantity == 1L || entry.getKey().isInputReusable()) {
1✔
1424
                elementsCompressed.add(entry.getKey());
1✔
1425
            } else {
1426
                MissingIngredients.Element<T, M> elementOld = entry.getKey();
1✔
1427
                MissingIngredients.Element<T, M> elementNewQuantity = new MissingIngredients.Element<>(
1✔
1428
                        elementOld.getAlternatives().stream()
1✔
1429
                                .map(alt -> new MissingIngredients.PrototypedWithRequested<>(alt.getRequestedPrototype(), alt.getQuantityMissing() * quantity))
1✔
1430
                                .toList(),
1✔
1431
                        elementOld.isInputReusable()
1✔
1432
                );
1433
                elementsCompressed.add(elementNewQuantity);
1✔
1434
            }
1435
        }
1✔
1436
        return new MissingIngredients<>(elementsCompressed);
1✔
1437
    }
1438

1439
    /**
1440
     * Create a collection of prototypes from the given recipe's outputs.
1441
     *
1442
     * Equal prototypes will be stacked.
1443
     *
1444
     * @param recipe A recipe.
1445
     * @return A map from ingredient component types to their list of prototypes.
1446
     */
1447
    public static Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> getRecipeOutputs(IRecipeDefinition recipe) {
1448
        Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> outputs = Maps.newHashMap();
1✔
1449

1450
        IMixedIngredients mixedIngredients = recipe.getOutput();
1✔
1451
        for (IngredientComponent ingredientComponent : mixedIngredients.getComponents()) {
1✔
1452
            outputs.put(ingredientComponent, getCompressedIngredients(ingredientComponent, mixedIngredients));
1✔
1453
        }
1✔
1454

1455
        return outputs;
1✔
1456
    }
1457

1458
    /**
1459
     * Creates a new recipe outputs object with all ingredient quantities multiplied by the given amount.
1460
     * @param recipeOutputs A recipe objects holder.
1461
     * @param amount An amount to multiply all instances by.
1462
     * @return A new recipe objects holder.
1463
     */
1464
    public static Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> multiplyRecipeOutputs(
1465
            Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> recipeOutputs, int amount) {
1466
        if (amount == 1) {
1✔
1467
            return recipeOutputs;
1✔
1468
        }
1469

1470
        Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> newRecipeOutputs = Maps.newIdentityHashMap();
1✔
1471
        for (Map.Entry<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> entry : recipeOutputs.entrySet()) {
1✔
1472
            newRecipeOutputs.put(entry.getKey(), multiplyPrototypedIngredients((List) entry.getValue(), amount));
1✔
1473
        }
1✔
1474
        return newRecipeOutputs;
1✔
1475
    }
1476

1477
    /**
1478
     * Multiply the quantity of a given prototyped ingredient list with the given amount.
1479
     * @param prototypedIngredients A prototyped ingredient list.
1480
     * @param amount An amount to multiply by.
1481
     * @param <T> The instance type.
1482
     * @param <M> The matching condition parameter.
1483
     * @return A multiplied prototyped ingredient list.
1484
     */
1485
    public static <T, M> List<IPrototypedIngredient<T, M>> multiplyPrototypedIngredients(Collection<IPrototypedIngredient<T, M>> prototypedIngredients,
1486
                                                                                         long amount) {
1487
        return prototypedIngredients
1✔
1488
                .stream()
1✔
1489
                .map(p -> multiplyPrototypedIngredient(p, amount))
1✔
1490
                .collect(Collectors.toList());
1✔
1491
    }
1492

1493
    /**
1494
     * Multiply the quantity of a given prototyped ingredient with the given amount.
1495
     * @param prototypedIngredient A prototyped ingredient.
1496
     * @param amount An amount to multiply by.
1497
     * @param <T> The instance type.
1498
     * @param <M> The matching condition parameter.
1499
     * @return A multiplied prototyped ingredient.
1500
     */
1501
    public static <T, M> IPrototypedIngredient<T, M> multiplyPrototypedIngredient(IPrototypedIngredient<T, M> prototypedIngredient,
1502
                                                                                  long amount) {
1503
        IIngredientMatcher<T, M> matcher = prototypedIngredient.getComponent().getMatcher();
1✔
1504
        return new PrototypedIngredient<>(prototypedIngredient.getComponent(),
1✔
1505
                matcher.withQuantity(prototypedIngredient.getPrototype(),
1✔
1506
                        matcher.getQuantity(prototypedIngredient.getPrototype()) * amount),
1✔
1507
                prototypedIngredient.getCondition());
1✔
1508
    }
1509

1510
    /**
1511
     * Merge two mixed ingredients in a new mixed ingredients object.
1512
     * Instances will be stacked.
1513
     * @param a A first mixed ingredients object.
1514
     * @param b A second mixed ingredients object.
1515
     * @return A merged mixed ingredients object.
1516
     */
1517
    public static IMixedIngredients mergeMixedIngredients(IMixedIngredients a, IMixedIngredients b) {
1518
        // Temporarily store instances in IngredientCollectionPrototypeMaps
1519
        Map<IngredientComponent<?, ?>, IngredientCollectionQuantitativeGrouper<?, ?, IngredientArrayList<?, ?>>> groupings = Maps.newIdentityHashMap();
1✔
1520
        for (IngredientComponent<?, ?> component : a.getComponents()) {
1✔
1521
            IngredientCollectionQuantitativeGrouper grouping = new IngredientCollectionQuantitativeGrouper<>(new IngredientArrayList<>(component));
1✔
1522
            groupings.put(component, grouping);
1✔
1523
            grouping.addAll(a.getInstances(component));
1✔
1524
        }
1✔
1525
        for (IngredientComponent<?, ?> component : b.getComponents()) {
1✔
1526
            IngredientCollectionQuantitativeGrouper grouping = groupings.get(component);
1✔
1527
            if (grouping == null) {
1✔
1528
                grouping = new IngredientCollectionQuantitativeGrouper<>(new IngredientArrayList<>(component));
1✔
1529
                groupings.put(component, grouping);
1✔
1530
            }
1531
            grouping.addAll(b.getInstances(component));
1✔
1532
        }
1✔
1533

1534
        // Convert IngredientCollectionPrototypeMaps to lists
1535
        Map<IngredientComponent<?, ?>, List<?>> ingredients = Maps.newIdentityHashMap();
1✔
1536
        for (Map.Entry<IngredientComponent<?, ?>, IngredientCollectionQuantitativeGrouper<?, ?, IngredientArrayList<?, ?>>> entry : groupings.entrySet()) {
1✔
1537
            ingredients.put(entry.getKey(), Lists.newArrayList(entry.getValue()));
1✔
1538
        }
1✔
1539
        return new MixedIngredients(ingredients);
1✔
1540
    }
1541

1542
    /**
1543
     * Stack all ingredients in the given mixed ingredients object.
1544
     * @param mixedIngredients A mixed ingredients object.
1545
     * @return A new mixed ingredients object.
1546
     */
1547
    protected static IMixedIngredients compressMixedIngredients(IMixedIngredients mixedIngredients) {
1548
        // Temporarily store instances in IngredientCollectionPrototypeMaps
1549
        Map<IngredientComponent<?, ?>, IngredientCollectionQuantitativeGrouper<?, ?, IngredientArrayList<?, ?>>> groupings = Maps.newIdentityHashMap();
1✔
1550
        for (IngredientComponent<?, ?> component : mixedIngredients.getComponents()) {
1✔
1551
            IngredientCollectionQuantitativeGrouper grouping = new IngredientCollectionQuantitativeGrouper<>(new IngredientArrayList<>(component));
1✔
1552
            groupings.put(component, grouping);
1✔
1553
            grouping.addAll(mixedIngredients.getInstances(component));
1✔
1554
        }
1✔
1555

1556
        // Convert IngredientCollectionPrototypeMaps to lists
1557
        Map<IngredientComponent<?, ?>, List<?>> ingredients = Maps.newIdentityHashMap();
1✔
1558
        for (Map.Entry<IngredientComponent<?, ?>, IngredientCollectionQuantitativeGrouper<?, ?, IngredientArrayList<?, ?>>> entry : groupings.entrySet()) {
1✔
1559
            IIngredientMatcher matcher = entry.getKey().getMatcher();
1✔
1560
            List<?> values = entry.getValue()
1✔
1561
                    .stream()
1✔
1562
                    .filter(i -> !matcher.isEmpty(i))
1✔
1563
                    .collect(Collectors.toList());
1✔
1564
            if (!values.isEmpty()) {
1✔
1565
                ingredients.put(entry.getKey(), values);
1✔
1566
            }
1567
        }
1✔
1568
        return new MixedIngredients(ingredients);
1✔
1569
    }
1570

1571
    /**
1572
     * Insert the ingredients of the given ingredient component type into the target to make it start crafting.
1573
     *
1574
     * If insertion in non-simulation mode fails,
1575
     * ingredients will be re-inserted into the network.
1576
     *
1577
     * @param ingredientComponent The ingredient component type.
1578
     * @param capabilityProvider The target capability provider.
1579
     * @param side The target side.
1580
     * @param ingredients The ingredients to insert.
1581
     * @param storageFallback The storage to insert any failed ingredients back into. Can be null in simulation mode.
1582
     * @param simulate If insertion should be simulated.
1583
     * @param <T> The instance type.
1584
     * @param <M> The matching condition parameter.
1585
     * @return If all instances could be inserted.
1586
     */
1587
    public static <T, M> boolean insertIngredientCrafting(IngredientComponent<T, M> ingredientComponent,
1588
                                                          ICapabilityProvider capabilityProvider,
1589
                                                          @Nullable Direction side,
1590
                                                          IMixedIngredients ingredients,
1591
                                                          IIngredientComponentStorage<T, M> storageFallback,
1592
                                                          boolean simulate) {
1593
        IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
×
1594
        IIngredientComponentStorage<T, M> storage = ingredientComponent.getStorage(capabilityProvider, side);
×
1595
        List<T> instances = ingredients.getInstances(ingredientComponent);
×
1596
        List<T> failedInstances = Lists.newArrayList();
×
1597
        boolean ok = true;
×
1598
        for (T instance : instances) {
×
1599
            T remaining = storage.insert(instance, simulate);
×
1600
            if (!matcher.isEmpty(remaining)) {
×
1601
                ok = false;
×
1602
                if (!simulate) {
×
1603
                    failedInstances.add(remaining);
×
1604
                }
1605
            }
1606
        }
×
1607

1608
        // If we had failed insertions, try to insert them back into the network.
1609
        for (T instance : failedInstances) {
×
1610
            T remaining = storageFallback.insert(instance, false);
×
1611
            if (!matcher.isEmpty(remaining)) {
×
1612
                throw new IllegalStateException("Insertion for a crafting recipe failed" +
×
1613
                        "due to inconsistent insertion behaviour by destination in simulation " +
1614
                        "and non-simulation: " + capabilityProvider + ". Lost: " + instances);
1615
            }
1616
        }
×
1617

1618
        return ok;
×
1619
    }
1620

1621
    /**
1622
     * Insert the ingredients of all applicable ingredient component types into the target to make it start crafting.
1623
     *
1624
     * If insertion in non-simulation mode fails,
1625
     * ingredients will be re-inserted into the network.
1626
     *
1627
     * @param targetGetter A function to get the target position.
1628
     * @param ingredients The ingredients to insert.
1629
     * @param network The network.
1630
     * @param channel The channel.
1631
     * @param simulate If insertion should be simulated.
1632
     * @return If all instances could be inserted.
1633
     */
1634
    public static boolean insertCrafting(Function<IngredientComponent<?, ?>, PartPos> targetGetter,
1635
                                         IMixedIngredients ingredients,
1636
                                         INetwork network, int channel,
1637
                                         boolean simulate) {
1638
        Map<IngredientComponent<?, ?>, BlockEntity> tileMap = Maps.newIdentityHashMap();
×
1639

1640
        // First, check if we can find valid tiles for all ingredient components
1641
        for (IngredientComponent<?, ?> ingredientComponent : ingredients.getComponents()) {
×
1642
            BlockEntity tile = BlockEntityHelpers.get(targetGetter.apply(ingredientComponent).getPos(), BlockEntity.class).orElse(null);
×
1643
            if (tile != null) {
×
1644
                tileMap.put(ingredientComponent, tile);
×
1645
            } else {
1646
                return false;
×
1647
            }
1648
        }
×
1649

1650
        // Next, insert the instances into the respective tiles
1651
        boolean ok = true;
×
1652
        for (Map.Entry<IngredientComponent<?, ?>, BlockEntity> entry : tileMap.entrySet()) {
×
1653
            IIngredientComponentStorage<?, ?> storageNetwork = simulate ? null : getNetworkStorage(network, channel, entry.getKey(), false);
×
1654
            if (!insertIngredientCrafting((IngredientComponent) entry.getKey(), entry.getValue(),
×
1655
                    targetGetter.apply(entry.getKey()).getSide(), ingredients,
×
1656
                    storageNetwork, simulate)) {
1657
                ok = false;
×
1658
            }
1659
        }
×
1660

1661
        return ok;
×
1662
    }
1663

1664
    /**
1665
     * Split the given crafting job amount into new jobs with a given split factor.
1666
     * @param craftingJob A crafting job to split.
1667
     * @param splitFactor The number of jobs to split the job over.
1668
     * @param dependencyGraph The dependency graph that will be updated if there are dependencies in the original job.
1669
     * @param identifierGenerator An identifier generator for the crafting jobs ids.
1670
     * @return The newly created crafting jobs.
1671
     */
1672
    public static List<CraftingJob> splitCraftingJobs(CraftingJob craftingJob, int splitFactor,
1673
                                                      CraftingJobDependencyGraph dependencyGraph,
1674
                                                      IIdentifierGenerator identifierGenerator) {
1675
        splitFactor = Math.min(splitFactor, craftingJob.getAmount());
1✔
1676
        int division = craftingJob.getAmount() / splitFactor;
1✔
1677
        int modulus = craftingJob.getAmount() % splitFactor;
1✔
1678

1679
        // Clone original job into splitFactor jobs
1680
        List<CraftingJob> newCraftingJobs = Lists.newArrayList();
1✔
1681
        for (int i = 0; i < splitFactor; i++) {
1✔
1682
            CraftingJob clonedJob = craftingJob.clone(identifierGenerator);
1✔
1683
            newCraftingJobs.add(clonedJob);
1✔
1684

1685
            // Update amount
1686
            int newAmount = division;
1✔
1687
            if (modulus > 0) {
1✔
1688
                // No amounts will be lost, as modulus is guaranteed to be smaller than splitFactor
1689
                newAmount++;
1✔
1690
                modulus--;
1✔
1691
            }
1692
            clonedJob.setAmount(newAmount);
1✔
1693
        }
1694

1695
        // Collect dependency links
1696
        Collection<CraftingJob> originalDependencies = dependencyGraph.getDependencies(craftingJob);
1✔
1697
        Collection<CraftingJob> originalDependents = dependencyGraph.getDependents(craftingJob);
1✔
1698

1699
        // Remove dependency links to and from the original jobs
1700
        for (CraftingJob dependency : originalDependencies) {
1✔
1701
            craftingJob.removeDependency(dependency);
1✔
1702
            dependencyGraph.removeDependency(craftingJob.getId(), dependency.getId());
1✔
1703
        }
1✔
1704
        for (CraftingJob dependent : originalDependents) {
1✔
1705
            dependent.removeDependency(craftingJob);
1✔
1706
            dependencyGraph.removeDependency(dependent.getId(), craftingJob.getId());
1✔
1707
        }
1✔
1708

1709
        // Create dependency links to and from the new crafting jobs
1710
        for (CraftingJob dependency : originalDependencies) {
1✔
1711
            for (CraftingJob newCraftingJob : newCraftingJobs) {
1✔
1712
                newCraftingJob.addDependency(dependency);
1✔
1713
                dependencyGraph.addDependency(newCraftingJob, dependency);
1✔
1714
            }
1✔
1715
        }
1✔
1716
        for (CraftingJob originalDependent : originalDependents) {
1✔
1717
            for (CraftingJob newCraftingJob : newCraftingJobs) {
1✔
1718
                originalDependent.addDependency(newCraftingJob);
1✔
1719
                dependencyGraph.addDependency(originalDependent, newCraftingJob);
1✔
1720
            }
1✔
1721
        }
1✔
1722

1723
        return newCraftingJobs;
1✔
1724
    }
1725

1726
    /**
1727
     * Insert the given ingredients into the given storage networks.
1728
     * @param ingredients A collection of ingredients.
1729
     * @param storageGetter A storage network getter.
1730
     * @param simulate If insertion should be simulated.
1731
     * @return The remaining ingredients that were not inserted.
1732
     */
1733
    public static IMixedIngredients insertIngredients(IMixedIngredients ingredients,
1734
                                                      Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
1735
                                                      boolean simulate) {
1736
        Map<IngredientComponent<?, ?>, List<?>> remainingIngredients = Maps.newIdentityHashMap();
×
1737
        for (IngredientComponent<?, ?> component : ingredients.getComponents()) {
×
1738
            IIngredientComponentStorage storage = storageGetter.apply(component);
×
1739
            IIngredientMatcher matcher = component.getMatcher();
×
1740
            for (Object instance : ingredients.getInstances(component)) {
×
1741
                Object remainder = storage.insert(instance, simulate);
×
1742
                if (!matcher.isEmpty(remainder)) {
×
1743
                    List remainingInstances = remainingIngredients.get(component);
×
1744
                    if (remainingInstances == null) {
×
1745
                        remainingInstances = Lists.newArrayList();
×
1746
                        remainingIngredients.put(component, remainingInstances);
×
1747
                    }
1748
                    remainingInstances.add(instance);
×
1749
                }
1750
            }
×
1751
        }
×
1752
        return new MixedIngredients(remainingIngredients);
×
1753
    }
1754

1755
    /**
1756
     * Insert the given ingredients into the given storage networks.
1757
     * If something fails to be inserted, produce a warning.
1758
     * @param ingredients A collection of ingredients.
1759
     * @param storageGetter A storage network getter.
1760
     * @param failureSink Ingredients that failed to be inserted will be inserted to this sink.
1761
     */
1762
    public static void insertIngredientsGuaranteed(IMixedIngredients ingredients,
1763
                                                   Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter,
1764
                                                   ICraftingResultsSink failureSink) {
1765
        IMixedIngredients remaining = insertIngredients(ingredients, storageGetter, false);
×
1766
        // If re-insertion into the network fails, insert it into the buffer of the crafting interface,
1767
        // to avoid loss of ingredients.
1768
        if (!remaining.isEmpty()) {
×
1769
            for (IngredientComponent<?, ?> component : remaining.getComponents()) {
×
1770
                for (Object instance : remaining.getInstances(component)) {
×
1771
                    failureSink.addResult((IngredientComponent) component, instance);
×
1772
                }
×
1773
            }
×
1774
        }
1775
    }
×
1776

1777
    /**
1778
     * Generates semi-unique IDs.
1779
     */
1780
    public static interface IIdentifierGenerator {
1781
        public int getNext();
1782
    }
1783

1784
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc