Generalize recipes and implement them#1119
Generalize recipes and implement them#1119Limeth wants to merge 1 commit intoSpongePowered:bleedingfrom
Conversation
| /** | ||
| * Delegates all NMS methods to the abstract methods | ||
| */ | ||
| public abstract class AbstractSpongeShapedCraftingRecipe extends ShapedRecipes implements ShapedCraftingRecipe { |
There was a problem hiding this comment.
Nope.
Mix ShapedCraftingRecipe into ShapedRecipes. Otherwise, mod recipes won't be API recipes (and neither will Mojang ones...)
| private final ShapedCraftingRecipe delegate; | ||
|
|
||
| public DelegateShapedCraftingRecipe(ShapedCraftingRecipe delegate) { | ||
| Preconditions.checkNotNull(delegate, "The delegate must not be null"); |
There was a problem hiding this comment.
This method is usually statically imported:
`checkNotNull(delegate, "delegate");
| return false; | ||
| } | ||
|
|
||
| if (recipeNMS.getMetadata() != 32767 && recipeNMS.getMetadata() != inventoryNMS.getMetadata()) |
There was a problem hiding this comment.
What is 32767 for a magic number here?
Either create a constant with a meaningful name or add a comment.
There was a problem hiding this comment.
I don't unfortunately know what exactly it is.
ade6f80 to
c3a0c37
Compare
| .map(itemStack -> itemStack.getItem() == ItemTypes.NONE) | ||
| .orElse(true); | ||
|
|
||
| if (!empty) |
| finalAisleX + "; " + finalAisleY + "] is not present. There should be" + | ||
| " one for each symbol in the aisle!")); | ||
|
|
||
| if (!ingredientPredicate.test(itemStack.createSnapshot())) |
| Preconditions.checkNotNull(mainItem); | ||
|
|
||
| this.mainItem = mainItem; | ||
| this.remainingItems = remainingItems == null ? ImmutableList.of() : ImmutableList.copyOf(remainingItems); |
There was a problem hiding this comment.
Mark the parameter @Nullable if it is to be null.
|
|
||
| @Override | ||
| public ItemStackSnapshot getMainItem() { | ||
| return mainItem; |
|
|
||
| @Override | ||
| public List<ItemStackSnapshot> getRemainingItems() { | ||
| return remainingItems; |
|
|
||
| @Override | ||
| public int getHeight() { | ||
| return recipeHeight; |
|
|
||
| @Override | ||
| public Optional<Predicate<ItemStackSnapshot>> getIngredientPredicate(int x, int y) { | ||
| return Optional.ofNullable(ingredients.get(x, y)); |
| checkState(!this.aisle.isEmpty(), "aisle must be set before setting aisle symbols"); | ||
|
|
||
| if (ingredient != null) { | ||
| ingredientMap.put(symbol, ingredient); |
|
|
||
| @Final | ||
| @Shadow | ||
| protected int recipeWidth; |
There was a problem hiding this comment.
squash it onto one line:
@Final @Shadow protected int recipeWidth
| private net.minecraft.item.ItemStack[] recipeItems; | ||
|
|
||
| public Optional<Predicate<ItemStackSnapshot>> sponge$getIngredientPredicate(int x, int y) { | ||
| if(x < 0 || x >= recipeWidth || y < 0 || y >= recipeHeight) |
There was a problem hiding this comment.
Missing brackets; space if (
|
I am currently kind of stuck. I can't quite figure out how to fix this mixin error I'm getting. I thought that maybe I had the hierarchy wrong and tried making As a side question, does anyone know if there is an easy way to achieve this? |
|
^ and that sir, is why you don't copy names from classes you're trying to mixin :P |
|
@Limeth You asked for help? How can I help you? |
|
I think I may have figured out the conversion between native and Sponge inventories, yay. See the latest commit. |
|
@ST-DDT Thanks for the response! The errors seem to have magically disappeared when I switched machines and I think I've just made breakthrough step in the implementation of this PR, so I don't need help for now. I'm sure I'll get stuck again very soon, though! :D |
|
Crafting recipes are working now. \o/ |
| public abstract Slot getSlot(int slotId); | ||
|
|
||
| @Shadow | ||
| public ItemStack slotClick(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player) { |
There was a problem hiding this comment.
If the method is public, you can mark it as abstract and avoid the actual method body.
There was a problem hiding this comment.
Also, we prefer that the @Shadow annotations are on the same line as the method (unlike @Override).
There was a problem hiding this comment.
I would, but then I wouldn't be able to call it from here.
| }) | ||
| public abstract class MixinContainerCrafting extends MixinContainer { | ||
|
|
||
| private final Container this$ = (Container) (Object) this; |
There was a problem hiding this comment.
Just inline this field, this otherwise is ugly.
|
|
||
| @SuppressWarnings("unchecked") | ||
| private List<CraftingRecipe> spongeRecipes() { | ||
| return (List<CraftingRecipe>) (List<?>) recipes; |
There was a problem hiding this comment.
Why is there a level of indirection? It doesn't make sense unless there's something I'm missing.
There was a problem hiding this comment.
You have getRecipes which is an override, and this spongeRecipes, which is something else? You can safely just eliminate this method entirely.
| boolean matches(InventoryCrafting inv, net.minecraft.world.World worldIn); | ||
|
|
||
| @Shadow | ||
| net.minecraft.item.ItemStack getCraftingResult(InventoryCrafting inv); |
There was a problem hiding this comment.
Same line, and preferably keep these without spaces at the top of the mixin.
| import java.util.stream.Collectors; | ||
|
|
||
| @Mixin(IRecipe.class) | ||
| public interface MixinIRecipe extends CraftingRecipe { |
There was a problem hiding this comment.
What you may want to do is annotate the class with @Implements(iface = @Interface(CraftingRecipe.class), prefix = "", unique = true)
There was a problem hiding this comment.
I had it like that before, but ended up making the interface extend CraftingRecipe directly. Why should I put it back?
There was a problem hiding this comment.
the unique bits, it ensures that the methods won't overlap vanilla methods, but you can have both extending and @Implements. Right, @Mumfrey?
There was a problem hiding this comment.
[22:18:47] [main/ERROR] [mixin]: mixins.common.core.json:item.recipe.crafting.MixinIRecipe: Prefix for iface Lorg/spongepowered/api/item/recipe/crafting/CraftingRecipe; is not valid
There was a problem hiding this comment.
@gabizou ^ that's what is printed to the console when I use @Implements, I guess I should remove it?
| @Override | ||
| public RecipeRegistry getRecipeRegistry() { | ||
| public SmeltingRecipeRegistry getSmeltingRecipeRegistry() { | ||
| throw new UnsupportedOperationException(); // TODO |
There was a problem hiding this comment.
Does this PR not implement the smelting recipe registry?
There was a problem hiding this comment.
Not yet. I can either start implementing the smelting recipe registry in this PR, or make another one when this is one is merged.
|
|
||
| @Nonnull | ||
| public static Optional<ItemStack> getContainerItem(@Nonnull ItemStack itemStack) { | ||
| Preconditions.checkNotNull(itemStack, "The itemStack must not be null"); |
There was a problem hiding this comment.
methods from Preconditions should be statically imported.
| @@ -36,7 +36,7 @@ | |||
| // TODO | |||
There was a problem hiding this comment.
This class was a stub. you should either implement it or remove if not needed
|
|
||
| @Override | ||
| public ItemStackSnapshot getExemplaryResult() { | ||
| return recipe.getExemplaryResult(); |
There was a problem hiding this comment.
Field accesses should be qualified with this. consistently throughout the codebase
Protip: most IDEs can do this automatically
| net.minecraft.item.ItemStack inventoryNMS = ItemStackUtil.fromSnapshotToNative(inventoryStack); | ||
|
|
||
| if (!recipeNMS.isEmpty() || !inventoryNMS.isEmpty()) | ||
| { |
There was a problem hiding this comment.
incorrect formatting, should have brace on previous line
(I know this is copied from vanilla code but formatting should be consistent with sponge's code)
|
Smelting recipes are now working too. \o/ |
gabizou
left a comment
There was a problem hiding this comment.
Code cleanup. Lots of mixed coding style, not sure if it's because it's from multiple authors or different IDE setups, but needs to match the current coding standards.
|
|
||
| // Crafting | ||
|
|
||
| @Nonnull |
There was a problem hiding this comment.
This is already included in the package level annotations for parameters, methods, and fields. Don't clutter it up in the implementation.
There was a problem hiding this comment.
It's giving me warnings when it's not there. ¯_(ツ)_/¯
There was a problem hiding this comment.
You have to tell your IDE what the NonnullByDefault annotation is, look it up for your ide.
| checkNotNull(itemStack, "The itemStack must not be null"); | ||
|
|
||
| net.minecraft.item.ItemStack nmsStack = ItemStackUtil.toNative(itemStack); | ||
| Item nmsItem = nmsStack.getItem(); |
There was a problem hiding this comment.
Could probably do some short circuit to ItemStack.isValid() which was added in 1.11.
There was a problem hiding this comment.
I can't seem to find that method.
| } | ||
|
|
||
| @Override | ||
| @Nonnull |
| import java.util.function.Supplier; | ||
|
|
||
| public abstract class AbstractSpongeCraftingRecipe implements CraftingRecipe, IRecipe { | ||
| @Override |
There was a problem hiding this comment.
Spacing here, group static methods together versus mixed in with instance methods. Or, you can mark the groups with comments.
| return matches(this::isValid, inv, worldIn); | ||
| } | ||
|
|
||
| public static boolean matches(BiFunction<GridInventory, World, Boolean> isValid, InventoryCrafting inv, net.minecraft.world.World worldIn) { |
There was a problem hiding this comment.
Do recipes require World instances? I'd imagine that we'd only ever want to deal with WorldServer instances (not client based worlds).
There was a problem hiding this comment.
Yes, see CraftingManager#getRemainingItems(InventoryCrafting, World).
|
|
||
| @Override | ||
| @Nonnull | ||
| public Optional<Predicate<ItemStackSnapshot>> getIngredientPredicate(int x, int y) { |
There was a problem hiding this comment.
Why is this even an Optional? Sounds like bad use to be honest. If anything, it can just be any or none predicate.
| private final List<SmeltingRecipe> recipes = Lists.newArrayList(); | ||
|
|
||
| /** | ||
| * @author Limeth |
There was a problem hiding this comment.
Need more than this. Needs a date, needs some @reason explanation.
| */ | ||
| @Overwrite | ||
| public void addSmeltingRecipe(ItemStack input, ItemStack stack, float experience) | ||
| { |
There was a problem hiding this comment.
The coding style is awkward here.
| Predicate<ItemStackSnapshot> ingredientPredicate = new MatchSmeltingVanillaItemStack(exemplaryIngredient); | ||
|
|
||
| this.recipes.add(new SpongeSmeltingRecipe(exemplaryResult, | ||
| exemplaryIngredient, ingredientPredicate, experience)); |
| import org.spongepowered.api.event.cause.entity.damage.DamageType; | ||
| import org.spongepowered.api.event.cause.entity.damage.source.*; | ||
| import org.spongepowered.api.event.cause.entity.spawn.*; | ||
| import org.spongepowered.api.event.cause.entity.damage.source.BlockDamageSource; |
There was a problem hiding this comment.
Don't touch star imports from things that you're not touching.
|
|
||
| @Override | ||
| public int getRecipeSize() { | ||
| return getRecipeSize(this::getSize); |
There was a problem hiding this comment.
Consistency with other static methods in this class; see usage in SpongeShapedCraftingRecipe.
There was a problem hiding this comment.
wat? I'm still very confused as to why this is necessary? seems like a massive hack/anti-pattern.
There was a problem hiding this comment.
SpongeShapedCraftingRecipe must extend ShapedRecipes, but cannot extend two classes at the same time, so methods in AbstractSpongeCraftingRecipe are declared statically and are called from within SpongeShapedCraftingRecipe.
There was a problem hiding this comment.
Move them to an interface?
There was a problem hiding this comment.
or just duplicate the implementations, anything but this.
There was a problem hiding this comment.
The pattern is just IoC but that simple fact is being occluded by the actual method of implementation. I had the same problem with Adapter but solved it in a slightly clearer way of abstracting the logic away to an inner class Adapter.Logic and then any Trait mixins call back against the static methods therein.
Honestly for consistency a similar approach should be used here. Instead of static methods in a base class, wrap the static methods in a Logic or Operations class and call back against it. The mixins should also probably be Trait in this case to denote their usage.
There was a problem hiding this comment.
I can't quite figure out what you mean by the Trait and can't quite picture how the end result would look, would you please link me the code you wrote?
| /** | ||
| * @author Limeth | ||
| */ | ||
| @Overwrite |
There was a problem hiding this comment.
All these overwrites make me twitchy. You should atleast maintain the old functionality of the class (ie. still put things into the maps) or you're going to break mods.
|
@Deamon5550 I've rewritten the |
| */ | ||
| @Overwrite | ||
| public float getSmeltingExperience(ItemStack stack) { | ||
| @Inject(method = "getSmeltingExperience", at = @At("RETURN")) |
There was a problem hiding this comment.
Shouldn't this be also set to cancellable?
There was a problem hiding this comment.
It's using setReturnValue so yes, it needs to be cancellable or it will throw an exception if that code path is followed.
| } | ||
|
|
||
| public static MatchCraftingVanillaItemStack never() { | ||
| return new MatchCraftingVanillaItemStack(); |
There was a problem hiding this comment.
IMO it would be better if you just return/use a simple predicate -> false here.
You could even cache the instance, if it is needed at all. (See the other comment)
| @Override | ||
| public Predicate<ItemStackSnapshot> getIngredientPredicate(int x, int y) { | ||
| if (x < 0 || x >= recipeWidth || y < 0 || y >= recipeHeight) { | ||
| return MatchCraftingVanillaItemStack.never(); |
There was a problem hiding this comment.
I would rather throw an IndexOutOfBoundsException here.
| * @return Whether the stacks match according to the vanilla Minecraft | ||
| * behavior | ||
| */ | ||
| public static boolean matchesVanillaItemStack(ItemStackSnapshot recipeStack, ItemStackSnapshot inventoryStack) { |
There was a problem hiding this comment.
Suggestion: We should probably expose this somewhere in the API. (Maybe in the CraftingRecipeRegistry or ISS?)
Either as method or as static field storing a Function<ISS,Predicate<ISS>>.
That way we allow some default matching combined with .and(someCostumConditions).
What do you think about it? Or shall we do that later?
There was a problem hiding this comment.
The behavior is specific to crafting recipes, smelting recipes use a different method, so I think it's a good idea to expose it in the CraftingRecipeRegistry and do the same for smelting recipes.
There was a problem hiding this comment.
I'm not fond of just a "catch all" matching for ItemStacks since there can be various "this is allowed" matches, including things where it just matches the ItemType, ItemType and quantity, ItemType and NBT, all of the above, etc.
|
Some feedback would be welcome. |
kashike
left a comment
There was a problem hiding this comment.
Please reformat to follow our codestyle. You are missing this. everywhere, braces missing/on wrong lines, etc.
Additionally, be sure to checkNotNull parameters to give a nicer NullPointerException message.
| Iterator<IInventory> inventories = fabric.allInventories().iterator(); | ||
| InventoryCrafting inventoryCrafting = (InventoryCrafting) inventories.next(); | ||
|
|
||
| if(inventories.hasNext()) |
| ItemStack nativeIngredient = ItemStackUtil.fromSnapshotToNative(ingredient); | ||
|
|
||
| for (Map.Entry<ItemStack, ItemStack> entry : this.smeltingList.entrySet()) | ||
| { |
|
|
||
| @Override | ||
| public int getWidth() { | ||
| return recipeWidth; |
|
|
||
| @Override | ||
| public int getHeight() { | ||
| return recipeHeight; |
| @Override | ||
| @Nonnull | ||
| public List<Predicate<ItemStackSnapshot>> getIngredientPredicates() { | ||
| return recipeItems.stream() |
| public MatchSmeltingVanillaItemStack(ItemStackSnapshot itemStackSnapshot) { | ||
| checkNotNull(itemStackSnapshot, "The itemStackSnapshot must not be null"); | ||
|
|
||
| this.itemStackSnapshot = itemStackSnapshot; |
There was a problem hiding this comment.
this.itemStackSnapshot = checkNotNull(itemStackSnapshot, "The itemStackSnapshot must not be null");
|
|
||
| @Override | ||
| public int getSize() { | ||
| return ingredients.size(); |
|
|
||
| @Override | ||
| public List<ItemStackSnapshot> getRemainingItems(GridInventory grid) { | ||
| return StreamSupport.stream(grid.<Slot>slots().spliterator(), false) |
There was a problem hiding this comment.
checkNotNull(grid, "grid")
|
|
||
| @Override | ||
| public ItemStackSnapshot getExemplaryResult() { | ||
| return exemplaryResult; |
|
|
||
| @Override | ||
| public List<Predicate<ItemStackSnapshot>> getIngredientPredicates() { | ||
| return ingredients; |
2d6e838 to
e4544b3
Compare
|
Changes made as suggested, waiting for more feedback. |
|
This PR is not dead! I am determined to get it merged. :D |
|
@Limeth: Can you rebase this against bleeding? |
ee4c52c to
9e86bcb
Compare
|
Rebased against bleeding. |
|
@Limeth: There still appear to be merge conflicts |
9e86bcb to
f5d4cec
Compare
|
Conflicting commits appeared within that hour. Rebased again, should be fine now. |
gabizou
left a comment
There was a problem hiding this comment.
Multiple changes needed, mostly style, but there's too much noise at the moment.
| ItemStack containerStack = ItemStackUtil.fromNative(nmsContainerStack); | ||
|
|
||
| return Optional.of(containerStack); | ||
| } else { |
| checkNotNull(world, "world"); | ||
|
|
||
| return Sponge.getRegistry().getCraftingRecipeRegistry() | ||
| .findMatchingRecipe(this.craftingGrid, world); |
| @Override | ||
| protected void init(SlotProvider<IInventory, ItemStack> slots) { | ||
| super.init(slots, false); | ||
|
|
There was a problem hiding this comment.
I like to separate variable assignments and method calls with a space.
There was a problem hiding this comment.
That's fine, but you shouldn't do this here because you haven't changed anything else in this class.
| * @return Whether the stacks match according to the vanilla Minecraft | ||
| * behavior | ||
| */ | ||
| public static boolean matchesVanillaItemStack(ItemStackSnapshot recipeStack, ItemStackSnapshot inventoryStack) { |
There was a problem hiding this comment.
I'm not fond of just a "catch all" matching for ItemStacks since there can be various "this is allowed" matches, including things where it just matches the ItemType, ItemType and quantity, ItemType and NBT, all of the above, etc.
| @Override | ||
| public ShapedCraftingRecipe.Builder where(char symbol, Predicate<ItemStackSnapshot> ingredient) throws IllegalArgumentException { | ||
| checkState(!this.aisle.isEmpty(), "aisle must be set before setting aisle symbols"); | ||
|
|
There was a problem hiding this comment.
You have a lot of extra new lines that just add empty space, and it's getting distracting/excessive. Condense where possible.
There was a problem hiding this comment.
Should I just remove all empty lines? How can I tell which ones you want preserved? Removing this one in particular goes against my skin, because not separating a code block with a new line looks confusing. I'd like to point out that imo I've spent too much time fixing code style by reading the reviews, as most of them have been about it. It's quite annoying having to wait for the reviews and finding out it's just another one about code style. It would be much easier for me if I had a clear understandable guide on what code style you prefer.
There was a problem hiding this comment.
There was a problem hiding this comment.
Thanks @ziceptor, I must've missed that. The question stays, though: Which empty lines are distracting? Which should I keep/remove?
There was a problem hiding this comment.
I'm commenting where I'm finding useless new lines for you to get an idea.
|
|
||
| checkState(width > 0, "The aisle cannot be empty."); | ||
|
|
||
| do { |
There was a problem hiding this comment.
This is a very weird loop. I'm confused as to why it can't just be a for-loop with the iterator...
| byIncreasingTheSlotIndex: | ||
| for (int x = 0; x < grid.getColumns(); x++) { | ||
| ItemStackSnapshot itemStackSnapshot = grid.getSlot(x, y) | ||
| .flatMap(Slot::peek).map(ItemStack::createSnapshot) |
There was a problem hiding this comment.
Don't have multiple calls on the same line for chaining in this case. split up between two separate lines.
|
|
||
| if (ingredient.test(itemStackSnapshot)) { | ||
| iterator.remove(); | ||
|
|
| continue; | ||
| } | ||
|
|
||
| Iterator<Predicate<ItemStackSnapshot>> iterator = ingredients.iterator(); |
There was a problem hiding this comment.
This logic is lacking comments to see what is actually going on (Yes, it's valid checking, but how is it valid checking?)
|
|
||
| NonNullList<ItemStack> result = NonNullList.withSize(inv.getSizeInventory(), ItemStack.EMPTY); | ||
|
|
||
| for(int i = 0; i < spongeResult.size(); i++) { |
There was a problem hiding this comment.
You need a space after for
|
|
||
| for (IRecipe irecipe : this.recipes) { | ||
| if (irecipe.matches(nativeInventory, (net.minecraft.world.World) world)) | ||
| { |
There was a problem hiding this comment.
Should be on the previous line
gabizou
left a comment
There was a problem hiding this comment.
Commented for some clarification of changes requested.
| return RecipeUtil.findMatchingRecipe(this.inventory, this.craftingLens.getCraftingGrid(), this.craftingLens.getOutputSlot()); | ||
| public Optional<CraftingRecipe> getRecipe(World world) { | ||
| checkNotNull(world, "world"); | ||
|
|
There was a problem hiding this comment.
This line is a distracting useless line, adding an extra whitespace where it's not really necessary or used.
| public static GridInventory toSpongeInventory(InventoryCrafting inv) { | ||
| DefaultInventoryFabric fabric = new DefaultInventoryFabric(inv); | ||
| GridInventoryLensImpl lens = new GridInventoryLensImpl(0, inv.getWidth(), inv.getHeight(), inv.getWidth(), SlotLensImpl::new); | ||
|
|
There was a problem hiding this comment.
This line is unnecessary extra, there's no real reason for it to be honest.
| if (inventories.hasNext()) { | ||
| throw new IllegalStateException("Another inventory found: " + inventories.next()); | ||
| } | ||
|
|
| Fabric<IInventory> fabric = ((GridInventoryAdapter) inv).getInventory(); | ||
| Iterator<IInventory> inventories = fabric.allInventories().iterator(); | ||
| InventoryCrafting inventoryCrafting = (InventoryCrafting) inventories.next(); | ||
|
|
There was a problem hiding this comment.
This line is "ok" since you have some logic going on in the next few lines.
| public static ItemStack getCraftingResult(Function<GridInventory, ItemStackSnapshot> getResult, InventoryCrafting inv) { | ||
| ItemStackSnapshot result = getResult.apply(toSpongeInventory(inv)); | ||
|
|
||
| Preconditions.checkNotNull(result, "The Sponge implementation returned a `null` result."); |
There was a problem hiding this comment.
You can static import the checkNotNull method (We do so throughout the implementation)
| int gridY = gapY + (gapY >= offsetY ? getHeight() : 0); | ||
| boolean empty = grid.getSlot(gridX, gridY) | ||
| .flatMap(Slot::peek) | ||
| .map(itemStack -> itemStack.getItem() == ItemTypes.NONE) |
There was a problem hiding this comment.
Does this need to check for isEmpty() itemstacks?
| if (aisle != null) { | ||
| Collections.addAll(this.aisle, aisle); | ||
| } | ||
|
|
| @Override | ||
| public ShapedCraftingRecipe.Builder where(char symbol, Predicate<ItemStackSnapshot> ingredient) throws IllegalArgumentException { | ||
| checkState(!this.aisle.isEmpty(), "aisle must be set before setting aisle symbols"); | ||
|
|
There was a problem hiding this comment.
I'm commenting where I'm finding useless new lines for you to get an idea.
| @Override | ||
| public ShapedCraftingRecipe.Builder result(ItemStackSnapshot result) { | ||
| Preconditions.checkNotNull(result, "result"); | ||
|
|
| Preconditions.checkNotNull(result, "result"); | ||
|
|
||
| this.result = result; | ||
|
|
|
Superseded by #1395 |
SpongeAPI|SpongeCommon|SpongeForge
This is a WIP continuation of kashike's PR, with his consent.
It aims to generalize how recipes are registered and handled. Instead of checking the equality of the ingredients, it works by testing ingredient predicates. It also aims to let the user implement the recipe interfaces by themselves, making it, for example, possible to transfer NBT data from the ingredients to the result.
In this PR,
ItemStackSnapshots are used extensively. This is to prevent the user from modifying the originalItemStack. I could instead provide anItemStack#copy, but that doesn't make it visually obvious the original item won't change.Crafting recipes
All recipes stored in
CraftingManagermust implement bothIRecipeandCraftingRecipe.Vanilla, mods:
delegation to native
IRecipe--MixinIRecipeimplementsCraftingRecipeShapedRecipes--MixinShapedRecipesimplementsShapedCraftingRecipeShapelessRecipes--MixinShapelessRecipesimplementsShapelessCraftingRecipeSponge:
delegation to sponge by overriding both native & sponge
CraftingRecipe-- wrap inSpongeCustomCraftingRecipe? Is it possible to mix-inIRecipe? Feedback needed.ShapedCraftingRecipe-- extendsShapedRecipesShapelessCraftingRecipe-- extendsShapelessRecipes