/* * Ex Deorum * Copyright (c) 2023 thedarkcolour * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package thedarkcolour.exdeorum.recipe; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.GsonHelper; import net.minecraft.world.Container; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.storage.loot.LootDataType; import net.minecraft.world.level.storage.loot.providers.number.BinomialDistributionGenerator; import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; import net.minecraft.world.level.storage.loot.providers.number.NumberProvider; import net.minecraft.world.level.storage.loot.providers.number.NumberProviders; import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator; import net.minecraftforge.common.util.Lazy; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.registries.ForgeRegistries; import org.jetbrains.annotations.Nullable; import thedarkcolour.exdeorum.recipe.barrel.BarrelCompostRecipe; import thedarkcolour.exdeorum.recipe.barrel.BarrelMixingRecipe; import thedarkcolour.exdeorum.recipe.crucible.CrucibleRecipe; import thedarkcolour.exdeorum.recipe.sieve.SieveRecipe; import thedarkcolour.exdeorum.registry.ERecipeTypes; import java.time.Duration; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Supplier; public final class RecipeUtil { private static final int CONSTANT_TYPE = 1; private static final int UNIFORM_TYPE = 2; private static final int BINOMIAL_TYPE = 3; private static final int UNKNOWN_TYPE = 99; private static final Cache> SIEVE_RECIPE_CACHE = CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(3)).build(); private static Lazy> barrelCompostRecipeCache; private static Lazy> lavaCrucibleRecipeCache; private static Lazy> waterCrucibleRecipeCache; public static void reload(RecipeManager recipes) { SIEVE_RECIPE_CACHE.invalidateAll(); barrelCompostRecipeCache = Lazy.of(() -> loadSimpleRecipeCache(recipes, ERecipeTypes.BARREL_COMPOST)); lavaCrucibleRecipeCache = Lazy.of(() -> loadSimpleRecipeCache(recipes, ERecipeTypes.LAVA_CRUCIBLE)); waterCrucibleRecipeCache = Lazy.of(() -> loadSimpleRecipeCache(recipes, ERecipeTypes.WATER_CRUCIBLE)); } private static ImmutableMap loadSimpleRecipeCache(RecipeManager recipes, Supplier> recipeType) { var builder = new ImmutableMap.Builder(); for (var recipe : recipes.byType(recipeType.get()).values()) { for (var item : recipe.getIngredient().getItems()) { builder.put(item.getItem(), recipe); } } return builder.buildKeepingLast(); } public static List getSieveRecipes(RecipeManager manager, Item mesh, ItemStack item) { var cacheKey = new SieveCacheKey(mesh, item.getItem()); var cacheVal = SIEVE_RECIPE_CACHE.getIfPresent(cacheKey); if (cacheVal != null) return cacheVal; var builder = new ImmutableList.Builder(); var cache = true; for (var recipe : byType(manager, ERecipeTypes.SIEVE.get())) { if (recipe.test(mesh, item)) { builder.add(recipe); if (recipe.dependsOnNbt) { cache = false; } } } var recipes = builder.build(); if (cache) { SIEVE_RECIPE_CACHE.put(cacheKey, recipes); } return recipes; } public static CrucibleRecipe getLavaCrucibleRecipe(ItemStack item) { return lavaCrucibleRecipeCache.get().get(item.getItem()); } public static CrucibleRecipe getWaterCrucibleRecipe(ItemStack item) { return waterCrucibleRecipeCache.get().get(item.getItem()); } public static BarrelCompostRecipe getBarrelCompostRecipe(ItemStack playerItem) { return barrelCompostRecipeCache.get().get(playerItem.getItem()); } public static > Collection byType(RecipeManager manager, RecipeType type) { return manager.byType(type).values(); } public static Ingredient readIngredient(JsonObject json, String key) { if (GsonHelper.isArrayNode(json, key)) { return Ingredient.fromJson(GsonHelper.getAsJsonArray(json, key)); } else { return Ingredient.fromJson(GsonHelper.getAsJsonObject(json, key)); } } public static Item readItem(JsonObject json, String key) { return ForgeRegistries.ITEMS.getValue(new ResourceLocation(GsonHelper.getAsString(json, key))); } public static NumberProvider readNumberProvider(JsonObject json, String key) { var obj = json.get(key); return LootDataType.PREDICATE.parser().fromJson(obj, NumberProvider.class); } public static void toNetworkNumberProvider(FriendlyByteBuf buffer, NumberProvider provider) { if (provider.getType() == NumberProviders.CONSTANT) { buffer.writeByte(CONSTANT_TYPE); buffer.writeFloat(((ConstantValue) provider).value); } else if (provider.getType() == NumberProviders.UNIFORM) { var uniform = (UniformGenerator) provider; buffer.writeByte(UNIFORM_TYPE); toNetworkNumberProvider(buffer, uniform.min); toNetworkNumberProvider(buffer, uniform.max); } else if (provider.getType() == NumberProviders.BINOMIAL) { var binomial = (BinomialDistributionGenerator) provider; buffer.writeByte(BINOMIAL_TYPE); toNetworkNumberProvider(buffer, binomial.n); toNetworkNumberProvider(buffer, binomial.p); } else { buffer.writeByte(UNKNOWN_TYPE); } } public static NumberProvider fromNetworkNumberProvider(FriendlyByteBuf buffer) { return switch (buffer.readByte()) { case CONSTANT_TYPE -> ConstantValue.exactly(buffer.readFloat()); case UNIFORM_TYPE -> new UniformGenerator(fromNetworkNumberProvider(buffer), fromNetworkNumberProvider(buffer)); case BINOMIAL_TYPE -> new BinomialDistributionGenerator(fromNetworkNumberProvider(buffer), fromNetworkNumberProvider(buffer)); default -> ConstantValue.exactly(1f); }; } // todo support Forge's ingredient types public static boolean areIngredientsEqual(Ingredient first, Ingredient second) { // although unlikely, we should check this anyway if (first == second) return true; if (first.isVanilla() && second.isVanilla()) { var firstValues = new ObjectArrayList<>(first.values); var secondValues = new ObjectArrayList<>(second.values); // if arrays are same size, check if their contents are equal (order does not matter) if (firstValues.size() == secondValues.size()) { outer: for (int i = 0; i < firstValues.size(); i++) { var firstValue = firstValues.get(i); for (int j = 0; j < firstValues.size(); j++) { if (areValuesEqual(firstValue, secondValues.get(j))) { firstValues.remove(i); secondValues.remove(j); i--; continue outer; } } return false; } // return true if everything was equal return true; } } return false; } private static boolean areValuesEqual(Ingredient.Value firstValue, Ingredient.Value secondValue) { Class firstKlass = firstValue.getClass(); Class secondKlass = secondValue.getClass(); // if values are the same type of class if (firstKlass == secondKlass) { if (firstKlass == Ingredient.ItemValue.class) { // if items are different, return false return ItemStack.matches(((Ingredient.ItemValue) firstValue).item, ((Ingredient.ItemValue) secondValue).item); } else if (firstKlass == Ingredient.TagValue.class) { // if tags are different, return false // identity comparison is okay because tags are always interned in vanilla return ((Ingredient.TagValue) firstValue).tag == ((Ingredient.TagValue) secondValue).tag; } else { var firstItems = firstValue.getItems(); var secondItems = secondValue.getItems(); var len = firstItems.size(); if (len == secondItems.size()) { Iterator firstIter = firstItems.iterator(); Iterator secondIter = secondItems.iterator(); while (firstIter.hasNext()) { if (!ItemStack.matches(firstIter.next(), secondIter.next())) { // if one of the items is different, return false return false; } } } else { // if values have different amounts of items, return false return false; } // if all items are the same, return true return true; } } else { // if the values are different types, return false return false; } } public static boolean hasSieveResult(RecipeManager recipes, Item mesh, ItemStack stack) { for (var recipe : byType(recipes, ERecipeTypes.SIEVE.get())) { if (recipe.test(mesh, stack)) { return true; } } return false; } public static boolean isCompostable(ItemStack stack) { return barrelCompostRecipeCache != null && barrelCompostRecipeCache.get().containsKey(stack.getItem()); } public static @Nullable BarrelMixingRecipe getBarrelMixingRecipe(RecipeManager recipes, ItemStack stack, FluidStack fluid) { for (var recipe : byType(recipes, ERecipeTypes.BARREL_MIXING.get())) { if (recipe.matches(stack, fluid)) { return recipe; } } return null; } public static double getExpectedValue(NumberProvider provider) { if (provider instanceof ConstantValue constant) { return constant.value; } else if (provider instanceof UniformGenerator uniform) { return getExpectedValue(uniform.min) + getExpectedValue(uniform.max) / 2.0; } else if (provider instanceof BinomialDistributionGenerator binomial) { return getExpectedValue(binomial.n) * getExpectedValue(binomial.p); } else { // no way of knowing beforehand so just put them last return -1.0; } } private record SieveCacheKey(Item mesh, Item ingredient) { } }