/* * Ex Deorum * Copyright (c) 2024 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.compat; import com.google.common.collect.ImmutableList; import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.minecraft.ChatFormatting; import net.minecraft.advancements.critereon.StatePropertiesPredicate; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.WallTorchBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; 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.UniformGenerator; import net.neoforged.fml.ModContainer; import net.neoforged.fml.ModList; import net.neoforged.neoforgespi.language.IModInfo; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import thedarkcolour.exdeorum.data.TranslationKeys; import thedarkcolour.exdeorum.loot.SummationGenerator; import thedarkcolour.exdeorum.recipe.BlockPredicate; import thedarkcolour.exdeorum.recipe.CodecUtil; import thedarkcolour.exdeorum.recipe.RecipeUtil; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; // common logic shared between JEI, EMI, and REI (boo REI sucks) public class XeiUtil { // One To One (Hammer, Crucible) public static final int ONE_TO_ONE_WIDTH = 72; public static final int ONE_TO_ONE_HEIGHT = 18; // Barrel mixing (Fluid/Item, Fluid/Fluid) public static final int BARREL_MIXING_WIDTH = 120; public static final int BARREL_MIXING_HEIGHT = 18; // Block predicate (Crucible Heat, Sieve) public static final Component REQUIRES_CERTAIN_STATE = Component.translatable(TranslationKeys.CROOK_CATEGORY_REQUIRES_STATE).withStyle(ChatFormatting.GRAY); // Sieve public static final int SIEVE_WIDTH = 162; public static final int SIEVE_ROW_START = 28; public static final int SIEVE_ROW_HEIGHT = 18; public static final Component BY_HAND_ONLY_LABEL = Component.translatable(TranslationKeys.SIEVE_RECIPE_BY_HAND_ONLY).withStyle(ChatFormatting.RED); public static final DecimalFormat FORMATTER = new DecimalFormat(); static { FORMATTER.setMinimumFractionDigits(0); FORMATTER.setMaximumFractionDigits(3); } // Takes a decimal probability and returns a user-friendly percentage value public static Component formatChance(double probability) { var chance = FORMATTER.format(probability * 100); return Component.translatable(TranslationKeys.SIEVE_RECIPE_CHANCE, chance).withStyle(ChatFormatting.GRAY); } public static List getStates(BlockPredicate predicate) { if (predicate instanceof BlockPredicate.BlockStatePredicate state) { return state.possibleStates() .filter(blockState -> !blockState.hasProperty(BlockStateProperties.WATERLOGGED) || !blockState.getValue(BlockStateProperties.WATERLOGGED)) .toList(); } else if (predicate instanceof BlockPredicate.SingleBlockPredicate block) { return ImmutableList.of(block.block().defaultBlockState()); } else if (predicate instanceof BlockPredicate.TagPredicate tag) { var list = new ArrayList(); for (var holder : BuiltInRegistries.BLOCK.getTagOrEmpty(tag.tag())) { if (holder.isBound()) { list.add(holder.value().defaultBlockState()); } } return list; } throw new IllegalArgumentException("Invalid Block Predicate"); } // Copied from mezz.jei.forge.platform.ModHelper and mezz.jei.library.ModIdHelper public static Component getModDisplayName(String modId) { String string = ModList.get().getModContainerById(modId) .map(ModContainer::getModInfo) .map(IModInfo::getDisplayName) .orElseGet(() -> StringUtils.capitalize(modId)); String withoutFormattingCodes = ChatFormatting.stripFormatting(string); return Component.literal((withoutFormattingCodes == null) ? "" : withoutFormattingCodes).withStyle(style -> style.withItalic(true).withColor(ChatFormatting.BLUE)); } public static List getBlockTooltip(List extraDetails, Block block) { var modId = BuiltInRegistries.BLOCK.getKey(block).getNamespace(); var tooltip = new ArrayList(); tooltip.add(Component.translatable(block.getDescriptionId())); tooltip.addAll(extraDetails); tooltip.add(getModDisplayName(modId)); return tooltip; } public static ImmutableList getStateRequirements(BlockPredicate.@Nullable BlockStatePredicate predicate) { ImmutableList.Builder requirements = ImmutableList.builder(); if (predicate != null) { var json = CodecUtil.encode(StatePropertiesPredicate.CODEC, predicate.properties()); if (json instanceof JsonObject obj) { for (var entry : obj.entrySet()) { requirements.add(Component.literal(" " + entry.getKey() + "=" + entry.getValue().toString()).withStyle(ChatFormatting.GRAY)); } } } return requirements.build(); } public static List getExtraDetails(BlockPredicate predicate) { List extraDetails; if (predicate instanceof BlockPredicate.TagPredicate tag) { extraDetails = ImmutableList.of(Component.literal("#" + tag.tag().location()).withStyle(ChatFormatting.GRAY)); } else if (predicate instanceof BlockPredicate.BlockStatePredicate state) { var requirements = getStateRequirements(state); extraDetails = new ArrayList<>(requirements.size() + 1); extraDetails.add(REQUIRES_CERTAIN_STATE); extraDetails.addAll(requirements); } else { extraDetails = List.of(); } return extraDetails; } public static void addSieveDropTooltip(boolean byHandOnly, NumberProvider provider, Consumer tooltipLines) { if (byHandOnly) { tooltipLines.accept(XeiUtil.BY_HAND_ONLY_LABEL); } if (provider instanceof BinomialDistributionGenerator binomial) { if (binomial.n() instanceof ConstantValue constant && constant.value() == 1) { var chanceLabel = XeiUtil.formatChance(RecipeUtil.getExpectedValue(binomial.p())); tooltipLines.accept(chanceLabel); } else { addAvgOutput(tooltipLines, RecipeUtil.getExpectedValue(provider)); } addMinMaxes(tooltipLines, 0, getMax(binomial.n())); } else if (provider.getClass() != ConstantValue.class) { var val = RecipeUtil.getExpectedValue(provider); if (val != -1.0) { addAvgOutput(tooltipLines, val); if (provider instanceof UniformGenerator || provider instanceof SummationGenerator) { addMinMaxes(tooltipLines, getMin(provider), getMax(provider)); } } } } private static double getMin(NumberProvider provider) { if (provider instanceof ConstantValue value) { return value.value(); } else if (provider instanceof UniformGenerator uniform) { return getMin(uniform.min()); } else if (provider instanceof BinomialDistributionGenerator) { return 0; } else if (provider instanceof SummationGenerator summation) { double sum = 0; for (var child : summation.providers()) { sum += getMin(child); } return sum; } return 0; } private static double getMax(NumberProvider provider) { if (provider instanceof ConstantValue value) { return value.value(); } else if (provider instanceof UniformGenerator uniform) { return getMax(uniform.max()); } else if (provider instanceof BinomialDistributionGenerator binomial) { return getMax(binomial.n()); } else if (provider instanceof SummationGenerator summation) { double sum = 0; for (var child : summation.providers()) { sum += getMax(child); } return sum; } return 0; } private static void addAvgOutput(Consumer tooltipLines, double avgValue) { String avgOutput = XeiUtil.FORMATTER.format(avgValue); tooltipLines.accept(Component.translatable(TranslationKeys.SIEVE_RECIPE_AVERAGE_OUTPUT, avgOutput).withStyle(ChatFormatting.GRAY)); } // when the player holds shift, they can see the min/max amounts of a drop private static void addMinMaxes(Consumer tooltipLines, double min, double max) { String minFormatted = XeiUtil.FORMATTER.format(min); String maxFormatted = XeiUtil.FORMATTER.format(max); tooltipLines.accept(Component.translatable(TranslationKeys.SIEVE_RECIPE_MIN_OUTPUT, minFormatted).withStyle(ChatFormatting.GRAY)); tooltipLines.accept(Component.translatable(TranslationKeys.SIEVE_RECIPE_MAX_OUTPUT, maxFormatted).withStyle(ChatFormatting.GRAY)); } public interface HeatRecipeAcceptor { void accept(int heat, BlockState state); } public static void addCrucibleHeatRecipes(HeatRecipeAcceptor acceptor) { var values = new Object2IntOpenHashMap(); for (var entry : RecipeUtil.getHeatSources()) { var state = entry.getKey(); var block = state.getBlock(); if (block instanceof WallTorchBlock) continue; if (block != Blocks.AIR) { final int newValue = entry.getIntValue(); values.computeInt(block, (key, value) -> { if (value != null) { return Math.max(value, newValue); } else { return newValue == 0 ? null : newValue; } }); } } for (var entry : values.object2IntEntrySet()) { acceptor.accept(entry.getIntValue(), entry.getKey().defaultBlockState()); } } }