package net.minecraft.world.item.crafting; import com.google.common.annotations.VisibleForTesting; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.logging.LogUtils; import com.mojang.serialization.JsonOps; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.SortedMap; import java.util.TreeMap; import java.util.function.Consumer; import java.util.stream.Collectors; import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.Registries; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.item.Item; import net.minecraft.world.item.crafting.SelectableRecipe.SingleInputEntry; import net.minecraft.world.item.crafting.SelectableRecipe.SingleInputSet; import net.minecraft.world.item.crafting.display.RecipeDisplay; import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; import net.minecraft.world.item.crafting.display.RecipeDisplayId; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class RecipeManager extends SimplePreparableReloadListener implements RecipeAccess { private static final Logger LOGGER = LogUtils.getLogger(); private static final Map, RecipeManager.IngredientExtractor> RECIPE_PROPERTY_SETS = Map.of( RecipePropertySet.SMITHING_ADDITION, (RecipeManager.IngredientExtractor)recipe -> recipe instanceof SmithingRecipe smithingRecipe ? smithingRecipe.additionIngredient() : Optional.empty(), RecipePropertySet.SMITHING_BASE, (RecipeManager.IngredientExtractor)recipe -> recipe instanceof SmithingRecipe smithingRecipe ? Optional.of(smithingRecipe.baseIngredient()) : Optional.empty(), RecipePropertySet.SMITHING_TEMPLATE, (RecipeManager.IngredientExtractor)recipe -> recipe instanceof SmithingRecipe smithingRecipe ? smithingRecipe.templateIngredient() : Optional.empty(), RecipePropertySet.FURNACE_INPUT, forSingleInput(RecipeType.SMELTING), RecipePropertySet.BLAST_FURNACE_INPUT, forSingleInput(RecipeType.BLASTING), RecipePropertySet.SMOKER_INPUT, forSingleInput(RecipeType.SMOKING), RecipePropertySet.CAMPFIRE_INPUT, forSingleInput(RecipeType.CAMPFIRE_COOKING) ); private static final FileToIdConverter RECIPE_LISTER = FileToIdConverter.registry(Registries.RECIPE); private final HolderLookup.Provider registries; private RecipeMap recipes = RecipeMap.EMPTY; private Map, RecipePropertySet> propertySets = Map.of(); private SingleInputSet stonecutterRecipes = SingleInputSet.empty(); private List allDisplays = List.of(); private Map>, List> recipeToDisplay = Map.of(); public RecipeManager(HolderLookup.Provider registries) { this.registries = registries; } protected RecipeMap prepare(ResourceManager resourceManager, ProfilerFiller profilerFiller) { SortedMap> sortedMap = new TreeMap(); SimpleJsonResourceReloadListener.scanDirectory( resourceManager, RECIPE_LISTER, this.registries.createSerializationContext(JsonOps.INSTANCE), Recipe.CODEC, sortedMap ); List> list = new ArrayList(sortedMap.size()); sortedMap.forEach((resourceLocation, recipe) -> { ResourceKey> resourceKey = ResourceKey.create(Registries.RECIPE, resourceLocation); RecipeHolder recipeHolder = new RecipeHolder(resourceKey, recipe); list.add(recipeHolder); }); return RecipeMap.create(list); } protected void apply(RecipeMap recipeMap, ResourceManager resourceManager, ProfilerFiller profilerFiller) { this.recipes = recipeMap; LOGGER.info("Loaded {} recipes", recipeMap.values().size()); } public void finalizeRecipeLoading(FeatureFlagSet enabledFeatures) { List> list = new ArrayList(); List list2 = RECIPE_PROPERTY_SETS.entrySet() .stream() .map(entry -> new RecipeManager.IngredientCollector((ResourceKey)entry.getKey(), (RecipeManager.IngredientExtractor)entry.getValue())) .toList(); this.recipes .values() .forEach( recipeHolder -> { Recipe recipe = recipeHolder.value(); if (!recipe.isSpecial() && recipe.placementInfo().isImpossibleToPlace()) { LOGGER.warn("Recipe {} can't be placed due to empty ingredients and will be ignored", recipeHolder.id().location()); } else { list2.forEach(ingredientCollector -> ingredientCollector.accept(recipe)); if (recipe instanceof StonecutterRecipe stonecutterRecipe && isIngredientEnabled(enabledFeatures, stonecutterRecipe.input()) && stonecutterRecipe.resultDisplay().isEnabled(enabledFeatures)) { list.add(new SingleInputEntry(stonecutterRecipe.input(), new SelectableRecipe(stonecutterRecipe.resultDisplay(), Optional.of(recipeHolder)))); } } } ); this.propertySets = (Map, RecipePropertySet>)list2.stream() .collect( Collectors.toUnmodifiableMap(ingredientCollector -> ingredientCollector.key, ingredientCollector -> ingredientCollector.asPropertySet(enabledFeatures)) ); this.stonecutterRecipes = new SingleInputSet<>(list); this.allDisplays = unpackRecipeInfo(this.recipes.values(), enabledFeatures); this.recipeToDisplay = (Map>, List>)this.allDisplays .stream() .collect(Collectors.groupingBy(serverDisplayInfo -> serverDisplayInfo.parent.id(), IdentityHashMap::new, Collectors.toList())); } static List filterDisabled(FeatureFlagSet enabledFeatures, List ingredients) { ingredients.removeIf(ingredient -> !isIngredientEnabled(enabledFeatures, ingredient)); return ingredients; } private static boolean isIngredientEnabled(FeatureFlagSet enabledFeatures, Ingredient ingredient) { return ingredient.items().allMatch(holder -> ((Item)holder.value()).isEnabled(enabledFeatures)); } public > Optional> getRecipeFor( RecipeType recipeType, I input, Level level, @Nullable ResourceKey> recipe ) { RecipeHolder recipeHolder = recipe != null ? this.byKeyTyped(recipeType, recipe) : null; return this.getRecipeFor(recipeType, input, level, recipeHolder); } public > Optional> getRecipeFor( RecipeType recipeType, I input, Level level, @Nullable RecipeHolder lastRecipe ) { return lastRecipe != null && lastRecipe.value().matches(input, level) ? Optional.of(lastRecipe) : this.getRecipeFor(recipeType, input, level); } public > Optional> getRecipeFor(RecipeType recipeType, I input, Level level) { return this.recipes.getRecipesFor(recipeType, input, level).findFirst(); } public Optional> byKey(ResourceKey> key) { return Optional.ofNullable(this.recipes.byKey(key)); } @Nullable private > RecipeHolder byKeyTyped(RecipeType type, ResourceKey> key) { RecipeHolder recipeHolder = this.recipes.byKey(key); return (RecipeHolder)(recipeHolder != null && recipeHolder.value().getType().equals(type) ? recipeHolder : null); } public Map, RecipePropertySet> getSynchronizedItemProperties() { return this.propertySets; } public SingleInputSet getSynchronizedStonecutterRecipes() { return this.stonecutterRecipes; } @Override public RecipePropertySet propertySet(ResourceKey propertySet) { return (RecipePropertySet)this.propertySets.getOrDefault(propertySet, RecipePropertySet.EMPTY); } @Override public SingleInputSet stonecutterRecipes() { return this.stonecutterRecipes; } public Collection> getRecipes() { return this.recipes.values(); } @Nullable public RecipeManager.ServerDisplayInfo getRecipeFromDisplay(RecipeDisplayId display) { return (RecipeManager.ServerDisplayInfo)this.allDisplays.get(display.index()); } public void listDisplaysForRecipe(ResourceKey> recipe, Consumer output) { List list = (List)this.recipeToDisplay.get(recipe); if (list != null) { list.forEach(serverDisplayInfo -> output.accept(serverDisplayInfo.display)); } } @VisibleForTesting protected static RecipeHolder fromJson(ResourceKey> recipe, JsonObject json, HolderLookup.Provider registries) { Recipe recipe2 = Recipe.CODEC.parse(registries.createSerializationContext(JsonOps.INSTANCE), json).getOrThrow(JsonParseException::new); return new RecipeHolder<>(recipe, recipe2); } public static > RecipeManager.CachedCheck createCheck(RecipeType recipeType) { return new RecipeManager.CachedCheck() { @Nullable private ResourceKey> lastRecipe; @Override public Optional> getRecipeFor(I input, ServerLevel level) { RecipeManager recipeManager = level.recipeAccess(); Optional> optional = recipeManager.getRecipeFor(recipeType, input, level, this.lastRecipe); if (optional.isPresent()) { RecipeHolder recipeHolder = (RecipeHolder)optional.get(); this.lastRecipe = recipeHolder.id(); return Optional.of(recipeHolder); } else { return Optional.empty(); } } }; } private static List unpackRecipeInfo(Iterable> recipes, FeatureFlagSet enabledFeatures) { List list = new ArrayList(); Object2IntMap object2IntMap = new Object2IntOpenHashMap<>(); for (RecipeHolder recipeHolder : recipes) { Recipe recipe = recipeHolder.value(); OptionalInt optionalInt; if (recipe.group().isEmpty()) { optionalInt = OptionalInt.empty(); } else { optionalInt = OptionalInt.of(object2IntMap.computeIfAbsent(recipe.group(), object -> object2IntMap.size())); } Optional> optional; if (recipe.isSpecial()) { optional = Optional.empty(); } else { optional = Optional.of(recipe.placementInfo().ingredients()); } for (RecipeDisplay recipeDisplay : recipe.display()) { if (recipeDisplay.isEnabled(enabledFeatures)) { int i = list.size(); RecipeDisplayId recipeDisplayId = new RecipeDisplayId(i); RecipeDisplayEntry recipeDisplayEntry = new RecipeDisplayEntry(recipeDisplayId, recipeDisplay, optionalInt, recipe.recipeBookCategory(), optional); list.add(new RecipeManager.ServerDisplayInfo(recipeDisplayEntry, recipeHolder)); } } } return list; } private static RecipeManager.IngredientExtractor forSingleInput(RecipeType recipeType) { return recipe -> recipe.getType() == recipeType && recipe instanceof SingleItemRecipe singleItemRecipe ? Optional.of(singleItemRecipe.input()) : Optional.empty(); } public interface CachedCheck> { Optional> getRecipeFor(I input, ServerLevel level); } public static class IngredientCollector implements Consumer> { final ResourceKey key; private final RecipeManager.IngredientExtractor extractor; private final List ingredients = new ArrayList(); protected IngredientCollector(ResourceKey key, RecipeManager.IngredientExtractor extractor) { this.key = key; this.extractor = extractor; } public void accept(Recipe recipe) { this.extractor.apply(recipe).ifPresent(this.ingredients::add); } public RecipePropertySet asPropertySet(FeatureFlagSet enabledFeatures) { return RecipePropertySet.create(RecipeManager.filterDisabled(enabledFeatures, this.ingredients)); } } @FunctionalInterface public interface IngredientExtractor { Optional apply(Recipe recipe); } public record ServerDisplayInfo(RecipeDisplayEntry display, RecipeHolder parent) { } }