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",