package lv.enes.mc.eris_alchemy; import jakarta.annotation.Nullable; import lv.enes.mc.eris_alchemy.ErisAlchemyRegistry.NetworkingConstants; import lv.enes.mc.eris_alchemy.block.EmcStorageBlock; import lv.enes.mc.eris_alchemy.recipe.BannedRecipe; import lv.enes.mc.eris_alchemy.recipe.SimplifiedRecipe; import lv.enes.mc.eris_alchemy.utils.*; import net.minecraft.client.Minecraft; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.tags.TagKey; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import org.quiltmc.loader.api.minecraft.ClientOnly; import org.quiltmc.qsl.networking.api.PacketByteBufs; import org.quiltmc.qsl.networking.api.ServerPlayConnectionEvents; import org.quiltmc.qsl.networking.api.ServerPlayNetworking; import org.quiltmc.qsl.networking.api.client.ClientPlayNetworking; import java.text.DecimalFormat; import java.util.*; import java.util.function.Supplier; import java.util.stream.Stream; public final class Emc { private Emc() {} private static final Map ITEM_VALUES = new HashMap<>(); private static final Map, OptionalDouble> ITEM_TAG_VALUES = new HashMap<>(); private static final Map, OptionalDouble> BLOCK_TAG_VALUES = new HashMap<>(); private static final List FAKE_RECIPES = new ArrayList<>(); private static final List BANNED_RECIPES = new ArrayList<>(); private static final Map VALUES = Collections.synchronizedMap(new HashMap<>()); private static final DecimalFormat FORMATTER = new DecimalFormat("0"); static { FORMATTER.setMaximumFractionDigits(1); } private static ServerLevel overworld = null; public static String formatEmc(double value) { return FORMATTER.format(value); } public static OptionalDouble get(ItemStack stack) { if (stack.isEmpty()) { return OptionalDouble.empty(); } var item = stack.getItem(); var itemId = ItemUtils.getId(item); return get(itemId) .stream() .map(value -> { if (item instanceof BlockItem blockItem) { if (blockItem.getBlock() instanceof EmcStorageBlock block) { return value + block.getStoredEmc(stack); } } return value; }) .findFirst(); } public static OptionalDouble get(ResourceLocation itemId) { return VALUES.getOrDefault(itemId, OptionalDouble.empty()); } @ClientOnly public static void initClient(Minecraft ignoredClient) { ClientPlayNetworking.registerGlobalReceiver( NetworkingConstants.UPDATE_EMCS, (client, handler, buf, responseSender) -> syncFrom(buf) ); } public static void initServer(MinecraftServer server) { overworld = server.overworld(); reinit(); warnOfMissingValues(); ServerPlayConnectionEvents.JOIN.register((handler, sender, server1) -> syncTo(handler.getPlayer())); } public static void reloadData( Map itemValues, Map itemTagValues, Map blockTagValues, List fakeRecipes, List bannedRecipes ) { ITEM_VALUES.clear(); ITEM_VALUES.putAll(itemValues); ITEM_TAG_VALUES.clear(); itemTagValues.forEach((id, value) -> ITEM_TAG_VALUES.put(TagKey.create(Registries.ITEM, id), value)); BLOCK_TAG_VALUES.clear(); blockTagValues.forEach((id, value) -> BLOCK_TAG_VALUES.put(TagKey.create(Registries.BLOCK, id), value)); FAKE_RECIPES.clear(); FAKE_RECIPES.addAll(fakeRecipes); BANNED_RECIPES.clear(); BANNED_RECIPES.addAll(bannedRecipes); reinit(); warnOfMissingValues(); } private static OptionalDouble calcEmc( ResourceLocation item, Map> allRecipes ) { return allRecipes.getOrDefault(item, List.of()) .stream() .map(Emc::calcEmcForRecipe) .flatMapToDouble(OptionalDouble::stream) .average(); } private static OptionalDouble calcEmcForIngredient(Ingredient ingredient) { return Arrays.stream(ingredient.getItems()) .map(Emc::get) .flatMapToDouble(OptionalDouble::stream) .average(); } private static OptionalDouble calcEmcForRecipe(SimplifiedRecipe recipe) { if (recipe.input().isEmpty()) { return OptionalDouble.empty(); } var inputEmcOpt = recipe.input() .stream() .map(Supplier::get) .map(Emc::calcEmcForIngredient) .collect(new OptionalDoubleSummer()); if (inputEmcOpt.isEmpty()) { return OptionalDouble.empty(); } var remainderEmcOpt = recipe.remainder() .stream() .map(Emc::get) .collect(new OptionalDoubleSummer()); if (remainderEmcOpt.isEmpty()) { return OptionalDouble.empty(); } var inputEmc = inputEmcOpt.getAsDouble(); var remainderEmc = remainderEmcOpt.getAsDouble(); if (remainderEmc > inputEmc) { ErisAlchemy.LOGGER.warn("Recipe generating {} creates too much EMC out of thin air!", recipe.output()); return OptionalDouble.empty(); } var outputDivisor = (double) recipe.output().getCount(); return OptionalDouble.of((inputEmc - remainderEmc) / outputDivisor); } private static Stream getRecipes(@Nullable Level world) { var recipes = FAKE_RECIPES.stream(); if (world != null) { recipes = Stream.concat( recipes, world.getRecipeManager() .getRecipes() .stream() .map(recipe -> SimplifiedRecipe.of(recipe, world.registryAccess())) .flatMap(List::stream) ); } return recipes; } private static void reinit() { VALUES.clear(); VALUES.putAll(ITEM_VALUES); ITEM_TAG_VALUES.forEach( (tag, emcValue) -> BuiltInRegistries.ITEM .getTagOrEmpty(tag) .forEach(holder -> VALUES.putIfAbsent(ItemUtils.getId(holder), emcValue)) ); BLOCK_TAG_VALUES.forEach( (tag, emcValue) -> BuiltInRegistries.BLOCK .getTagOrEmpty(tag) .forEach(holder -> VALUES.putIfAbsent(ItemUtils.getId(holder), emcValue)) ); ErisAlchemy.LOGGER.info("Calculating EMC values from recipes..."); var recipes = new HashMap>(); getRecipes(overworld) .filter(recipe -> !recipe.hasDuplication()) .filter(recipe -> recipe.isAllowed(BANNED_RECIPES)) .forEach(recipe -> recipes .computeIfAbsent(ItemUtils.getId(recipe.output()), k -> new ArrayList<>()) .add(recipe) ); var sortedItems = sorted(recipes); sortedItems.stream() .filter(id -> !VALUES.containsKey(id)) .forEach(id -> calcEmc(id, recipes).ifPresent(v -> VALUES.put(id, OptionalDouble.of(v)))); ErisAlchemy.LOGGER.info("Done calculating EMC values..."); sync(); } private static void sortDps( Set permSorted, ConsList tmpSorted, ResourceLocation item, Map> data ) { if (permSorted.contains(item)) { return; } var newTmpSorted = ConsList.cons(item, tmpSorted); if (tmpSorted.contains(item)) { ErisAlchemy.LOGGER.warn("Cycle in recipes detected: {}, breaking here", newTmpSorted); return; } data.getOrDefault(item, List.of()) .stream() .flatMap(SimplifiedRecipe::dependencies) .distinct() .forEach(dep -> sortDps(permSorted, newTmpSorted, dep, data)); permSorted.add(item); } private static Set sorted(Map> unsorted) { var res = new LinkedHashSet(); unsorted.forEach((item, recipes) -> sortDps(res, ConsList.nil(), item, unsorted)); return res; } private static void sync() { syncTo(PlayerUtils.all()); } private static void syncFrom(FriendlyByteBuf buf) { var map = buf.readMap(FriendlyByteBuf::readResourceLocation, BufUtils::readOptionalDouble); VALUES.clear(); VALUES.putAll(map); } private static FriendlyByteBuf syncTo(FriendlyByteBuf buf) { buf.writeMap(VALUES, FriendlyByteBuf::writeResourceLocation, BufUtils::writeOptionalDouble); return buf; } private static void syncTo(Collection players) { var buf = syncTo(PacketByteBufs.create()); ServerPlayNetworking.send(players, NetworkingConstants.UPDATE_EMCS, buf); } private static void syncTo(ServerPlayer player) { var buf = syncTo(PacketByteBufs.create()); ServerPlayNetworking.send(player, NetworkingConstants.UPDATE_EMCS, buf); } private static void warnOfMissingValues() { if (overworld == null) { return; } BuiltInRegistries.ITEM .keySet() .stream() .filter(item -> !VALUES.containsKey(item)) .forEach(item -> ErisAlchemy.LOGGER.warn("No EMC value for '{}' known", item)); } }