Fabric API

Fabric API

106M Downloads

Recipe remainders

JamiesWhiteShirt opened this issue ยท 14 comments

commented
  1. There is no way to control the construction of ItemStacks for recipe remainders. This could be useful for recipes where items take damage during crafting.

  2. There is no non-hacky way to make an item have itself as a crafting remainder.

A Function<ItemStack, ItemStack> as part of the Item.Builder might be a good idea.

commented

Implemented in #2556.

commented

Mojang's code heavily assumes getting the recipe remainder directly from the item (furnace, brewing stand, crafting would all at least have to be patched). I'd say the few cases in which you'd want to transform an ItemStack instead of just supplying one are best handled with a custom recipe.

What do you think?

commented

I want to incorporate it as part of the ingredient entries, but has encountered some difficulty mainly for the limitation of mixin on package private classes.

commented

We've had an extensive discussion on this topic in Discord and one of the outcomes was that for a lot of cases, it'll be sufficient to implement a custom Recipe Subclass (i.e. of ShapelessRecipe) and matching Recipe Serializer, and then override the vanilla method Recipe#getRemainingStacks.

Using that approach, it should be possible to implement things that take damage during crafting.

commented

Example code for working around this limitation:

/**
 * An extended version of ShapelessRecipe to support damaging the QuartzKnife whenever it is used to craft something.
 */
public class QuartzKnifeRecipe extends ShapelessRecipe {

    public QuartzKnifeRecipe(ShapelessRecipe original) {
        super(
                original.getId(),
                ((ShapelessRecipeMixin) original).getGroup(),
                original.getOutput(),
                original.getPreviewInputs()
        );
    }

    @Override
    public RecipeSerializer<?> getSerializer() {
        return QuartzKnifeRecipeSerializer.INSTANCE;
    }

    @Override
    public DefaultedList<ItemStack> getRemainingStacks(CraftingInventory inventory) {
        DefaultedList<ItemStack> defaultedList = DefaultedList.ofSize(inventory.size(), ItemStack.EMPTY);

        for (int i = 0; i < defaultedList.size(); ++i) {
            ItemStack stack = inventory.getStack(i);
            Item item = stack.getItem();
/////////////////////////// THIS IS THE NEW STUFF ///////////////////////////
            if (item instanceof QuartzCuttingKnifeItem) {
                // If it still has durability left, put a more damaged one back, otherwise consume
                int newDamage = stack.getDamage() + 1;
                if (newDamage < stack.getMaxDamage()) {
                    stack = stack.copy();
                    stack.setDamage(newDamage);
                    defaultedList.set(i, stack);
                }
/////////////////////////// END OF THE NEW STUFF ///////////////////////////
            } else if (item.hasRecipeRemainder()) {
                defaultedList.set(i, new ItemStack(item.getRecipeRemainder()));
            }
        }

        return defaultedList;
    }

}

And a matching serializer:

public class QuartzKnifeRecipeSerializer extends ShapelessRecipe.Serializer {

    public static final QuartzKnifeRecipeSerializer INSTANCE = new QuartzKnifeRecipeSerializer();

    @Override
    public ShapelessRecipe read(Identifier identifier, JsonObject jsonObject) {
        return new QuartzKnifeRecipe(super.read(identifier, jsonObject));
    }

    @Override
    public ShapelessRecipe read(Identifier identifier, PacketByteBuf packetByteBuf) {
        return new QuartzKnifeRecipe(super.read(identifier, packetByteBuf));
    }

}

The only thing that somewhat sucks is that getGroup is client-side only in ShapelessRecipe, so I've added an accessor mixin as well:

@Mixin(ShapelessRecipe.class)
public interface ShapelessRecipeMixin {

    @Accessor("group")
    String getGroup();

}

Then register the serializer as usual:

Registry.register(Registry.RECIPE_SERIALIZER, AppEng.makeId("quartz_knife"), QuartzKnifeRecipeSerializer.INSTANCE);

And use it in your recipe for profit:

{
  "type": "appliedenergistics2:quartz_knife",
  "result": {
    "item": "appliedenergistics2:cable_anchor",
    "count": 3
  },
  "ingredients": [
    {
      "tag": "appliedenergistics2:metal_ingots"
    },
    {
      "tag": "appliedenergistics2:knife"
    }
  ]
}

Boom! Knife will be damaged and eventually consumed when crafting using this recipe.

commented

Indeed. Registering recipe remainder as an intrinsic property of an item is problematic as it causes some classloading issues and circular references.

commented

bumping an old issue to say that something like this is probably still needed. requiring everyone in the world using your item to also use your recipe type is infeasible.

I definitely need it for Botania, and I think a simple function stack -> stack in the settings will suffice.

commented

Found this issue because I was looking for something similar to damage the item. If you want something more generic I just skipped the recipe remainder entirely and checked if the item could take damage, and if it could apply the damage to the item. Maybe this is a dumb way to do it but I felt it that the

requiring everyone in the world using your item to also use your recipe type is infeasible.

requirement was removed doing it this way.

public DamageDurabilityRecipe(ShapelessRecipe shapeless) {
    super(shapeless.getId(), shapeless.getGroup(), shapeless.getOutput(), shapeless.getPreviewInputs());
}

@Override
public DefaultedList<ItemStack> getRemainingStacks(CraftingInventory inventory) {
    DefaultedList<ItemStack> defaultedList = DefaultedList.ofSize(inventory.size(), ItemStack.EMPTY);

    for(int i = 0; i < defaultedList.size(); ++i) {
        ItemStack itemStack = inventory.getStack(i);
        if (itemStack.isDamageable()) {
            int newDamage = itemStack.getDamage() + 1;
            if (newDamage < itemStack.getMaxDamage()) {
                itemStack = itemStack.copy();
                itemStack.setDamage(newDamage);
                defaultedList.set(i, itemStack);
            }
        }
    }

    return defaultedList;
}

@Override
public RecipeSerializer<?> getSerializer() {
    return Croptopia.DAMAGE_DURABILITY;
}

public static final DamageDurabilitySerializer DAMAGE_DURABILITY = registerSerializer("crafting_damage_durability", new DamageDurabilitySerializer());
Same serialize and register method as previously posted above and then this is the result.

{ "type": "modid:crafting_damage_durability", "ingredients": [ { "item": "minecraft:diamond_sword" }, { "item": "minecraft:diamond" } ], "result": { "item": "minecraft:diamond", "count": 2 } }
I agree there should be something else to be able to use an item Remainder, whether that's a function or a boolean in the Settings builder that will just tell the Item constructor to set the private Item to itself.

commented

Consensus from reliable members of the community seems to be that this is both necessary for the port of certain mods and for the proper function of certain item archetypes. It seems like a great candidate for a PR to start being drafted.

commented

Anyone who takes this on should also try to implement #116 at the same time

commented

Any progress on this? Would be nice to be able to use a vanilla feature without having to use mixins

commented

The linked PR is pending an update. Also not sure why you refer to this as a vanilla feature given that it's not in vanilla.

commented

The linked PR is pending an update. Also not sure why you refer to this as a vanilla feature given that it's not in vanilla.

What I meant is that recipe remainders are implemented in vanilla and are used by vanilla (buckets for example) so it would be nice to be able to use the same feature in mods

commented

You can use them with item settings, but they will not be ItemStack-aware, which is what this issue is for.