From a7e2dbc2afff1c62ac404301c5a4b4b82fb475cb Mon Sep 17 00:00:00 2001 From: Chub74 Date: Sun, 7 Jun 2026 13:20:04 +0200 Subject: [PATCH] Adding Variant Recipes Added the ability to add variant recipes, meaning you don't need multiple recipes for every variant anymore and just can use one recipe. Example: make_trapdoors: production_time: 4s name: Make Trap Doors type: VARIANT input: chest: v: 3839 ==: org.bukkit.inventory.ItemStack type: CHEST amount: 2 variant_input: log: v: 3839 ==: org.bukkit.inventory.ItemStack type: OAK_LOG amount: 1 variant_output: trapdoor: v: 3839 ==: org.bukkit.inventory.ItemStack type: OAK_TRAPDOOR amount: 128 variant_prefixes: - OAK - BIRCH - SPRUCE - JUNGLE - ACACIA - DARK_OAK - CHERRY - MANGROVE - PALE_OAK variant_overrides: BAMBOO: input: BAMBOO_BLOCK CRIMSON: input: CRIMSON_STEM WARPED: input: WARPED_STEM --- .../igotyou/FactoryMod/ConfigParser.java | 142 +++++-- .../FactoryMod/recipes/VariantRecipe.java | 385 ++++++++++++++++++ .../FactoryMod/utility/ItemUseGUI.java | 11 + 3 files changed, 512 insertions(+), 26 deletions(-) create mode 100644 plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/recipes/VariantRecipe.java diff --git a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java index db647f3c8d..d21a8da57f 100644 --- a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java +++ b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/ConfigParser.java @@ -1,5 +1,30 @@ package com.github.igotyou.FactoryMod; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import org.apache.commons.lang3.text.WordUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitRunnable; + import com.github.igotyou.FactoryMod.eggs.FurnCraftChestEgg; import com.github.igotyou.FactoryMod.eggs.IFactoryEgg; import com.github.igotyou.FactoryMod.eggs.PipeEgg; @@ -25,6 +50,7 @@ import com.github.igotyou.FactoryMod.recipes.RecipeScalingUpgradeRecipe; import com.github.igotyou.FactoryMod.recipes.RepairRecipe; import com.github.igotyou.FactoryMod.recipes.Upgraderecipe; +import com.github.igotyou.FactoryMod.recipes.VariantRecipe; import com.github.igotyou.FactoryMod.recipes.WordBankRecipe; import com.github.igotyou.FactoryMod.recipes.heliodor.HeliodorCreateRecipe; import com.github.igotyou.FactoryMod.recipes.heliodor.HeliodorFinishRecipe; @@ -37,35 +63,12 @@ import com.github.igotyou.FactoryMod.structures.PipeStructure; import com.github.igotyou.FactoryMod.utility.FactoryGarbageCollector; import com.github.igotyou.FactoryMod.utility.FactoryModGUI; -import org.apache.commons.lang3.text.WordUtils; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitRunnable; -import vg.civcraft.mc.civmodcore.config.ConfigHelper; -import vg.civcraft.mc.civmodcore.inventory.CustomItem; -import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; +import vg.civcraft.mc.civmodcore.config.ConfigHelper; import static vg.civcraft.mc.civmodcore.config.ConfigHelper.parseTime; import static vg.civcraft.mc.civmodcore.config.ConfigHelper.parseTimeAsTicks; +import vg.civcraft.mc.civmodcore.inventory.CustomItem; +import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; public class ConfigParser { @@ -578,6 +581,93 @@ private IRecipe parseRecipe(ConfigurationSection config) { } result = new ProductionRecipe(identifier, name, productionTime, input, output, recipeRepresentation, modi); break; + case "VARIANT": + ConfigurationSection variantInputSection = config.getConfigurationSection("variant_input"); + if (variantInputSection == null) { + plugin.warning("No variant_input specified for VARIANT recipe " + name); + result = null; + break; + } + ItemStack variantInput = parseFirstItem(variantInputSection); + if (variantInput == null || variantInput.getType().isEmpty()) { + plugin.warning("Could not parse variant_input for VARIANT recipe " + name); + result = null; + break; + } + ConfigurationSection variantOutputSection = config.getConfigurationSection("variant_output"); + if (variantOutputSection == null) { + plugin.warning("No variant_output specified for VARIANT recipe " + name); + result = null; + break; + } + ItemStack variantOutput = parseFirstItem(variantOutputSection); + if (variantOutput == null || variantOutput.getType().isEmpty()) { + plugin.warning("Could not parse variant_output for VARIANT recipe " + name); + result = null; + break; + } + ItemMap fixedOutputMap; + ConfigurationSection fixedOutputSection = config.getConfigurationSection("output"); + if (fixedOutputSection == null) { + fixedOutputMap = new ItemMap(); + } else { + fixedOutputMap = ConfigHelper.parseItemMap(fixedOutputSection); + } + List variantPrefixes = config.getStringList("variant_prefixes"); + ConfigurationSection overridesSection = config.getConfigurationSection("variant_overrides"); + Map variantMap = new LinkedHashMap<>(); + String baseInputName = variantInput.getType().name(); + int firstUnderscore = baseInputName.indexOf('_'); + String basePrefix = firstUnderscore != -1 ? baseInputName.substring(0, firstUnderscore) : baseInputName; + String inputSuffix = firstUnderscore != -1 ? baseInputName.substring(firstUnderscore) : ""; + String baseOutputName = variantOutput.getType().name(); + int outFirstUnderscore = baseOutputName.indexOf('_'); + String outputSuffix = outFirstUnderscore != -1 ? baseOutputName.substring(outFirstUnderscore) : ""; + boolean outputHasSuffix = outFirstUnderscore != -1; + for (String prefix : variantPrefixes) { + String inputName = prefix + inputSuffix; + Material inputMat = Material.getMaterial(inputName); + Material outputMat; + String outputName; + if (outputHasSuffix) { + outputName = prefix + outputSuffix; + outputMat = Material.getMaterial(outputName); + } else { + outputName = baseOutputName; + outputMat = variantOutput.getType(); + } + if (inputMat != null && outputMat != null) { + variantMap.put(inputMat, outputMat); + } else { + plugin.warning("Could not resolve VARIANT mapping for prefix " + prefix + + " in recipe " + name + " (" + inputName + " -> " + outputName + ")"); + } + } + if (overridesSection != null) { + for (String key : overridesSection.getKeys(false)) { + ConfigurationSection overrideSec = overridesSection.getConfigurationSection(key); + if (overrideSec != null) { + Material overrideInput = Material.getMaterial(overrideSec.getString("input", "")); + Material overrideOutput; + String explicitOutput = overrideSec.getString("output", null); + if (explicitOutput != null) { + overrideOutput = Material.getMaterial(explicitOutput); + } else { + overrideOutput = outputHasSuffix ? Material.getMaterial(key + outputSuffix) : variantOutput.getType(); + } + if (overrideInput != null && overrideOutput != null) { + variantMap.put(overrideInput, overrideOutput); + } else { + plugin.warning("Could not resolve VARIANT override for " + key + + " in recipe " + name); + } + } + } + } + variantMap.putIfAbsent(variantInput.getType(), variantOutput.getType()); + result = new VariantRecipe(identifier, name, productionTime, input, variantInput, + fixedOutputMap, variantOutput, variantMap); + break; case "COMPACT": String compactedLore = config.getString("compact_lore", (parentRecipe instanceof CompactingRecipe) ? ((CompactingRecipe) parentRecipe).getCompactedLore() diff --git a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/recipes/VariantRecipe.java b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/recipes/VariantRecipe.java new file mode 100644 index 0000000000..4dedcaca7c --- /dev/null +++ b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/recipes/VariantRecipe.java @@ -0,0 +1,385 @@ +package com.github.igotyou.FactoryMod.recipes; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import com.github.igotyou.FactoryMod.factories.FurnCraftChestFactory; +import com.github.igotyou.FactoryMod.utility.MultiInventoryWrapper; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import vg.civcraft.mc.civmodcore.inventory.items.ItemMap; +import vg.civcraft.mc.civmodcore.inventory.items.ItemUtils; +import vg.civcraft.mc.civmodcore.utilities.TextUtil; + +/** + * A recipe where a base input material is mapped to a base output material, + * and additional variants are derived by replacing the prefix in the material name. + * At runtime, the actual variant is selected based on which input material is present + * in the factory chest. + */ +public class VariantRecipe extends InputRecipe { + + private final ItemStack baseVariantInput; + private final ItemStack baseVariantOutput; + private final ItemMap fixedInput; + private final ItemMap fixedOutput; + private final Map inputToOutputMap; + private final String genericInputName; + private final String genericOutputName; + + public VariantRecipe( + String identifier, + String name, + int productionTime, + ItemMap fixedInput, + ItemStack baseVariantInput, + ItemMap fixedOutput, + ItemStack baseVariantOutput, + Map inputToOutputMap + ) { + super(identifier, name, productionTime, fixedInput); + this.fixedInput = fixedInput; + this.baseVariantInput = baseVariantInput; + this.fixedOutput = fixedOutput; + this.baseVariantOutput = baseVariantOutput; + this.inputToOutputMap = new LinkedHashMap<>(inputToOutputMap); + this.genericInputName = computeGenericName(baseVariantInput); + this.genericOutputName = computeGenericName(baseVariantOutput); + } + + private static String computeGenericName(ItemStack item) { + String name = ItemUtils.getItemName(item); + int firstSpace = name.indexOf(' '); + + if (firstSpace != -1) { + return "Any" + name.substring(firstSpace); + } + + return "Any " + name; + } + + private static String computeMatchingName(ItemStack item) { + String name = ItemUtils.getItemName(item); + int firstSpace = name.indexOf(' '); + + if (firstSpace != -1) { + return "Matching" + name.substring(firstSpace); + } + + return "Matching " + name; + } + + public boolean acceptsInputMaterial(Material material) { + return inputToOutputMap.containsKey(material); + } + + public boolean producesOutputMaterial(Material material) { + return inputToOutputMap.containsValue(material); + } + + @Override + public boolean enoughMaterialAvailable(Inventory inputInv) { + if (!fixedInput.isContainedIn(inputInv)) { + return false; + } + + ItemMap invMap = new ItemMap(inputInv); + + for (Map.Entry entry : inputToOutputMap.entrySet()) { + ItemStack check = new ItemStack(entry.getKey(), baseVariantInput.getAmount()); + + if (invMap.getAmount(check) >= baseVariantInput.getAmount()) { + return true; + } + + } + + return false; + } + + @Override + public boolean applyEffect(Inventory inputInv, Inventory outputInv, FurnCraftChestFactory fccf) { + MultiInventoryWrapper combo = new MultiInventoryWrapper(inputInv, outputInv); + logBeforeRecipeRun(combo, fccf); + + Material selectedInput = null; + Material selectedOutput = null; + ItemMap invMap = new ItemMap(inputInv); + + for (Map.Entry entry : inputToOutputMap.entrySet()) { + ItemStack check = new ItemStack(entry.getKey(), baseVariantInput.getAmount()); + + if (invMap.getAmount(check) >= baseVariantInput.getAmount()) { + selectedInput = entry.getKey(); + selectedOutput = entry.getValue(); + break; + } + } + + if (selectedInput == null) { + return false; + } + + ItemMap toRemove = fixedInput.clone(); + toRemove.addItemAmount(new ItemStack(selectedInput, 1), baseVariantInput.getAmount()); + + ItemMap toAdd = fixedOutput.clone(); + toAdd.addItemAmount(new ItemStack(selectedOutput, 1), baseVariantOutput.getAmount()); + + if (!toRemove.isContainedIn(inputInv)) { + return false; + } + + if (!canFitInOutput(toAdd, outputInv)) { + return false; + } + + List insertedOutput = new ArrayList<>(); + if (toRemove.removeSafelyFrom(inputInv)) { + if (addOutputToInventorySafely(toAdd, outputInv, insertedOutput)) { + logAfterRecipeRun(combo, fccf); + return true; + } else { + rollbackOutput(outputInv, insertedOutput); + restoreInput(toRemove, inputInv, fccf); + return false; + } + } + + return false; + } + + @Override + public EffectFeasibility evaluateEffectFeasibility(Inventory inputInv, Inventory outputInv) { + Material selectedOutput = null; + ItemMap invMap = new ItemMap(inputInv); + + for (Map.Entry entry : inputToOutputMap.entrySet()) { + ItemStack check = new ItemStack(entry.getKey(), baseVariantInput.getAmount()); + + if (invMap.getAmount(check) >= baseVariantInput.getAmount()) { + selectedOutput = entry.getValue(); + break; + } + } + + if (selectedOutput == null) { + return new EffectFeasibility(false, "not enough materials"); + } + + ItemMap totalOutput = fixedOutput.clone(); + totalOutput.addItemAmount(new ItemStack(selectedOutput, 1), baseVariantOutput.getAmount()); + boolean isFeasible = totalOutput.fitsIn(outputInv); + + return new EffectFeasibility(isFeasible, isFeasible ? null : "it ran out of storage space"); + } + + @Override + public List getInputRepresentation(Inventory i, FurnCraftChestFactory fccf) { + List result = new ArrayList<>(); + + for (ItemStack is : fixedInput.getItemStackRepresentation()) { + ItemStack clone = is.clone(); + + if (i != null) { + int possibleRuns = fixedInput.getMultiplesContainedIn(i); + ItemUtils.addLore(clone, ChatColor.GREEN + "Enough materials for " + possibleRuns + " runs"); + } + + result.add(clone); + } + + ItemStack variantDisplay = baseVariantInput.clone(); + ItemMeta meta = variantDisplay.getItemMeta(); + meta.displayName(Component.text(genericInputName, NamedTextColor.YELLOW)); + variantDisplay.setItemMeta(meta); + + if (i != null) { + ItemMap invMap = new ItemMap(i); + int maxVariantRuns = 0; + + for (Material mat : inputToOutputMap.keySet()) { + int have = invMap.getAmount(new ItemStack(mat, 1)); + maxVariantRuns = Math.max(maxVariantRuns, have / baseVariantInput.getAmount()); + } + + int fixedRuns = fixedInput.getMultiplesContainedIn(i); + int totalRuns = Math.min(fixedRuns, maxVariantRuns); + ItemUtils.addLore(variantDisplay, ChatColor.GREEN + "Enough materials for " + totalRuns + " runs"); + } + result.add(variantDisplay); + + return result; + } + + @Override + public List getOutputRepresentation(Inventory i, FurnCraftChestFactory fccf) { + List result = new ArrayList<>(); + + for (ItemStack is : fixedOutput.getItemStackRepresentation()) { + ItemStack clone = is.clone(); + + if (i != null) { + int possibleRuns = calculatePossibleRuns(i); + ItemUtils.addLore(clone, ChatColor.GREEN + "Enough materials for " + possibleRuns + " runs"); + } + + result.add(clone); + } + + ItemStack variantDisplay = baseVariantOutput.clone(); + ItemMeta meta = variantDisplay.getItemMeta(); + meta.displayName(Component.text(computeMatchingName(baseVariantOutput), NamedTextColor.YELLOW)); + variantDisplay.setItemMeta(meta); + + if (i != null) { + int possibleRuns = calculatePossibleRuns(i); + ItemUtils.addLore(variantDisplay, ChatColor.GREEN + "Enough materials for " + possibleRuns + " runs"); + } + + result.add(variantDisplay); + + return result; + } + + @Override + public List getTextualInputRepresentation(Inventory i, FurnCraftChestFactory fccf) { + List result = formatLore(fixedInput); + result.add(baseVariantInput.getAmount() + " " + genericInputName); + + return result; + } + + @Override + public List getTextualOutputRepresentation(Inventory i, FurnCraftChestFactory fccf) { + List result = formatLore(fixedOutput); + result.add(baseVariantOutput.getAmount() + " " + computeMatchingName(baseVariantOutput)); + + return result; + } + + @Override + public ItemStack getRecipeRepresentation(Inventory inputInv) { + + Material displayMaterial = baseVariantOutput.getType(); + Material selectedVariantInput = null; + + if (inputInv != null) { + ItemMap inventoryMap = new ItemMap(inputInv); + + for (Map.Entry entry : inputToOutputMap.entrySet()) { + ItemStack check = new ItemStack(entry.getKey(), baseVariantInput.getAmount()); + + if (inventoryMap.getAmount(check) >= baseVariantInput.getAmount()) { + displayMaterial = entry.getValue(); + selectedVariantInput = entry.getKey(); + break; + } + } + } + + ItemStack res = new ItemStack(displayMaterial); + ItemMeta im = res.getItemMeta(); + im.setDisplayName(ChatColor.DARK_GREEN + getName()); + List lore = new ArrayList<>(); + lore.add(ChatColor.GOLD + "Input:"); + + ItemMap inventoryMap = inputInv != null ? new ItemMap(inputInv) : null; + + for (Map.Entry entry : fixedInput.getAllItems().entrySet()) { + + if (entry.getValue() <= 0) { + continue; + } + + String name = formatIngredientName(entry.getKey()); + + if (inputInv != null) { + int have = inventoryMap.getAmount(entry.getKey()); + ChatColor color = have >= entry.getValue() ? ChatColor.GREEN : ChatColor.RED; + lore.add(ChatColor.GRAY + " - " + color + have + "/" + entry.getValue() + " " + name); + } else { + lore.add(ChatColor.GRAY + " - " + ChatColor.AQUA + entry.getValue() + " " + name); + } + } + + if (inputInv != null) { + int maxHave = 0; + + if (selectedVariantInput != null) { + maxHave = inventoryMap.getAmount(new ItemStack(selectedVariantInput, 1)); + } else { + for (Material mat : inputToOutputMap.keySet()) { + maxHave = Math.max(maxHave, inventoryMap.getAmount(new ItemStack(mat, 1))); + } + } + + ChatColor color = maxHave >= baseVariantInput.getAmount() ? ChatColor.GREEN : ChatColor.RED; + lore.add(ChatColor.GRAY + " - " + color + maxHave + "/" + baseVariantInput.getAmount() + " " + genericInputName); + } else { + lore.add(ChatColor.GRAY + " - " + ChatColor.AQUA + baseVariantInput.getAmount() + " " + genericInputName); + } + + lore.add(""); + lore.add(ChatColor.GOLD + "Output:"); + + for (String s : formatLore(fixedOutput)) { + lore.add(ChatColor.GRAY + " - " + ChatColor.AQUA + s); + } + + String variantOutputName; + + if (selectedVariantInput != null) { + variantOutputName = ItemUtils.getItemName(new ItemStack(displayMaterial)); + } else { + variantOutputName = computeMatchingName(baseVariantOutput); + } + + lore.add(ChatColor.GRAY + " - " + ChatColor.AQUA + baseVariantOutput.getAmount() + " " + variantOutputName); + + lore.add(""); + lore.add(ChatColor.DARK_AQUA + "Time: " + ChatColor.GRAY + TextUtil.formatDuration(getProductionTime() * 50, java.util.concurrent.TimeUnit.MILLISECONDS)); + im.setLore(lore); + res.setItemMeta(im); + + return res; + } + + @Override + public Material getRecipeRepresentationMaterial() { + return baseVariantOutput.getType(); + } + + @Override + public String getTypeIdentifier() { + return "VARIANT"; + } + + private int calculatePossibleRuns(Inventory i) { + if (i == null) { + return 0; + } + + ItemMap invMap = new ItemMap(i); + int maxVariantRuns = 0; + + for (Material mat : inputToOutputMap.keySet()) { + int have = invMap.getAmount(new ItemStack(mat, 1)); + maxVariantRuns = Math.max(maxVariantRuns, have / baseVariantInput.getAmount()); + } + + int fixedRuns = fixedInput.getMultiplesContainedIn(i); + + return Math.min(fixedRuns, maxVariantRuns); + } + +} diff --git a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/utility/ItemUseGUI.java b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/utility/ItemUseGUI.java index 8525df7994..a16a3917e9 100644 --- a/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/utility/ItemUseGUI.java +++ b/plugins/factorymod-paper/src/main/java/com/github/igotyou/FactoryMod/utility/ItemUseGUI.java @@ -7,6 +7,7 @@ import com.github.igotyou.FactoryMod.recipes.IRecipe; import com.github.igotyou.FactoryMod.recipes.InputRecipe; import com.github.igotyou.FactoryMod.recipes.ProductionRecipe; +import com.github.igotyou.FactoryMod.recipes.VariantRecipe; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -126,6 +127,11 @@ private ItemStack getItemAsInput(FurnCraftChestEgg fccEgg, InputRecipe recipe, I if (recipe.getInput().getAmount(item) != 0) { return getItemRecipeStack(fccEgg, recipe, item); } + if (recipe instanceof VariantRecipe vr) { + if (vr.acceptsInputMaterial(item.getType())) { + return getItemRecipeStack(fccEgg, recipe, item); + } + } return null; } @@ -142,6 +148,11 @@ private ItemStack getItemAsOutput(FurnCraftChestEgg fccEgg, InputRecipe recipe, return getItemRecipeStack(fccEgg, recipe, item); } } + if (recipe instanceof VariantRecipe vr) { + if (vr.producesOutputMaterial(item.getType())) { + return getItemRecipeStack(fccEgg, recipe, item); + } + } return null; }