-
-
Notifications
You must be signed in to change notification settings - Fork 342
Recipe API #1582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recipe API #1582
Conversation
I have a few suggestions for the shaped builder API: The current API statically allows to build incomplete recipes. For example a simple recipe with Ingredient a = ItemTypes.DIAMOND_AXE;
Ingredient s = Ingredient.builder().with(ItemTypes.LOG, ItemTypes.LOG2).build();
Sponge.getRegistry().getCraftingRecipeRegistry().register(plugin, "chopchop",
CraftingRecipe.shapedBuilder()
.aisle({{a, a}, {a, s}, {NONE, s}})
.result(ItemStack.of(ItemTypes.DIAMOND_AXE, 1))
.build());
The above example: Ingredient a = ItemTypes.DIAMOND_AXE;
Ingredient s = Ingredient.builder().with(ItemTypes.LOG, ItemTypes.LOG2).build();
Sponge.getRegistry().getCraftingRecipeRegistry().register(plugin, "chopchop",
CraftingRecipe.shapedBuilder()
.row(a, a)
.row(a, s)
.row(1, s)
.result(ItemStack.of(ItemTypes.DIAMOND_AXE, 1))
.build()); If the original intention of the string-pattern was to be able to reuse patterns, I can only say: functions. |
* {@link GridInventory} | ||
*/ | ||
public List<ItemStackSnapshot> getRemainingItems() { | ||
return remainingItems; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.
// TODO expose group String? | ||
|
||
/** | ||
* Checks if the given {@link GridInventory} fits the required constraints |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a GridInventory anymore
@pschichtel It should be expected that people who want to write plugins/mods should test their plugin/mod before releasing it for use. If they test, they'll see an exception mentioning an ingredient was forgotten, just like they would if they forgot to specify something on another builder (the
|
* Crafting recipes can only be crafted when all of the ingredients match | ||
* the items in the input grid. | ||
*/ | ||
public interface Ingredient extends Predicate<ItemStack> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may consider creating an ingredient from a game dictionary entry.
96a1a03
to
0c76943
Compare
@kashike by this logic, we should all start writing our projects in JavaScript or LUA. If we don't use the type-system, we even have one? Sure, if I write good code and have a 100% test coverage before every release, everything will work perfectly fine with either API. Then again, we're talking about Minecraft plugins, often written by beginner or intermediate skilled developers, barely tested if at all. Even skilled developers benefit from faster development cycles as the compiler can show the error much faster than deploying the plugin in a server, joining the server and testing the functionality. |
An exception would be thrown when |
@kashike consider larger plugins, developed by fairly inexperienced developers (there exist quite a few of those, some of them widely used). Even my first Bukkit plugin was downloaded like 20k times on bukkit dev. I didn't always test every little feature. Who has even the time for that? Or the userbase to run beta phases? A good builder kind of guides a user to a properly build object. Take a look at the jOOQ project, which is basically just a builder for SQL statements. |
@pschichtel This "oh crap" feature presents on 90% of the builders. Then, in your theory, builders should be never used because they are extremely likely for noobs to have issues on. |
A builder having an incomplete state at some point before |
Sure, an issue like that (registering a recipe later down the line, after a server has been running) can pop up. As can an NPE, SOE, IOOBE, etc. |
@pschichtel This is harder than you've expected to screw up. I've seen many "stupid modders" doing silly things around, but never one who fucked up the most basic recipes. It is easy to use and easy to understand. |
@liach Probably. That makes them consistent (which is not a bad property!), but in cases where a static verification is possible and easy, why not do it? I'm also perfectly aware, that these recipe builders are not hard to use. It's not about hard or easy, it's about preventing accidental mistakes, that can happen to anyone at any skill level in projects of any size. @Deamon5550 It is fine sure. There are also cases where a complete static verification is not possible. For example if Vanilla MC doesn't support non-square, that could not be modelled in the builder, without potentially breaking support for mods. I also don't think every builder in Sponge MUST be completely statically safe, as that would introduce a lot of changes, but it would easily possible to model them that way. As mentioned, take a look at jOOQ. @kashike Sure, any error can popup during run time, with or without recipes. This is not about making Plugins uncrashable, but about preventing one kind of error while at the same time increasing code quality. While the static safety is a good reason to consider my approach, it is also not the only one (actually not even my main one, I might have focused to much on that). Has anyone a compelling reason to require these characters for the ingredient mapping? I don't see any reason why they need to be there. They don't add any new information to the code, just a potential (possibly unlikely) source of errors. The fact, that they can easily be generated confirms this. "Because it was always this way" should not be a reason in a discussion about a new API. |
@Faithcaio Can you add a shortcut for plugin makers to turn an oredict entry into an ingredient? |
* @param entry The GameDictionary entry. | ||
* @return This Builder, for chaining | ||
*/ | ||
default Builder with(GameDictionary.Entry entry) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might consider adding display items from these entries too.
@Faithcaio How are recipes with Logic handled? e.g. buckets returning empty buckets, clone recipes, Fireworks, etc. |
@ryantheleach They are registered separately in code, not as jsons. |
Vanilla has a concept of items that are a container for the bucket logic. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this Recipe API and its good that it receives some attention again.
*/ | ||
void register(Recipe recipe); | ||
void register(Object plugin, String id, T recipe); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do I have to set the id here instead of on the recipe itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the recipes should be a catalog type? That's a good point.
* @param items The list of items. | ||
* @return This Builder, for chaining | ||
*/ | ||
Builder withDisplay(List<ItemStack> items); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also add a vararg variant and a variant with Snapshots and ItemTypes, because in the methods above you use them as well.
* @param result The resultant stack | ||
* @return The builder | ||
*/ | ||
Builder result(ItemStack result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should default implement this method to use the ItemStackSnapshot
one. And remove the default impl for the other one, because ItemStacks are mutable. Similar to the ShapelessRecipe one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vanilla expects ItemStacks. Seems pointless to convert Itemstacks into Snapshots just to turn them back into ItemStacks and copy them in the impl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well all other builders do the same (using Snapshots). And you have to copy them anyway.
Input | RecipeCreation | Usage |
---|---|---|
ItemStack | Copy | Copy 2 |
ItemStack | Snapshot | ItemStack Copy |
Because the input stack needs to be copied to prevent edits after recipe creation because of a dangling reference. And just for consistency i suggest using the snapshots everywhere.
PS: Default implementations can be overwritten
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still don't get it why I should convert everything into a Snapshot.
In the impl I have to convert everything back into ItemStack and then copy.
So it would convert ItemStack -> Snapshot -> ItemStack -> Copy
Instead of ItemStack -> Copy
Also why would anyone overwrite the Builder?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as the item stack don't change after they are sent into the registry, things will be fine. We just need to copy it in the implementation before we send it into the registry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ST-DDT If you provide both the ItemStackSnapshot
and the ItemStack
options, why does it matter which one is default implemented? It certainly shouldn't matter from a developer/API consumer standpoint - just because something is default implemented doesn't mean that it changes the API signature.
From a consistency standpoint, yes, including an option to supply an ItemStackSnapshot
as a result makes sense to me. However, default methods really are an implementation detail bleeding into the API package anyway, so if we want to be really pure about it, I'd say don't add default methods to new APIs at all, only to existing interfaces that get extended and plugin developers are expected to implement.
* @param ingredient The required ingredient | ||
* @return This builder, for chaining | ||
*/ | ||
Builder ingredient(ItemStackSnapshot ingredient); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe default implement this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How?
|
||
/** | ||
* Retrieves the recipe which would be crafted when the player clicks | ||
* the output slot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The javadocs seem to be wrong as smelting does not require clicking, does it?
@Override | ||
public int hashCode() { | ||
int result1 = this.result.hashCode(); | ||
long temp = Double.doubleToLongBits(this.experience); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Double.hashCode(experience)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just Objects.hashcode
should be fine, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apparantly this is how IntelliJ generates it
*/ | ||
Builder result(ItemStack result); | ||
|
||
// TODO results dependent on craftinggrid & world? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such a method would be nice. However I would split the builder for it in multiple subclasses to keep the interfaces and implementations as clean as possible (= less confusing).
AisleCraftingRecipeBuild
orMapping....
RowBased....
Dynamic...
@Faithcaio As of Minecraft 1.12, recipes are organized into registries. I wonder if it is necessary to put recipes into catalog types and Use a class to contain all vanilla recipes like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting way of splitting the builders (not what i imagined, but i guess that works as well)
@Faithcaio Although people cannot remove recipe in the way they remove advancements, however, the recipes will be disabled by recipe books. Do we add recipe books as a way to disable, rather than ban, recipes? |
We could add them, but i'd like to keep it separate from this PR. |
So this and Impl in Common is basicly done. Can we get this merged this weekend? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good aside from these, and some missing javadocs
@@ -304,6 +305,8 @@ | |||
|
|||
public static final Class<WorldGeneratorModifier> WORLD_GENERATOR_MODIFIER = WorldGeneratorModifier.class; | |||
|
|||
public static final Class<CraftingRecipe> CRAFTING_RECIPES = CraftingRecipe.class; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to be sorted
|
||
/** | ||
* The group this CraftingRecipe belongs to. | ||
* The group defines on which Tab the Recipe is shown in the RecipeBook. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no it doesn't
24b9daa
to
8cefab6
Compare
8cefab6
to
642a973
Compare
* @param world The world where the item would be crafted in | ||
* @return The recipe or {@link Optional#empty()} if no recipe is formed | ||
*/ | ||
Optional<CraftingRecipe> getRecipe(World world); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does World matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vanilla matches Recipes with an InventoryCrafting and a World.
In vanilla it is only used to get MapData.
Maybe we could provide some kind of CraftingContext instead?
* A CraftingGridInventory represents the inventory of something that can craft | ||
* items. This is excluding the Result slot. | ||
*/ | ||
public interface CraftingGridInventory extends GridInventory { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is result excluded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the equivalent to InventoryCrafting.
CraftingInventory is a view on InventoryCrafting and the ResultSlot
Any more concerns? If not I'll merge it today. |
* | ||
* @return The list of items to display the Ingredient in a recipe. | ||
*/ | ||
List<ItemStack> displayedItems(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ItemStack is mutable. Shouldn't this return a list of ItemStackSnapshot
s?
83584dd
to
97baaa6
Compare
CraftingRecipe with Ingredients SmeltingRecipes
97baaa6
to
b2dff8d
Compare
SpongeAPI|SpongeCommon|SpongeForge
Continuation of #1449 of #1098.
Updated for MC 1.12
RecipeAPI:
Recipe
: Base Recipe only has a basicgetExemplaryResult()
. For most Recipe this is also the actual result but it may vary.CraftingRecipe
: Recipes for a Crafting Window.Ingredient
: Predicate to match in CraftingRecipes.ShapedCraftingRecipe
: Ingredients must be arranged specificallyShaplessCraftingRecipe
: Ingredients can be arranged randomlySmeltingRecipe
: Recipes for Furnaces (uses ITemStacks not Ingredients)RecipeRegistry
: Base Registry for RecipesCraftingRecipeRegistry
: Registration of CraftingRecipesSmeltingRecipeRegistry
: Registration of SmeltingRecipesCraftingResult
: Result of a crafted RecipeExample Usage
OR implement the CraftingRecipe yourself to allow customizing the result based on input:

TODO and Discuss:
Ingredient.matchingStacks
in API? (displayedItems) used client side to show the items used in the recipedoLimitedCrafting
gamerule could block all recipes added via API.Ideas for further enhancement:
see #1585