diff --git a/examples/postInit/minecraft.groovy b/examples/postInit/minecraft.groovy index 717d841bc..c929d7505 100644 --- a/examples/postInit/minecraft.groovy +++ b/examples/postInit/minecraft.groovy @@ -159,12 +159,14 @@ mods.minecraft.crafting.shapelessBuilder() // mods.minecraft.crafting.replaceShapeless('minecraft:pink_dye_from_pink_tulp', item('minecraft:clay'), [item('minecraft:nether_star')]) // Furnace: -// Converts an input item into an output itemstack after a set amount of time, with the ability to give experience and -// using fuel to run. +// Converts an input item into an output itemstack after a configurable amount of time, with the ability to give experience +// and using fuel to run. Can also convert the item in the fuel slot. -mods.minecraft.furnace.removeByInput(item('minecraft:clay')) +mods.minecraft.furnace.removeByInput(item('minecraft:clay:*')) mods.minecraft.furnace.removeByOutput(item('minecraft:brick')) +mods.minecraft.furnace.removeFuelConversionBySmeltedStack(item('minecraft:sponge', 1)) // mods.minecraft.furnace.removeAll() +// mods.minecraft.furnace.removeAllFuelConversions() mods.minecraft.furnace.recipeBuilder() .input(ore('ingotGold')) @@ -175,6 +177,8 @@ mods.minecraft.furnace.recipeBuilder() // mods.minecraft.furnace.add(ore('ingotIron'), item('minecraft:diamond')) mods.minecraft.furnace.add(item('minecraft:nether_star'), item('minecraft:clay') * 64, 13) +mods.minecraft.furnace.add(item('minecraft:diamond'), item('minecraft:clay'), 2, 50) +mods.minecraft.furnace.addFuelConversion(item('minecraft:diamond'), item('minecraft:bucket').transform(item('minecraft:lava_bucket'))) // Default GameRules: // Create or assign a default value to GameRules. diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/CustomFurnaceManager.java b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/CustomFurnaceManager.java new file mode 100644 index 000000000..e2c1ce1cd --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/CustomFurnaceManager.java @@ -0,0 +1,58 @@ +package com.cleanroommc.groovyscript.compat.vanilla; + +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.helper.ingredient.IngredientHelper; +import com.cleanroommc.groovyscript.helper.ingredient.itemstack.ItemStack2IntProxyMap; +import com.github.bsideup.jabel.Desugar; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +@GroovyBlacklist +public class CustomFurnaceManager { + + /** + * Time an itemstack takes to smelt. + *

+ * By default, minecraft uses 200 ticks for everything, GroovyScript uses a mixin to allow variable times to smelt. + * + * @see com.cleanroommc.groovyscript.core.mixin.furnace.TileEntityFurnaceMixin TileEntityFurnaceMixin + */ + public static final ItemStack2IntProxyMap TIME_MAP = new ItemStack2IntProxyMap(); + + /** + * Recipes for converting the fuel slot of a furnace into another item when a valid item is smelted. + *

+ * By default, minecraft has custom logic to make smelting a wet sponge convert an empty bucket into a water bucket. + * + * @see com.cleanroommc.groovyscript.core.mixin.furnace.TileEntityFurnaceMixin TileEntityFurnaceMixin + * @see FuelConversionRecipe + */ + public static final List FUEL_TRANSFORMERS = new ArrayList<>(); + + static { + // reproduce the vanilla logic for converting empty buckets into water buckets on smelting wet sponge + // in groovyscript this would be `furnace.addFuelConversion(item('minecraft:sponge', 1), item('minecraft:bucket').transform(item('minecraft:water_bucket')))` + var bucket = IngredientHelper.toIIngredient(((ItemStackMixinExpansion) (Object) (new ItemStack(Items.BUCKET))).transform(new ItemStack(Items.WATER_BUCKET))); + var wetSponge = IngredientHelper.toIIngredient(new ItemStack(Item.getItemFromBlock(Blocks.SPONGE), 1, 1)); + FUEL_TRANSFORMERS.add(new FuelConversionRecipe(wetSponge, bucket)); + } + + /** + * When the smelted ItemStack passes the {@link #smelted} filter and the {@link #fuel} filter, + * the {@link #fuel} IIngredient will use {@link IIngredient#applyTransform(ItemStack)} to convert the fuel stack. + * + * @param smelted an IIngredient that is checked against the item being smelted + * @param fuel an IIngredient that is checked against the fuel item, and if it passes uses {@link IIngredient#applyTransform(ItemStack)} to convert the fuel item. + */ + @Desugar + public record FuelConversionRecipe(IIngredient smelted, IIngredient fuel) { + + } + +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/Furnace.java b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/Furnace.java index 5f6c611f4..a15f9c320 100644 --- a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/Furnace.java +++ b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/Furnace.java @@ -5,20 +5,29 @@ import com.cleanroommc.groovyscript.api.IIngredient; import com.cleanroommc.groovyscript.api.documentation.annotations.*; import com.cleanroommc.groovyscript.helper.SimpleObjectStream; +import com.cleanroommc.groovyscript.helper.ingredient.GroovyScriptCodeConverter; import com.cleanroommc.groovyscript.helper.ingredient.IngredientHelper; import com.cleanroommc.groovyscript.helper.recipe.AbstractRecipeBuilder; +import com.cleanroommc.groovyscript.registry.AbstractReloadableStorage; import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.FurnaceRecipes; +import net.minecraftforge.oredict.OreDictionary; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import java.util.Map; -@RegistryDescription +@RegistryDescription( + admonition = @Admonition("groovyscript.wiki.minecraft.furnace.note0") +) public class Furnace extends VirtualizedRegistry { + private static final float EXPERIENCE_DEFAULT = 0.1f; + private static final int TIME_DEFAULT = 200; + + private final AbstractReloadableStorage conversionStorage = new AbstractReloadableStorage<>(); + @RecipeBuilderDescription(example = @Example(".input(ore('ingotGold')).output(item('minecraft:nether_star')).exp(0.5)")) public RecipeBuilder recipeBuilder() { return new RecipeBuilder(); @@ -26,178 +35,153 @@ public RecipeBuilder recipeBuilder() { @MethodDescription(type = MethodDescription.Type.ADDITION, description = "groovyscript.wiki.minecraft.furnace.add0", example = @Example(value = "ore('ingotIron'), item('minecraft:diamond')", commented = true)) public void add(IIngredient input, ItemStack output) { - add(input, output, 0.1f); + recipeBuilder().input(input).output(output).register(); } @MethodDescription(type = MethodDescription.Type.ADDITION, description = "groovyscript.wiki.minecraft.furnace.add1", example = @Example("item('minecraft:nether_star'), item('minecraft:clay') * 64, 13")) public void add(IIngredient input, ItemStack output, float exp) { - if (GroovyLog.msg("Error adding Minecraft Furnace recipe") - .add(IngredientHelper.isEmpty(input), () -> "Input must not be empty") - .add(IngredientHelper.isEmpty(output), () -> "Output must not be empty") - .add(IngredientHelper.overMaxSize(input, 1), () -> "Input size must be 1") - .error() - .postIfNotEmpty()) { - return; - } - if (exp < 0) { - exp = 0.1f; - } - output = output.copy(); - for (ItemStack itemStack : input.getMatchingStacks()) { - add(new Recipe(itemStack, output, exp)); - } + recipeBuilder().exp(exp).input(input).output(output).register(); + } + + @MethodDescription(type = MethodDescription.Type.ADDITION, description = "groovyscript.wiki.minecraft.furnace.add2", example = @Example("item('minecraft:diamond'), item('minecraft:clay'), 2, 50")) + public void add(IIngredient input, ItemStack output, float exp, int time) { + recipeBuilder().time(time).exp(exp).input(input).output(output).register(); } @GroovyBlacklist public void add(Recipe recipe) { FurnaceRecipes.instance().addSmeltingRecipe(recipe.input, recipe.output, recipe.exp); + CustomFurnaceManager.TIME_MAP.put(recipe.input, recipe.time); addScripted(recipe); } @GroovyBlacklist - public boolean remove(Recipe recipe, boolean isScripted) { - return removeByInput(recipe.input, isScripted, isScripted); - } - - @GroovyBlacklist - private ItemStack findTrueInput(ItemStack input) { - ItemStack trueInput = FurnaceRecipeManager.inputMap.get(input); - if (trueInput == null && input.getMetadata() != Short.MAX_VALUE) { - input = new ItemStack(input.getItem(), input.getCount(), Short.MAX_VALUE); - trueInput = FurnaceRecipeManager.inputMap.get(input); - } - return trueInput; - } - - @MethodDescription(example = @Example("item('minecraft:clay')")) - public boolean removeByInput(ItemStack input) { - return removeByInput(input, true); - } - - public boolean removeByInput(ItemStack input, boolean log) { - return removeByInput(input, log, true); + public boolean remove(Recipe recipe) { + FurnaceRecipes.instance().getSmeltingList().remove(recipe.input, recipe.output); + CustomFurnaceManager.TIME_MAP.removeInt(recipe.input); + addBackup(recipe); + return true; } - @GroovyBlacklist - public boolean removeByInput(ItemStack input, boolean log, boolean isScripted) { - if (IngredientHelper.isEmpty(input)) { - if (log) { - GroovyLog.msg("Error adding Minecraft Furnace recipe") - .add(IngredientHelper.isEmpty(input), () -> "Input must not be empty") - .error() - .postIfNotEmpty(); - } + @MethodDescription(example = @Example("item('minecraft:clay:*')")) + public boolean removeByInput(IIngredient input) { + if (GroovyLog.msg("Error adding Minecraft Furnace recipe") + .add(IngredientHelper.isEmpty(input), () -> "Input must not be empty") + .error() + .postIfNotEmpty()) { return false; } - - ItemStack trueInput = findTrueInput(input); - if (trueInput == null) { - if (log) { - GroovyLog.msg("Error removing Minecraft Furnace recipe") - .add("Can't find recipe for input " + input) - .error() - .post(); + if (FurnaceRecipes.instance().getSmeltingList().entrySet().removeIf(entry -> { + if (input.test(entry.getKey())) { + addBackup(Recipe.of(entry.getKey(), entry.getValue())); + return true; } return false; - } - ItemStack output = FurnaceRecipes.instance().getSmeltingList().remove(trueInput); - if (output != null) { - float exp = FurnaceRecipes.instance().getSmeltingExperience(output); - Recipe recipe = new Recipe(trueInput, output, exp); - if (isScripted) addBackup(recipe); + })) { return true; - } else { - if (log) { - GroovyLog.msg("Error removing Minecraft Furnace recipe") - .add("Found input, but no output for " + input) - .error() - .post(); + } + var log = GroovyLog.msg("Error removing Minecraft Furnace recipe").error(); + log.add("Can't find recipe for input " + input); + if ((Object) input instanceof ItemStack is && is.getMetadata() != OreDictionary.WILDCARD_VALUE) { + var wild = new ItemStack(is.getItem(), 1, OreDictionary.WILDCARD_VALUE); + if (!FurnaceRecipes.instance().getSmeltingResult(wild).isEmpty()) { + log.add("there was no input found for {}, but there was an input matching the wildcard itemstack {}", GroovyScriptCodeConverter.asGroovyCode(is, false), GroovyScriptCodeConverter.asGroovyCode(wild, false)); } } - + log.post(); return false; } @MethodDescription(example = @Example("item('minecraft:brick')")) public boolean removeByOutput(IIngredient output) { - return removeByOutput(output, true); - } - - public boolean removeByOutput(IIngredient output, boolean log) { - return removeByOutput(output, log, true); - } - - @GroovyBlacklist - public boolean removeByOutput(IIngredient output, boolean log, boolean isScripted) { - if (IngredientHelper.isEmpty(output)) { - if (log) { - GroovyLog.msg("Error adding Minecraft Furnace recipe") - .add(IngredientHelper.isEmpty(output), () -> "Output must not be empty") - .error() - .postIfNotEmpty(); - } + if (GroovyLog.msg("Error adding Minecraft Furnace recipe") + .add(IngredientHelper.isEmpty(output), () -> "Output must not be empty") + .error() + .postIfNotEmpty()) { return false; } - - List recipesToRemove = new ArrayList<>(); - for (Map.Entry entry : FurnaceRecipes.instance().getSmeltingList().entrySet()) { + if (FurnaceRecipes.instance().getSmeltingList().entrySet().removeIf(entry -> { if (output.test(entry.getValue())) { - float exp = FurnaceRecipes.instance().getSmeltingExperience(entry.getValue()); - Recipe recipe = new Recipe(entry.getKey(), entry.getValue(), exp); - recipesToRemove.add(recipe); - } - } - if (recipesToRemove.isEmpty()) { - if (log) { - GroovyLog.msg("Error removing Minecraft Furnace recipe") - .add("Can't find recipe for output " + output) - .error() - .post(); + addBackup(Recipe.of(entry.getKey(), entry.getValue())); + return true; } return false; + })) { + return true; } - - for (Recipe recipe : recipesToRemove) { - if (isScripted) addBackup(recipe); - FurnaceRecipes.instance().getSmeltingList().remove(recipe.input); - } - - return true; + GroovyLog.msg("Error removing Minecraft Furnace recipe") + .add("Can't find recipe for output " + output) + .error() + .post(); + return false; } @MethodDescription(type = MethodDescription.Type.QUERY) public SimpleObjectStream streamRecipes() { List recipes = new ArrayList<>(); - for (Map.Entry entry : FurnaceRecipes.instance().getSmeltingList().entrySet()) { - float exp = FurnaceRecipes.instance().getSmeltingExperience(entry.getValue()); - recipes.add(new Recipe(entry.getKey(), entry.getValue(), exp)); - } - return new SimpleObjectStream<>(recipes, false).setRemover(recipe -> remove(recipe, true)); + FurnaceRecipes.instance().getSmeltingList().forEach((key, value) -> recipes.add(Recipe.of(key, value))); + return new SimpleObjectStream<>(recipes, false).setRemover(this::remove); } @MethodDescription(priority = 2000, example = @Example(commented = true)) public void removeAll() { FurnaceRecipes.instance().getSmeltingList().entrySet().removeIf(entry -> { - float exp = FurnaceRecipes.instance().getSmeltingExperience(entry.getValue()); - Recipe recipe = new Recipe(entry.getKey(), entry.getValue(), exp); + Recipe recipe = Recipe.of(entry.getKey(), entry.getValue()); addBackup(recipe); return true; }); } + @MethodDescription(type = MethodDescription.Type.ADDITION, description = "groovyscript.wiki.add_to_list") + public boolean addFuelConversion(CustomFurnaceManager.FuelConversionRecipe recipe) { + CustomFurnaceManager.FUEL_TRANSFORMERS.add(recipe); + return conversionStorage.addScripted(recipe); + } + + @MethodDescription(description = "groovyscript.wiki.remove_from_list") + public boolean removeFuelConversion(CustomFurnaceManager.FuelConversionRecipe recipe) { + return CustomFurnaceManager.FUEL_TRANSFORMERS.remove(recipe) && conversionStorage.addBackup(recipe); + } + + @MethodDescription(type = MethodDescription.Type.ADDITION, example = @Example("item('minecraft:diamond'), item('minecraft:bucket').transform(item('minecraft:lava_bucket'))")) + public boolean addFuelConversion(IIngredient smelted, IIngredient fuel) { + return addFuelConversion(new CustomFurnaceManager.FuelConversionRecipe(smelted, fuel)); + } + + @MethodDescription(example = @Example("item('minecraft:sponge', 1)")) + public boolean removeFuelConversionBySmeltedStack(ItemStack smelted) { + return CustomFurnaceManager.FUEL_TRANSFORMERS.removeIf(x -> x.smelted().test(smelted) && conversionStorage.addBackup(x)); + } + + @MethodDescription(type = MethodDescription.Type.QUERY, description = "groovyscript.wiki.streamRecipes") + public SimpleObjectStream streamFuelConversions() { + return new SimpleObjectStream<>(CustomFurnaceManager.FUEL_TRANSFORMERS).setRemover(this::removeFuelConversion); + } + + @MethodDescription(priority = 2000, example = @Example(commented = true)) + public void removeAllFuelConversions() { + CustomFurnaceManager.FUEL_TRANSFORMERS.removeIf(conversionStorage::addBackup); + } + @GroovyBlacklist @Override public void onReload() { - getScriptedRecipes().forEach(recipe -> remove(recipe, false)); + // since time is entirely custom, it can be cleared directly + CustomFurnaceManager.TIME_MAP.clear(); + getScriptedRecipes().forEach(recipe -> FurnaceRecipes.instance().getSmeltingList().remove(recipe.input, recipe.output)); getBackupRecipes().forEach(recipe -> FurnaceRecipes.instance().addSmeltingRecipe(recipe.input, recipe.output, recipe.exp)); + CustomFurnaceManager.FUEL_TRANSFORMERS.addAll(conversionStorage.restoreFromBackup()); + CustomFurnaceManager.FUEL_TRANSFORMERS.removeAll(conversionStorage.removeScripted()); } @Property(property = "input", comp = @Comp(eq = 1)) @Property(property = "output", comp = @Comp(eq = 1)) public static class RecipeBuilder extends AbstractRecipeBuilder { - @Property(comp = @Comp(gte = 0)) - private float exp = 0.1f; + @Property(comp = @Comp(gte = 0), defaultValue = "0.1f") + private float exp = EXPERIENCE_DEFAULT; + @Property(comp = @Comp(gte = 1), defaultValue = "200") + private int time = TIME_DEFAULT; @RecipeBuilderMethodDescription public RecipeBuilder exp(float exp) { @@ -205,6 +189,18 @@ public RecipeBuilder exp(float exp) { return this; } + @RecipeBuilderMethodDescription(field = "exp") + public RecipeBuilder experience(float exp) { + this.exp = exp; + return this; + } + + @RecipeBuilderMethodDescription + public RecipeBuilder time(int time) { + this.time = time; + return this; + } + @Override public String getErrorMsg() { return "Error adding Minecraft Furnace recipe"; @@ -214,9 +210,8 @@ public String getErrorMsg() { public void validate(GroovyLog.Msg msg) { validateItems(msg, 1, 1, 1, 1); validateFluids(msg); - if (exp < 0) { - exp = 0.1f; - } + msg.add(exp < 0, "exp must be a float greater than or equal to 0, yet it was {}", exp); + msg.add(time <= 0, "time must be an integer greater than 1, yet it was {}", time); } @Override @@ -224,8 +219,9 @@ public void validate(GroovyLog.Msg msg) { public @Nullable Recipe register() { if (!validate()) return null; Recipe recipe = null; - for (ItemStack itemStack : input.get(0).getMatchingStacks()) { - recipe = new Recipe(itemStack, output.get(0), exp); + var out = output.get(0); + for (ItemStack input : input.get(0).getMatchingStacks()) { + recipe = new Recipe(input, out, exp, time); VanillaModule.INSTANCE.furnace.add(recipe); } return recipe; @@ -237,11 +233,28 @@ public static class Recipe { private final ItemStack input; private final ItemStack output; private final float exp; + private final int time; + + private Recipe(ItemStack input, ItemStack output) { + this(input, output, EXPERIENCE_DEFAULT); + } private Recipe(ItemStack input, ItemStack output, float exp) { + this(input, output, exp, TIME_DEFAULT); + } + + private Recipe(ItemStack input, ItemStack output, float exp, int time) { this.input = input; this.output = output; this.exp = exp; + this.time = time; + } + + private static Recipe of(ItemStack input, ItemStack output) { + float exp = FurnaceRecipes.instance().getSmeltingExperience(output); + int time = CustomFurnaceManager.TIME_MAP.getInt(output); + if (time <= 0) time = TIME_DEFAULT; + return new Recipe(input, output, exp, time); } public ItemStack getInput() { @@ -255,5 +268,9 @@ public ItemStack getOutput() { public float getExp() { return exp; } + + public int getTime() { + return time; + } } } diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/FurnaceRecipeManager.java b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/FurnaceRecipeManager.java deleted file mode 100644 index 31c1f7a8a..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/FurnaceRecipeManager.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.cleanroommc.groovyscript.compat.vanilla; - -import com.cleanroommc.groovyscript.api.GroovyBlacklist; -import it.unimi.dsi.fastutil.Hash; -import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; -import net.minecraft.item.ItemStack; -import net.minecraftforge.oredict.OreDictionary; - -import java.util.Objects; - -@GroovyBlacklist -public class FurnaceRecipeManager { - - public static final ObjectOpenCustomHashSet inputMap = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() { - - @Override - public int hashCode(ItemStack o) { - return Objects.hash(o.getItem(), o.getMetadata()); - } - - @Override - public boolean equals(ItemStack a, ItemStack b) { - return a == b || (a != null && b != null && OreDictionary.itemMatches(a, b, false)); - } - }); -} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/FurnaceRecipeMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/FurnaceRecipeMixin.java deleted file mode 100644 index 85c5ef4ca..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/FurnaceRecipeMixin.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cleanroommc.groovyscript.core.mixin; - -import com.cleanroommc.groovyscript.compat.vanilla.FurnaceRecipeManager; -import net.minecraft.item.ItemStack; -import net.minecraft.item.crafting.FurnaceRecipes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(value = FurnaceRecipes.class) -public abstract class FurnaceRecipeMixin { - - @Inject(method = "addSmeltingRecipe", at = @At("RETURN")) - public void addRecipe(ItemStack input, ItemStack stack, float experience, CallbackInfo ci) { - FurnaceRecipeManager.inputMap.add(input); - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/furnace/TileEntityFurnaceMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/furnace/TileEntityFurnaceMixin.java new file mode 100644 index 000000000..cb2b3471e --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/furnace/TileEntityFurnaceMixin.java @@ -0,0 +1,49 @@ +package com.cleanroommc.groovyscript.core.mixin.furnace; + +import com.cleanroommc.groovyscript.compat.vanilla.CustomFurnaceManager; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntityFurnace; +import net.minecraft.util.NonNullList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TileEntityFurnace.class) +public class TileEntityFurnaceMixin { + + @Shadow + public NonNullList furnaceItemStacks; + + @ModifyReturnValue(method = "getCookTime", at = @At("RETURN")) + private int groovy$customCookTime(int original, ItemStack stack) { + int time = CustomFurnaceManager.TIME_MAP.getInt(stack); + return time <= 0 ? original : time; + } + + /** + * Skip the default bucket -> water bucket conversion when smelting a wet sponge, + * as its logic is replaced by an entry in {@link CustomFurnaceManager#FUEL_TRANSFORMERS}. + */ + @WrapWithCondition(method = "smeltItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/NonNullList;set(ILjava/lang/Object;)Ljava/lang/Object;", ordinal = 1)) + private boolean groovy$skipNormalBucketReplacement(NonNullList instance, int p_set_1_, E p_set_2_) { + return false; + } + + @Inject(method = "smeltItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;shrink(I)V")) + private void groovy$customFuelReplacement(CallbackInfo ci, @Local(ordinal = 0) ItemStack input) { + var fuel = furnaceItemStacks.get(1); + for (var fuelTransformer : CustomFurnaceManager.FUEL_TRANSFORMERS) { + if (!fuelTransformer.smelted().test(input)) continue; + if (!fuelTransformer.fuel().test(fuel)) continue; + var stack = fuelTransformer.fuel().applyTransform(fuel); + furnaceItemStacks.set(1, stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack); + return; // we can only correctly do one transformation, so only the first transformer operates. + } + } + +} diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStack2IntProxyMap.java b/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStack2IntProxyMap.java new file mode 100644 index 000000000..fa369387c --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStack2IntProxyMap.java @@ -0,0 +1,47 @@ +package com.cleanroommc.groovyscript.helper.ingredient.itemstack; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraftforge.oredict.OreDictionary; + + +/** + * Some Minecraft logic functions different when interacting with + * {@link ItemStack}s with metadata equal to {@link Short#MAX_VALUE} ({@link net.minecraftforge.oredict.OreDictionary#WILDCARD_VALUE}. + *

+ * This class handles this logic via two maps - + * {@link Object2IntOpenHashMap} and {@link Object2IntOpenCustomHashMap} (with the hash strategy being {@link ItemStackHashStrategy#STRATEGY}. + * The former is for if the {@link ItemStack} being checked has wildcard metadata, + * and the latter is for if it doesn't. + *

+ * This means that insertion inserts into one of two maps depending on metadata, + * and retrieval first checks the wildcard map before checking the metadata-specific map. + */ +public class ItemStack2IntProxyMap { + + private final Object2IntMap wildcard = new Object2IntOpenHashMap<>(); + private final Object2IntMap metadata = new Object2IntOpenCustomHashMap<>(ItemStackHashStrategy.STRATEGY); + + public int put(ItemStack key, int value) { + if (key.getItemDamage() == OreDictionary.WILDCARD_VALUE) return wildcard.put(key.getItem(), value); + return metadata.put(key, value); + } + + public int removeInt(ItemStack key) { + if (key.getItemDamage() == OreDictionary.WILDCARD_VALUE) return wildcard.removeInt(key.getItem()); + return metadata.removeInt(key); + } + + public int getInt(ItemStack key) { + if (wildcard.containsKey(key.getItem())) wildcard.getInt(key.getItem()); + return metadata.getInt(key); + } + + public void clear() { + wildcard.clear(); + metadata.clear(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStackHashStrategy.java b/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStackHashStrategy.java new file mode 100644 index 000000000..28163104d --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/helper/ingredient/itemstack/ItemStackHashStrategy.java @@ -0,0 +1,29 @@ +package com.cleanroommc.groovyscript.helper.ingredient.itemstack; + +import it.unimi.dsi.fastutil.Hash; +import net.minecraft.item.ItemStack; + +/** + * Hash strategy for fastutils that checks item and metadata. + * Note that in many cases, metadata equal to {@link Short#MAX_VALUE} + * (aka {@link net.minecraftforge.oredict.OreDictionary#WILDCARD_VALUE OreDictionary.WILDCARD_VALUE}) + * has special logic, and will need to be handled separately from the other ItemStacks. + *
+ * This cannot be part of the hash strategy, as doing so would require + * violating {@link Object#hashCode()}. + */ +public class ItemStackHashStrategy implements Hash.Strategy { + + public static final ItemStackHashStrategy STRATEGY = new ItemStackHashStrategy(); + + @Override + public int hashCode(ItemStack o) { + return 31 * o.getItem().hashCode() + o.getMetadata(); + } + + @Override + public boolean equals(ItemStack a, ItemStack b) { + return a == b || (a != null && b != null && ItemStack.areItemsEqual(a, b)); + } +} + diff --git a/src/main/resources/assets/groovyscript/lang/en_us.lang b/src/main/resources/assets/groovyscript/lang/en_us.lang index 2686c4778..eb13c1a67 100644 --- a/src/main/resources/assets/groovyscript/lang/en_us.lang +++ b/src/main/resources/assets/groovyscript/lang/en_us.lang @@ -148,10 +148,16 @@ groovyscript.wiki.minecraft.crafting.replaceShapeless0=Adds a shapeless recipe i groovyscript.wiki.minecraft.crafting.replaceShapeless1=Adds a shapeless recipe in the format `name`, `output`, `input` and removes the recipe matching the given name groovyscript.wiki.minecraft.furnace.title=Furnace -groovyscript.wiki.minecraft.furnace.description=Converts an input item into an output itemstack after a set amount of time, with the ability to give experience and using fuel to run. +groovyscript.wiki.minecraft.furnace.description=Converts an input item into an output itemstack after a configurable amount of time, with the ability to give experience and using fuel to run. Can also convert the item in the fuel slot. +groovyscript.wiki.minecraft.furnace.note0=Fuel Conversion Recipes may not function as desired in all furnaces - only the vanilla furnace has specific support. By default the only recipe reproduces the vanilla behavior of a wet sponge converting an empty bucket into a water bucket. groovyscript.wiki.minecraft.furnace.add0=Adds a recipe in the format `input`, `output` groovyscript.wiki.minecraft.furnace.add1=Adds a recipe in the format `input`, `output`, `experience` +groovyscript.wiki.minecraft.furnace.add2=Adds a recipe in the format `input`, `output`, `experience`, `time` groovyscript.wiki.minecraft.furnace.exp.value=Sets the experience rewarded for smelting the given input +groovyscript.wiki.minecraft.furnace.time.value=Sets the time in ticks the recipe takes +groovyscript.wiki.minecraft.furnace.addFuelConversion=Add a conversion recipe in the format `smelted`, `fuel`, with `fuel` using an IIngredient transformer +groovyscript.wiki.minecraft.furnace.removeFuelConversionBySmeltedStack=Removes all conversion recipes with the given smelted item +groovyscript.wiki.minecraft.furnace.removeAllFuelConversions=Removes all conversion recipes groovyscript.wiki.minecraft.ore_dict.title=Ore Dictionary groovyscript.wiki.minecraft.ore_dict.description=Manipulate the Ore Dictionary and what itemstacks are part of what oredicts. diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 1c1a8e98a..09c8c8191 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -12,7 +12,6 @@ "EventBusMixin", "FluidStackMixin", "ForgeRegistryMixin", - "FurnaceRecipeMixin", "InventoryCraftingAccess", "ItemMixin", "ItemStackMixin", @@ -22,6 +21,7 @@ "SlotCraftingAccess", "TileEntityPistonMixin", "VillagerProfessionAccessor", + "furnace.TileEntityFurnaceMixin", "groovy.AsmDecompilerMixin", "groovy.ClassNodeResolverMixin", "groovy.ClosureMixin",