Skip to content

Commit dc67cd7

Browse files
DFS stack-based recipe lookup implementation (#3986)
Co-authored-by: Tech22 <37029404+techlord22@users.noreply.github.com>
1 parent 781b6e7 commit dc67cd7

File tree

4 files changed

+201
-112
lines changed

4 files changed

+201
-112
lines changed

src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ protected void regressRecipe() {
316316
}
317317

318318
public @NotNull Iterator<GTRecipe> searchRecipe() {
319-
return machine.getRecipeType().searchRecipe(machine, r -> matchRecipe(r).isSuccess());
319+
return machine.getRecipeType().searchRecipe(machine, r -> true);
320320
}
321321

322322
public void findAndHandleRecipe() {

src/main/java/com/gregtechceu/gtceu/api/recipe/lookup/RecipeDB.java

Lines changed: 71 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919

2020
import com.mojang.datafixers.util.Either;
2121
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
22-
import lombok.AccessLevel;
23-
import lombok.RequiredArgsConstructor;
2422
import org.jetbrains.annotations.*;
2523

2624
import java.util.*;
@@ -63,7 +61,7 @@ public void clear() {
6361
if (list == null) {
6462
return null;
6563
}
66-
return findRecursive(list, predicate);
64+
return find(list, predicate);
6765
}
6866

6967
/**
@@ -77,7 +75,7 @@ public void clear() {
7775
@VisibleForTesting
7876
public @Nullable GTRecipe find(@NotNull List<List<AbstractMapIngredient>> list,
7977
@NotNull Predicate<GTRecipe> predicate) {
80-
return findRecursive(list, predicate);
78+
return (new RecipeIterator(this, list, predicate)).next();
8179
}
8280

8381
/**
@@ -99,7 +97,7 @@ public void clear() {
9997
list.add(MapIngredientTypeManager.getFrom(ingredient, cap));
10098
}
10199
});
102-
return findRecursive(list, predicate);
100+
return find(list, predicate);
103101
}
104102

105103
/**
@@ -118,98 +116,6 @@ public void clear() {
118116
return new RecipeIterator(this, list, predicate);
119117
}
120118

121-
/**
122-
* Recursively finds a recipe.
123-
*
124-
* @param ingredients the ingredients to search with
125-
* @param predicate if the found recipe is valid
126-
* @return the recipe
127-
*/
128-
private @Nullable GTRecipe findRecursive(@NotNull List<List<AbstractMapIngredient>> ingredients,
129-
@NotNull Predicate<GTRecipe> predicate) {
130-
// Try each ingredient as a starting point, adding it to the skip-list.
131-
// The skip-list is a packed long, where each 1 bit represents an index to skip
132-
for (int i = 0; i < ingredients.size(); i++) {
133-
BitSet skipSet = new BitSet(ingredients.size());
134-
skipSet.set(i);
135-
GTRecipe r = findRecursive(ingredients, rootBranch, predicate, i, 0, skipSet);
136-
if (r != null) {
137-
return r;
138-
}
139-
}
140-
return null;
141-
}
142-
143-
/**
144-
* Recursively finds a recipe by checking the current branch's nodes.
145-
*
146-
* @param ingredients the ingredients to search with
147-
* @param branch the branch to search
148-
* @param predicate if the found recipe is valid
149-
* @param index the index of the ingredient list to check
150-
* @param count how deep we are in recursion, < ingredients.length
151-
* @param skip bitmask of ingredients already checked
152-
* @return the recipe
153-
*/
154-
private @Nullable GTRecipe findRecursive(@NotNull List<List<AbstractMapIngredient>> ingredients,
155-
@NotNull Branch branch, @NotNull Predicate<GTRecipe> predicate,
156-
int index, int count, @NotNull BitSet skip) {
157-
// exhausted all the ingredients, and didn't find anything
158-
if (count == ingredients.size()) {
159-
return null;
160-
}
161-
162-
// Iterate over current level of nodes.
163-
for (AbstractMapIngredient obj : ingredients.get(index)) {
164-
// determine the root nodes
165-
var nodes = nodesForIngredient(obj, branch);
166-
var result = nodes.get(obj);
167-
if (result == null) {
168-
continue;
169-
}
170-
// if there is a recipe (left mapping), return it immediately as found, if it can be handled
171-
// Otherwise, recurse and go to the next branch.
172-
GTRecipe recipe = result.map(r -> predicate.test(r) ? r : null,
173-
b -> findRecursiveDive(ingredients, b, predicate, index, count, skip));
174-
if (recipe != null) {
175-
return recipe;
176-
}
177-
}
178-
return null;
179-
}
180-
181-
/**
182-
* Recursively finds a recipe by diving deeper down a path.
183-
*
184-
* @param ingredients the ingredients to search with
185-
* @param branch the branch to search
186-
* @param predicate if the found recipe is valid
187-
* @param index the index of the ingredient list to check
188-
* @param count how deep we are in recursion, must be < ingredients.length
189-
* @param skip bitmask of ingredients already checked
190-
* @return the recipe
191-
*/
192-
private @Nullable GTRecipe findRecursiveDive(@NotNull List<List<AbstractMapIngredient>> ingredients,
193-
@NotNull Branch branch, @NotNull Predicate<GTRecipe> predicate,
194-
int index, int count, @NotNull BitSet skip) {
195-
// loop through all ingredients, wrapping around the end until all are tried.
196-
for (int i = (index + 1) % ingredients.size(); i != index; i = (i + 1) % ingredients.size()) {
197-
if (skip.get(i)) {
198-
continue;
199-
}
200-
// Recursive call
201-
// Append the current index to the skip list
202-
skip.set(i);
203-
// Increase the count, so the recursion can terminate if needed (ingredients is exhausted)
204-
GTRecipe r = findRecursive(ingredients, branch, predicate, i, count + 1, skip);
205-
if (r != null) {
206-
return r;
207-
}
208-
skip.clear(i);
209-
}
210-
return null;
211-
}
212-
213119
/**
214120
* Converts a Recipe Capability holder's handlers into a list of {@link AbstractMapIngredient}
215121
*
@@ -358,38 +264,93 @@ private boolean addRecursive(@NotNull GTRecipe recipe,
358264
return true;
359265
}
360266

361-
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
267+
private static class SearchFrame {
268+
269+
int index; // ingredient slot we’re exploring
270+
int ingredientIndex; // position within ingredients[index]
271+
Branch branch; // branch in the recipe DB
272+
273+
public SearchFrame(int index, Branch branch) {
274+
this.index = index;
275+
this.ingredientIndex = 0;
276+
this.branch = branch;
277+
}
278+
}
279+
362280
public static class RecipeIterator implements Iterator<GTRecipe> {
363281

364282
private final @NotNull RecipeDB db;
365283
private final @NotNull List<List<AbstractMapIngredient>> ingredients;
366284
private final @NotNull Predicate<GTRecipe> predicate;
367-
private int index;
285+
286+
private final Deque<SearchFrame> stack = new ArrayDeque<>();
287+
288+
@VisibleForTesting
289+
public RecipeIterator(@NotNull RecipeDB db,
290+
@NotNull List<List<AbstractMapIngredient>> ingredients,
291+
@NotNull Predicate<GTRecipe> predicate) {
292+
this.db = db;
293+
this.ingredients = ingredients;
294+
this.predicate = predicate;
295+
296+
for (int i = ingredients.size() - 1; i >= 0; i--) {
297+
stack.push(new SearchFrame(i, db.rootBranch));
298+
}
299+
}
368300

369301
@Override
370302
public boolean hasNext() {
371-
return index < ingredients.size();
303+
return !stack.isEmpty();
372304
}
373305

374306
@Override
375-
public @Nullable GTRecipe next() {
376-
while (index < ingredients.size()) {
377-
BitSet skipSet = new BitSet(ingredients.size());
378-
skipSet.set(index);
379-
GTRecipe r = db.findRecursive(ingredients, db.rootBranch, predicate, index, 0, skipSet);
380-
index++;
381-
if (r != null) {
382-
return r;
307+
public GTRecipe next() {
308+
while (!stack.isEmpty()) {
309+
// We stay on one frame until all ingredients have been checked
310+
SearchFrame frame = stack.peek();
311+
312+
if (frame.ingredientIndex >= ingredients.get(frame.index).size()) {
313+
stack.pop();
314+
continue;
383315
}
316+
317+
List<AbstractMapIngredient> ingredientList = ingredients.get(frame.index);
318+
AbstractMapIngredient ingredient = ingredientList.get(frame.ingredientIndex);
319+
// Increment candidate pos for next iteration
320+
frame.ingredientIndex++;
321+
var nodes = nodesForIngredient(ingredient, frame.branch);
322+
var result = nodes.get(ingredient);
323+
if (result == null) {
324+
continue;
325+
}
326+
327+
// Option 1: It's a recipe
328+
if (result.left().isPresent()) {
329+
var recipe = result.left().get();
330+
if (predicate.test(recipe)) {
331+
return recipe;
332+
}
333+
}
334+
335+
// Option 2: It's a branch, dive deeper
336+
result.ifRight(b -> {
337+
for (int j = ingredients.size() - 1; j >= 0; j--) {
338+
stack.push(new SearchFrame(j, b));
339+
}
340+
});
384341
}
385-
return null;
342+
343+
return null; // no more recipes
386344
}
387345

388346
/**
389347
* Reset the iterator
390348
*/
391349
public void reset() {
392-
this.index = 0;
350+
stack.clear();
351+
for (int i = ingredients.size() - 1; i >= 0; i--) {
352+
stack.push(new SearchFrame(i, db.rootBranch));
353+
}
393354
}
394355
}
395356
}

src/test/java/com/gregtechceu/gtceu/api/recipe/lookup/GTRecipeLookupTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static void prepare(ServerLevel level) {
4747
.inputItems(Items.COBBLESTONE, 1)
4848
.outputItems(Items.STONE, 1)
4949
.buildRawRecipe();
50-
GTRecipe SMELT_ACACIA_WOOD = recipeType.recipeBuilder("smelt_acacia_wood")
50+
SMELT_ACACIA_WOOD = recipeType.recipeBuilder("smelt_acacia_wood")
5151
.inputItems(Items.ACACIA_WOOD, 1)
5252
.outputItems(Items.CHARCOAL, 1)
5353
.buildRawRecipe();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.gregtechceu.gtceu.gametest.stresstest;
2+
3+
import com.gregtechceu.gtceu.GTCEu;
4+
import com.gregtechceu.gtceu.api.GTValues;
5+
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
6+
import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine;
7+
import com.gregtechceu.gtceu.api.recipe.GTRecipeType;
8+
import com.gregtechceu.gtceu.api.recipe.lookup.RecipeDB;
9+
import com.gregtechceu.gtceu.api.recipe.lookup.ingredient.AbstractMapIngredient;
10+
import com.gregtechceu.gtceu.api.recipe.lookup.ingredient.MapIngredientTypeManager;
11+
import com.gregtechceu.gtceu.common.data.GTRecipeTypes;
12+
import com.gregtechceu.gtceu.common.machine.multiblock.part.FluidHatchPartMachine;
13+
import com.gregtechceu.gtceu.common.machine.multiblock.part.ItemBusPartMachine;
14+
import com.gregtechceu.gtceu.gametest.util.TestUtils;
15+
16+
import net.minecraft.core.BlockPos;
17+
import net.minecraft.core.registries.BuiltInRegistries;
18+
import net.minecraft.gametest.framework.BeforeBatch;
19+
import net.minecraft.gametest.framework.GameTest;
20+
import net.minecraft.gametest.framework.GameTestHelper;
21+
import net.minecraft.server.level.ServerLevel;
22+
import net.minecraft.world.item.ItemStack;
23+
import net.minecraft.world.item.Items;
24+
import net.minecraft.world.item.crafting.Ingredient;
25+
import net.minecraft.world.level.block.Blocks;
26+
import net.minecraftforge.gametest.GameTestHolder;
27+
import net.minecraftforge.gametest.PrefixGameTestTemplate;
28+
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
32+
import static com.gregtechceu.gtceu.gametest.util.TestUtils.getMetaMachine;
33+
34+
@PrefixGameTestTemplate(false)
35+
@GameTestHolder(GTCEu.MOD_ID)
36+
public class RecipeIteratorStressTest {
37+
38+
private static final boolean DO_RUN_RECIPE_ITERATOR_STRESSTEST = false;
39+
40+
private static GTRecipeType LCR_RECIPE_TYPE;
41+
42+
@BeforeBatch(batch = "StressTests")
43+
public static void prepare(ServerLevel level) {
44+
LCR_RECIPE_TYPE = TestUtils.createRecipeType("stress_tests", 3, 3, 3, 3);
45+
// Force insert the recipe into the manager.
46+
LCR_RECIPE_TYPE.getAdditionHandler().beginStaging();
47+
LCR_RECIPE_TYPE.getAdditionHandler().addStaging(LCR_RECIPE_TYPE
48+
.recipeBuilder(GTCEu.id("test_multiblock_stress_tests"))
49+
.inputItems(new ItemStack(Blocks.COBBLESTONE), new ItemStack(Blocks.ACACIA_WOOD))
50+
.outputItems(new ItemStack(Blocks.STONE))
51+
.EUt(GTValues.VA[GTValues.HV]).duration(1)
52+
// NBT has a schematic in it with an HV energy input hatch
53+
.buildRawRecipe());
54+
LCR_RECIPE_TYPE.getAdditionHandler().completeStaging();
55+
}
56+
57+
private static BusHolder getBussesAndForm(GameTestHelper helper) {
58+
WorkableMultiblockMachine controller = (WorkableMultiblockMachine) getMetaMachine(
59+
helper.getBlockEntity(new BlockPos(1, 2, 0)));
60+
TestUtils.formMultiblock(controller);
61+
controller.setRecipeType(LCR_RECIPE_TYPE);
62+
ItemBusPartMachine inputBus1 = (ItemBusPartMachine) getMetaMachine(
63+
helper.getBlockEntity(new BlockPos(2, 1, 0)));
64+
ItemBusPartMachine inputBus2 = (ItemBusPartMachine) getMetaMachine(
65+
helper.getBlockEntity(new BlockPos(2, 2, 0)));
66+
ItemBusPartMachine outputBus1 = (ItemBusPartMachine) getMetaMachine(
67+
helper.getBlockEntity(new BlockPos(0, 1, 0)));
68+
FluidHatchPartMachine outputHatch1 = (FluidHatchPartMachine) getMetaMachine(
69+
helper.getBlockEntity(new BlockPos(0, 2, 0)));
70+
return new BusHolder(inputBus1, inputBus2, outputBus1, outputHatch1, controller);
71+
}
72+
73+
private record BusHolder(ItemBusPartMachine inputBus1, ItemBusPartMachine inputBus2, ItemBusPartMachine outputBus1,
74+
FluidHatchPartMachine outputHatch1, WorkableMultiblockMachine controller) {}
75+
76+
@GameTest(template = "empty", batch = "StressTests")
77+
public static void iteratorStressTest(GameTestHelper helper) {
78+
if (!DO_RUN_RECIPE_ITERATOR_STRESSTEST) {
79+
helper.succeed();
80+
return;
81+
}
82+
List<List<AbstractMapIngredient>> list = new ArrayList();
83+
for (var item : BuiltInRegistries.ITEM) {
84+
list.add(MapIngredientTypeManager.getFrom(Ingredient.of(item), ItemRecipeCapability.CAP));
85+
}
86+
for (var block : BuiltInRegistries.BLOCK) {
87+
list.add(MapIngredientTypeManager.getFrom(Ingredient.of(block), ItemRecipeCapability.CAP));
88+
}
89+
90+
long start = System.nanoTime();
91+
92+
long currentIterator = 0;
93+
for (int i = 0; i < 20; i++) {
94+
RecipeDB.RecipeIterator iterator = new RecipeDB.RecipeIterator(GTRecipeTypes.ASSEMBLER_RECIPES.db(), list,
95+
(ignored) -> true);
96+
while (iterator.hasNext()) {
97+
var recipe = iterator.next();
98+
currentIterator++;
99+
}
100+
}
101+
long end = System.nanoTime();
102+
GTCEu.LOGGER.info("current iterator recipes: " + currentIterator / 100);
103+
GTCEu.LOGGER.info("Lookup in big tree Took " + (end - start) / 1_000_000.0 + " ms");
104+
helper.succeed();
105+
}
106+
107+
@GameTest(template = "lcr_input_separation", batch = "StressTests")
108+
public static void iteratorOnMachineStressTest(GameTestHelper helper) {
109+
if (!DO_RUN_RECIPE_ITERATOR_STRESSTEST) {
110+
helper.succeed();
111+
return;
112+
}
113+
114+
BusHolder busHolder = getBussesAndForm(helper);
115+
busHolder.inputBus1.getInventory().setStackInSlot(0, new ItemStack(Items.COBBLESTONE));
116+
busHolder.inputBus1.getInventory().setStackInSlot(1, new ItemStack(Blocks.ACACIA_WOOD));
117+
busHolder.controller.recipeLogic.searchRecipe();
118+
long start = System.nanoTime();
119+
120+
for (int i = 0; i < 1000; i++) {
121+
busHolder.controller.recipeLogic.findAndHandleRecipe();
122+
busHolder.controller.recipeLogic.markLastRecipeDirty();
123+
}
124+
long end = System.nanoTime();
125+
GTCEu.LOGGER.info("On machine took " + (end - start) / 1_000_000.0 + " ms");
126+
helper.succeed();
127+
}
128+
}

0 commit comments

Comments
 (0)