1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
|
package gregtech.api.recipe;
import static gregtech.api.util.GTRecipeBuilder.ENABLE_COLLISION_CHECK;
import static gregtech.api.util.GTRecipeBuilder.handleInvalidRecipe;
import static gregtech.api.util.GTRecipeBuilder.handleRecipeCollision;
import static gregtech.api.util.GTUtility.areStacksEqualOrNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import net.minecraft.item.ItemStack;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidStack;
import org.jetbrains.annotations.Unmodifiable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import gregtech.api.GregTechAPI;
import gregtech.api.interfaces.IRecipeMap;
import gregtech.api.objects.GTItemStack;
import gregtech.api.util.GTOreDictUnificator;
import gregtech.api.util.GTRecipe;
import gregtech.api.util.GTRecipeBuilder;
import gregtech.api.util.GTStreamUtil;
import gregtech.api.util.MethodsReturnNonnullByDefault;
/**
* Responsible for recipe addition / search for recipemap.
* <p>
* In order to bind custom backend to recipemap, use {@link RecipeMapBuilder#of(String, BackendCreator)}.
*/
@SuppressWarnings("unused")
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
public class RecipeMapBackend {
private RecipeMap<?> recipeMap;
/**
* Recipe index based on items.
*/
private final SetMultimap<GTItemStack, GTRecipe> itemIndex = HashMultimap.create();
/**
* Recipe index based on fluids.
*/
private final SetMultimap<String, GTRecipe> fluidIndex = HashMultimap.create();
/**
* All the recipes belonging to this backend, indexed by recipe category.
*/
private final Map<RecipeCategory, Collection<GTRecipe>> recipesByCategory = new HashMap<>();
/**
* List of recipemaps that also receive recipe addition from this backend.
*/
private final List<IRecipeMap> downstreams = new ArrayList<>(0);
/**
* All the properties specific to this backend.
*/
protected final RecipeMapBackendProperties properties;
public RecipeMapBackend(RecipeMapBackendPropertiesBuilder propertiesBuilder) {
this.properties = propertiesBuilder.build();
GregTechAPI.itemStackMultiMaps.add(itemIndex);
}
void setRecipeMap(RecipeMap<?> recipeMap) {
this.recipeMap = recipeMap;
}
/**
* @return Properties specific to this backend.
*/
public RecipeMapBackendProperties getProperties() {
return properties;
}
/**
* @return All the recipes belonging to this backend. Returned collection is immutable,
* use {@link #compileRecipe} to add / {@link #removeRecipes} to remove.
*/
@Unmodifiable
public Collection<GTRecipe> getAllRecipes() {
return Collections.unmodifiableCollection(allRecipes());
}
/**
* @return Raw recipe list
*/
private Collection<GTRecipe> allRecipes() {
return recipesByCategory.values()
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* @return All the recipes belonging to this backend, indexed by recipe category.
*/
@Unmodifiable
public Collection<GTRecipe> getRecipesByCategory(RecipeCategory recipeCategory) {
return Collections
.unmodifiableCollection(recipesByCategory.getOrDefault(recipeCategory, Collections.emptyList()));
}
@Unmodifiable
public Map<RecipeCategory, Collection<GTRecipe>> getRecipeCategoryMap() {
return Collections.unmodifiableMap(recipesByCategory);
}
// region add recipe
/**
* Adds the supplied recipe to the recipe list and index, without any check.
*
* @return Supplied recipe.
*/
public GTRecipe compileRecipe(GTRecipe recipe) {
if (recipe.getRecipeCategory() == null) {
recipe.setRecipeCategory(recipeMap.getDefaultRecipeCategory());
}
recipesByCategory.computeIfAbsent(recipe.getRecipeCategory(), v -> new ArrayList<>())
.add(recipe);
for (FluidStack fluid : recipe.mFluidInputs) {
if (fluid == null) continue;
fluidIndex.put(
fluid.getFluid()
.getName(),
recipe);
}
return addToItemMap(recipe);
}
/**
* Adds the supplied recipe to the item cache.
*/
protected GTRecipe addToItemMap(GTRecipe recipe) {
for (ItemStack item : recipe.mInputs) {
if (item == null) continue;
itemIndex.put(new GTItemStack(item), recipe);
}
if (recipe instanceof GTRecipe.GTRecipe_WithAlt recipeWithAlt) {
for (ItemStack[] itemStacks : recipeWithAlt.mOreDictAlt) {
if (itemStacks == null) continue;
for (ItemStack item : itemStacks) {
if (item == null) continue;
itemIndex.put(new GTItemStack(item), recipe);
}
}
}
return recipe;
}
/**
* Builds recipe from supplied recipe builder and adds it.
*/
protected Collection<GTRecipe> doAdd(GTRecipeBuilder builder) {
Iterable<? extends GTRecipe> recipes = properties.recipeEmitter.apply(builder);
Collection<GTRecipe> ret = new ArrayList<>();
for (GTRecipe recipe : recipes) {
if (recipe.mFluidInputs.length < properties.minFluidInputs
|| recipe.mInputs.length < properties.minItemInputs) {
return Collections.emptyList();
}
if (properties.recipeTransformer != null) {
recipe = properties.recipeTransformer.apply(recipe);
}
if (recipe == null) continue;
if (builder.isCheckForCollision() && ENABLE_COLLISION_CHECK && checkCollision(recipe)) {
handleCollision(recipe);
continue;
}
if (recipe.getRecipeCategory() != null && recipe.getRecipeCategory().recipeMap != this.recipeMap) {
handleInvalidRecipe();
continue;
}
ret.add(compileRecipe(recipe));
}
if (!ret.isEmpty()) {
builder.clearInvalid();
for (IRecipeMap downstream : downstreams) {
downstream.doAdd(builder);
}
}
return ret;
}
private void handleCollision(GTRecipe recipe) {
StringBuilder errorInfo = new StringBuilder();
boolean hasAnEntry = false;
for (FluidStack fluid : recipe.mFluidInputs) {
if (fluid == null) {
continue;
}
String s = fluid.getLocalizedName();
if (s == null) {
continue;
}
if (hasAnEntry) {
errorInfo.append("+")
.append(s);
} else {
errorInfo.append(s);
}
hasAnEntry = true;
}
for (ItemStack item : recipe.mInputs) {
if (item == null) {
continue;
}
String itemName = item.getDisplayName();
if (hasAnEntry) {
errorInfo.append("+")
.append(itemName);
} else {
errorInfo.append(itemName);
}
hasAnEntry = true;
}
handleRecipeCollision(errorInfo.toString());
}
void addDownstream(IRecipeMap downstream) {
downstreams.add(downstream);
}
/**
* Removes supplied recipes from recipe list. Do not use unless absolute necessity!
*/
public void removeRecipes(Collection<? extends GTRecipe> recipesToRemove) {
for (Collection<GTRecipe> recipes : recipesByCategory.values()) {
recipes.removeAll(recipesToRemove);
}
for (GTItemStack key : new HashMap<>(itemIndex.asMap()).keySet()) {
itemIndex.get(key)
.removeAll(recipesToRemove);
}
for (String key : new HashMap<>(fluidIndex.asMap()).keySet()) {
fluidIndex.get(key)
.removeAll(recipesToRemove);
}
}
/**
* Removes supplied recipe from recipe list. Do not use unless absolute necessity!
*/
public void removeRecipe(GTRecipe recipe) {
removeRecipes(Collections.singleton(recipe));
}
/**
* If you want to shoot your foot...
*/
public void clearRecipes() {
recipesByCategory.clear();
}
// endregion
/**
* Re-unificates all the items present in recipes. Also reflects recipe removals.
*/
public void reInit() {
itemIndex.clear();
for (GTRecipe recipe : allRecipes()) {
GTOreDictUnificator.setStackArray(true, true, recipe.mInputs);
GTOreDictUnificator.setStackArray(true, true, recipe.mOutputs);
addToItemMap(recipe);
}
}
/**
* @return If supplied item is a valid input for any of the recipes
*/
public boolean containsInput(ItemStack item) {
return itemIndex.containsKey(new GTItemStack(item)) || itemIndex.containsKey(new GTItemStack(item, true));
}
/**
* @return If supplied fluid is a valid input for any of the recipes
*/
public boolean containsInput(Fluid fluid) {
return fluidIndex.containsKey(fluid.getName());
}
// region find recipe
/**
* Checks if given recipe conflicts with already registered recipes.
*
* @return True if collision is found.
*/
boolean checkCollision(GTRecipe recipe) {
return matchRecipeStream(recipe.mInputs, recipe.mFluidInputs, null, null, false, true, true).findAny()
.isPresent();
}
/**
* Overwrites {@link #matchRecipeStream} method. Also override {@link #doesOverwriteFindRecipe} to make it work.
*/
@Nullable
protected GTRecipe overwriteFindRecipe(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot,
@Nullable GTRecipe cachedRecipe) {
return null;
}
/**
* @return Whether to use {@link #overwriteFindRecipe} for finding recipe.
*/
protected boolean doesOverwriteFindRecipe() {
return false;
}
/**
* Modifies successfully found recipe. Make sure not to mutate the found recipe but use copy!
*/
@Nullable
protected GTRecipe modifyFoundRecipe(GTRecipe recipe, ItemStack[] items, FluidStack[] fluids,
@Nullable ItemStack specialSlot) {
return recipe;
}
/**
* Called when {@link #matchRecipeStream} cannot find recipe.
*/
@Nullable
protected GTRecipe findFallback(ItemStack[] items, FluidStack[] fluids, @Nullable ItemStack specialSlot) {
return null;
}
/**
* Returns all the matched recipes in the form of Stream, without any additional check for matches.
*
* @param rawItems Item inputs.
* @param fluids Fluid inputs.
* @param specialSlot Content of the special slot. Normal recipemaps don't need this, but some do.
* Set {@link RecipeMapBuilder#specialSlotSensitive} to make it actually functional.
* Alternatively overriding {@link #filterFindRecipe} will also work.
* @param cachedRecipe If this is not null, this method tests it before all other recipes.
* @param notUnificated If this is set to true, item inputs will be unificated.
* @param dontCheckStackSizes If this is set to true, this method won't check item count and fluid amount
* for the matched recipe.
* @param forCollisionCheck If this method is called to check collision with already registered recipes.
* @return Stream of matches recipes.
*/
Stream<GTRecipe> matchRecipeStream(ItemStack[] rawItems, FluidStack[] fluids, @Nullable ItemStack specialSlot,
@Nullable GTRecipe cachedRecipe, boolean notUnificated, boolean dontCheckStackSizes,
boolean forCollisionCheck) {
if (doesOverwriteFindRecipe()) {
return GTStreamUtil.ofNullable(overwriteFindRecipe(rawItems, fluids, specialSlot, cachedRecipe));
}
if (recipesByCategory.isEmpty()) {
return Stream.empty();
}
// Some recipe classes require a certain amount of inputs of certain kinds. Like "at least 1 fluid + 1 item"
// or "at least 2 items" before they start searching for recipes.
// This improves performance massively, especially when people leave things like programmed circuits,
// molds or shapes in their machines.
// For checking collision, we assume min inputs check already has been passed as of building the recipe.
if (!forCollisionCheck) {
if (properties.minFluidInputs > 0) {
int count = 0;
for (FluidStack fluid : fluids) if (fluid != null) count++;
if (count < properties.minFluidInputs) {
return Stream.empty();
}
}
if (properties.minItemInputs > 0) {
int count = 0;
for (ItemStack item : rawItems) if (item != null) count++;
if (count < properties.minItemInputs) {
return Stream.empty();
}
}
}
ItemStack[] items;
// Unification happens here in case the item input isn't already unificated.
if (notUnificated) {
items = GTOreDictUnificator.getStackArray(true, (Object[]) rawItems);
} else {
items = rawItems;
}
return Stream.<Stream<GTRecipe>>of(
// Check the recipe which has been used last time in order to not have to search for it again, if possible.
GTStreamUtil.ofNullable(cachedRecipe)
.filter(recipe -> recipe.mCanBeBuffered)
.filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
.map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
.filter(Objects::nonNull),
// Now look for the recipes inside the item index, but only when the recipes actually can have items inputs.
GTStreamUtil.ofConditional(!itemIndex.isEmpty(), items)
.filter(Objects::nonNull)
.flatMap(item -> Stream.of(new GTItemStack(item), new GTItemStack(item, true)))
.map(itemIndex::get)
.flatMap(Collection::stream)
.filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
.map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
.filter(Objects::nonNull),
// If the minimum amount of items required for the recipes is 0, then it could match to fluid-only recipes,
// so check fluid index too.
GTStreamUtil.ofConditional(properties.minItemInputs == 0, fluids)
.filter(Objects::nonNull)
.map(
fluidStack -> fluidIndex.get(
fluidStack.getFluid()
.getName()))
.flatMap(Collection::stream)
.filter(recipe -> filterFindRecipe(recipe, items, fluids, specialSlot, dontCheckStackSizes))
.map(recipe -> modifyFoundRecipe(recipe, items, fluids, specialSlot))
.filter(Objects::nonNull),
// Lastly, find fallback.
forCollisionCheck ? Stream.empty()
: GTStreamUtil.ofSupplier(() -> findFallback(items, fluids, specialSlot))
.filter(Objects::nonNull))
.flatMap(Function.identity());
}
/**
* The minimum filter required for recipe match logic. You can override this to have custom validation.
* <p>
* Other checks like machine voltage will be done in another places.
* <p>
* Note that this won't be called if {@link #doesOverwriteFindRecipe} is true.
*/
protected boolean filterFindRecipe(GTRecipe recipe, ItemStack[] items, FluidStack[] fluids,
@Nullable ItemStack specialSlot, boolean dontCheckStackSizes) {
if (recipe.mEnabled && !recipe.mFakeRecipe
&& recipe.isRecipeInputEqual(false, dontCheckStackSizes, fluids, items)) {
return !properties.specialSlotSensitive
|| areStacksEqualOrNull((ItemStack) recipe.mSpecialItems, specialSlot);
}
return false;
}
// endregion
@FunctionalInterface
public interface BackendCreator<B extends RecipeMapBackend> {
/**
* @see RecipeMapBackend#RecipeMapBackend
*/
B create(RecipeMapBackendPropertiesBuilder propertiesBuilder);
}
}
|