From 32368f8388a496c0a91ddb2174007bd60d7bb53d Mon Sep 17 00:00:00 2001
From: modmuss50 <modmuss50@gmail.com>
Date: Fri, 28 Jan 2022 19:18:43 +0000
Subject: [PATCH] First look at data generated machine recipes. Doesn't support
 all the features but should be a good start.

---
 .../src/main/java/reborncore/RebornCore.java  |   3 -
 .../crafting/ingredient/FluidIngredient.java  |   1 -
 ...Ingredient.java => IngredientFactory.java} |  53 ++---
 .../ingredient/IngredientManager.java         |  76 -------
 .../crafting/ingredient/RebornIngredient.java |  30 +--
 .../crafting/ingredient/StackIngredient.java  |   1 -
 .../crafting/ingredient/TagIngredient.java    |   9 +-
 .../crafting/serde/AbstractRecipeSerde.java   |   6 +-
 .../crafting/serde/RebornRecipeSerde.java     |   2 +-
 .../datagen/TechRebornDataGen.groovy          |   3 +
 .../recipes/TechRebornRecipesProvider.groovy  |   7 +
 .../recipes/machine/IngredientBuilder.groovy  |  80 +++++++
 .../machine/MachineRecipeJsonFactory.groovy   | 213 ++++++++++++++++++
 .../grinder/GrinderRecipesProvider.groovy     |  46 ++++
 14 files changed, 380 insertions(+), 150 deletions(-)
 rename RebornCore/src/main/java/reborncore/common/crafting/ingredient/{DummyIngredient.java => IngredientFactory.java} (59%)
 delete mode 100644 RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientManager.java
 create mode 100644 src/datagen/groovy/techreborn/datagen/recipes/machine/IngredientBuilder.groovy
 create mode 100644 src/datagen/groovy/techreborn/datagen/recipes/machine/MachineRecipeJsonFactory.groovy
 create mode 100644 src/datagen/groovy/techreborn/datagen/recipes/machine/grinder/GrinderRecipesProvider.groovy

diff --git a/RebornCore/src/main/java/reborncore/RebornCore.java b/RebornCore/src/main/java/reborncore/RebornCore.java
index 6604cbb14..c8cec9c7a 100644
--- a/RebornCore/src/main/java/reborncore/RebornCore.java
+++ b/RebornCore/src/main/java/reborncore/RebornCore.java
@@ -43,7 +43,6 @@ import reborncore.common.blockentity.MachineBaseBlockEntity;
 import reborncore.common.blocks.BlockWrenchEventHandler;
 import reborncore.common.chunkloading.ChunkLoaderManager;
 import reborncore.common.config.Configuration;
-import reborncore.common.crafting.ingredient.IngredientManager;
 import reborncore.common.misc.ModSounds;
 import reborncore.common.misc.RebornCoreTags;
 import reborncore.common.multiblock.MultiblockRegistry;
@@ -108,8 +107,6 @@ public class RebornCore implements ModInitializer {
 		// packets
 		ServerBoundPackets.init();
 
-		IngredientManager.setup();
-
 		RebornCoreCommands.setup();
 
 		RebornCoreTags.WATER_EXPLOSION_ITEM.toString();
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/FluidIngredient.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/FluidIngredient.java
index 24306087a..825a95419 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/FluidIngredient.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/FluidIngredient.java
@@ -56,7 +56,6 @@ public class FluidIngredient extends RebornIngredient {
 	private final Lazy<Ingredient> previewIngredient;
 
 	public FluidIngredient(Fluid fluid, Optional<List<Item>> holders, Optional<Integer> count) {
-		super(IngredientManager.FLUID_RECIPE_TYPE);
 		this.fluid = fluid;
 		this.holders = holders;
 		this.count = count;
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/DummyIngredient.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientFactory.java
similarity index 59%
rename from RebornCore/src/main/java/reborncore/common/crafting/ingredient/DummyIngredient.java
rename to RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientFactory.java
index 5de2017f5..e303b8fc9 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/DummyIngredient.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientFactory.java
@@ -24,42 +24,31 @@
 
 package reborncore.common.crafting.ingredient;
 
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import net.minecraft.item.ItemStack;
-import net.minecraft.recipe.Ingredient;
-import net.minecraft.util.Identifier;
+import com.google.gson.JsonParseException;
+import org.jetbrains.annotations.Nullable;
 
-import java.util.Collections;
-import java.util.List;
+public class IngredientFactory {
+	public static RebornIngredient deserialize(@Nullable JsonElement jsonElement) {
+		if (jsonElement == null || !jsonElement.isJsonObject()) {
+			throw new JsonParseException("ingredient must be a json object");
+		}
 
-public class DummyIngredient extends RebornIngredient {
+		final JsonObject json = jsonElement.getAsJsonObject();
 
-	public DummyIngredient() {
-		super(new Identifier("reborncore", "dummy"));
-	}
+		if (json.has("type")) {
+			throw new UnsupportedOperationException("type is unsupported");
+		}
 
-	@Override
-	public boolean test(ItemStack itemStack) {
-		return false;
-	}
-
-	@Override
-	public Ingredient getPreview() {
-		return Ingredient.EMPTY;
-	}
-
-	@Override
-	public List<ItemStack> getPreviewStacks() {
-		return Collections.emptyList();
-	}
-
-	@Override
-	protected JsonObject toJson(boolean networkSync) {
-		return new JsonObject();
-	}
-
-	@Override
-	public int getCount() {
-		return 0;
+		if (json.has("fluid")) {
+			return FluidIngredient.deserialize(json);
+		} else if (json.has("tag") || json.has("tag_server_sync")) {
+			return TagIngredient.deserialize(json);
+		} else if (json.has("item")) {
+			return StackIngredient.deserialize(json);
+		} else {
+			throw new UnsupportedOperationException("Unable to determine ingredient: " + json);
+		}
 	}
 }
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientManager.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientManager.java
deleted file mode 100644
index d406dc2a9..000000000
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/IngredientManager.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * This file is part of RebornCore, licensed under the MIT License (MIT).
- *
- * Copyright (c) 2021 TeamReborn
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package reborncore.common.crafting.ingredient;
-
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import net.minecraft.util.Identifier;
-import net.minecraft.util.JsonHelper;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.HashMap;
-import java.util.function.Function;
-
-public class IngredientManager {
-
-	public static final Identifier STACK_RECIPE_TYPE = new Identifier("reborncore", "stack");
-	public static final Identifier FLUID_RECIPE_TYPE = new Identifier("reborncore", "fluid");
-	public static final Identifier TAG_RECIPE_TYPE = new Identifier("reborncore", "tag");
-
-	private static final HashMap<Identifier, Function<JsonObject, RebornIngredient>> recipeTypes = new HashMap<>();
-
-	public static void setup() {
-		recipeTypes.put(STACK_RECIPE_TYPE, StackIngredient::deserialize);
-		recipeTypes.put(FLUID_RECIPE_TYPE, FluidIngredient::deserialize);
-		recipeTypes.put(TAG_RECIPE_TYPE, TagIngredient::deserialize);
-	}
-
-	public static RebornIngredient deserialize(@Nullable JsonElement jsonElement) {
-		if (jsonElement == null || !jsonElement.isJsonObject()) {
-			throw new JsonParseException("ingredient must be a json object");
-		}
-
-		JsonObject json = jsonElement.getAsJsonObject();
-
-		Identifier recipeTypeIdent = STACK_RECIPE_TYPE;
-		//TODO find a better way to do this.
-		if (json.has("fluid")) {
-			recipeTypeIdent = FLUID_RECIPE_TYPE;
-		} else if (json.has("tag")) {
-			recipeTypeIdent = TAG_RECIPE_TYPE;
-		}
-
-		if (json.has("type")) {
-			recipeTypeIdent = new Identifier(JsonHelper.getString(json, "type"));
-		}
-
-		Function<JsonObject, RebornIngredient> recipeTypeFunction = recipeTypes.get(recipeTypeIdent);
-		if (recipeTypeFunction == null) {
-			throw new JsonParseException("No recipe type found for " + recipeTypeIdent.toString());
-		}
-		return recipeTypeFunction.apply(json);
-	}
-}
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/RebornIngredient.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/RebornIngredient.java
index b006560bc..26c340a68 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/RebornIngredient.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/RebornIngredient.java
@@ -27,20 +27,11 @@ package reborncore.common.crafting.ingredient;
 import com.google.gson.JsonObject;
 import net.minecraft.item.ItemStack;
 import net.minecraft.recipe.Ingredient;
-import net.minecraft.util.Identifier;
 
 import java.util.List;
-import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 public abstract class RebornIngredient implements Predicate<ItemStack> {
-
-	private final Identifier ingredientType;
-
-	public RebornIngredient(Identifier ingredientType) {
-		this.ingredientType = ingredientType;
-	}
-
 	@Override
 	public abstract boolean test(ItemStack itemStack);
 
@@ -48,26 +39,7 @@ public abstract class RebornIngredient implements Predicate<ItemStack> {
 
 	public abstract List<ItemStack> getPreviewStacks();
 
-	protected abstract JsonObject toJson(boolean networkSync);
+	public abstract JsonObject toJson(boolean networkSync);
 
 	public abstract int getCount();
-
-	//Same as above but adds the type
-	public final JsonObject writeToJson(boolean networkSync) {
-		JsonObject jsonObject = toJson(networkSync);
-		jsonObject.addProperty("type", ingredientType.toString());
-		return jsonObject;
-	}
-
-	public final JsonObject writeToSyncJson() {
-		return writeToJson(true);
-	}
-
-	public <T extends RebornIngredient> void ifType(Class<T> clazz, Consumer<T> consumer) {
-		if (this.getClass().isAssignableFrom(clazz)) {
-			//noinspection unchecked
-			consumer.accept((T) this);
-		}
-	}
-
 }
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/StackIngredient.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/StackIngredient.java
index 522f56b2a..cdbc8e77b 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/StackIngredient.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/StackIngredient.java
@@ -54,7 +54,6 @@ public class StackIngredient extends RebornIngredient {
 	private final boolean requireEmptyNbt;
 
 	public StackIngredient(List<ItemStack> stacks, Optional<Integer> count, Optional<NbtCompound> nbt, boolean requireEmptyNbt) {
-		super(IngredientManager.STACK_RECIPE_TYPE);
 		this.stacks = stacks;
 		this.count = count;
 		this.nbt = nbt;
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/TagIngredient.java b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/TagIngredient.java
index 607589440..fadfa8781 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/ingredient/TagIngredient.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/ingredient/TagIngredient.java
@@ -48,7 +48,6 @@ public class TagIngredient extends RebornIngredient {
 	private final Optional<Integer> count;
 
 	public TagIngredient(Tag.Identified<Item> tag, Optional<Integer> count) {
-		super(IngredientManager.TAG_RECIPE_TYPE);
 		this.tag = tag;
 		this.count = count;
 	}
@@ -81,7 +80,7 @@ public class TagIngredient extends RebornIngredient {
 			count = Optional.of(JsonHelper.getInt(json, "count"));
 		}
 
-		if (json.has("server_sync")) {
+		if (json.has("tag_server_sync")) {
 			Identifier tagIdent = new Identifier(JsonHelper.getString(json, "tag_identifier"));
 			List<Item> items = new ArrayList<>();
 			for (int i = 0; i < JsonHelper.getInt(json, "items"); i++) {
@@ -109,13 +108,15 @@ public class TagIngredient extends RebornIngredient {
 			return toItemJsonObject();
 		}
 
-		throw new UnsupportedOperationException("TODO");
+		JsonObject jsonObject = new JsonObject();
+		jsonObject.addProperty("tag", tag.getId().toString());
+		return jsonObject;
 	}
 
 	private JsonObject toItemJsonObject() {
 		//Tags are not synced across the server so we sync all the items
 		JsonObject jsonObject = new JsonObject();
-		jsonObject.addProperty("server_sync", true);
+		jsonObject.addProperty("tag_server_sync", true);
 
 		Item[] items = tag.values().toArray(new Item[0]);
 		jsonObject.addProperty("items", items.length);
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/serde/AbstractRecipeSerde.java b/RebornCore/src/main/java/reborncore/common/crafting/serde/AbstractRecipeSerde.java
index ca524161d..67f27c922 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/serde/AbstractRecipeSerde.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/serde/AbstractRecipeSerde.java
@@ -34,7 +34,7 @@ import net.minecraft.util.JsonHelper;
 import net.minecraft.util.registry.Registry;
 import reborncore.common.crafting.RebornRecipe;
 import reborncore.common.crafting.RecipeUtils;
-import reborncore.common.crafting.ingredient.IngredientManager;
+import reborncore.common.crafting.ingredient.IngredientFactory;
 import reborncore.common.crafting.ingredient.RebornIngredient;
 import reborncore.common.util.DefaultedListCollector;
 import reborncore.common.util.serialization.SerializationUtil;
@@ -52,7 +52,7 @@ public abstract class AbstractRecipeSerde<R extends RebornRecipe> implements Rec
 
 	protected List<RebornIngredient> getIngredients(JsonObject jsonObject) {
 		return SerializationUtil.stream(JsonHelper.getArray(jsonObject, "ingredients"))
-				.map(IngredientManager::deserialize)
+				.map(IngredientFactory::deserialize)
 				.collect(DefaultedListCollector.toList());
 	}
 
@@ -63,7 +63,7 @@ public abstract class AbstractRecipeSerde<R extends RebornRecipe> implements Rec
 
 	protected void writeIngredients(R recipe, JsonObject jsonObject, boolean networkSync) {
 		final JsonArray ingredientsArray = new JsonArray();
-		recipe.getRebornIngredients().stream().map(RebornIngredient::writeToSyncJson).forEach(ingredientsArray::add);
+		recipe.getRebornIngredients().stream().map(ingredient -> ingredient.toJson(networkSync)).forEach(ingredientsArray::add);
 		jsonObject.add("ingredients", ingredientsArray);
 	}
 
diff --git a/RebornCore/src/main/java/reborncore/common/crafting/serde/RebornRecipeSerde.java b/RebornCore/src/main/java/reborncore/common/crafting/serde/RebornRecipeSerde.java
index fd7843024..08c96cf66 100644
--- a/RebornCore/src/main/java/reborncore/common/crafting/serde/RebornRecipeSerde.java
+++ b/RebornCore/src/main/java/reborncore/common/crafting/serde/RebornRecipeSerde.java
@@ -54,7 +54,7 @@ public abstract class RebornRecipeSerde<R extends RebornRecipe> extends Abstract
 	public final void toJson(R recipe, JsonObject jsonObject, boolean networkSync) {
 		writePower(recipe, jsonObject);
 		writeTime(recipe, jsonObject);
-		writeIngredients(recipe, jsonObject, true);
+		writeIngredients(recipe, jsonObject, networkSync);
 		writeOutputs(recipe, jsonObject);
 
 		collectJsonData(recipe, jsonObject, networkSync);
diff --git a/src/datagen/groovy/techreborn/datagen/TechRebornDataGen.groovy b/src/datagen/groovy/techreborn/datagen/TechRebornDataGen.groovy
index 99c4f11b5..83ee0f849 100644
--- a/src/datagen/groovy/techreborn/datagen/TechRebornDataGen.groovy
+++ b/src/datagen/groovy/techreborn/datagen/TechRebornDataGen.groovy
@@ -26,6 +26,7 @@ package techreborn.datagen
 
 import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint
 import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator
+import techreborn.datagen.recipes.machine.grinder.GrinderRecipesProvider
 import techreborn.datagen.recipes.smelting.SmeltingRecipesProvider
 import techreborn.datagen.recipes.crafting.CraftingRecipesProvider
 import techreborn.datagen.tags.TRItemTagProvider
@@ -40,5 +41,7 @@ class TechRebornDataGen implements DataGeneratorEntrypoint {
         // tags before all else, very important!!
         fabricDataGenerator.addProvider(SmeltingRecipesProvider.&new)
         fabricDataGenerator.addProvider(CraftingRecipesProvider.&new)
+
+		fabricDataGenerator.addProvider(GrinderRecipesProvider.&new)
     }
 }
diff --git a/src/datagen/groovy/techreborn/datagen/recipes/TechRebornRecipesProvider.groovy b/src/datagen/groovy/techreborn/datagen/recipes/TechRebornRecipesProvider.groovy
index 75cd024c6..e01b0acdb 100644
--- a/src/datagen/groovy/techreborn/datagen/recipes/TechRebornRecipesProvider.groovy
+++ b/src/datagen/groovy/techreborn/datagen/recipes/TechRebornRecipesProvider.groovy
@@ -32,6 +32,9 @@ import net.minecraft.item.ItemConvertible
 import net.minecraft.recipe.Ingredient
 import net.minecraft.tag.Tag
 import net.minecraft.util.Identifier
+import techreborn.datagen.recipes.machine.MachineRecipeJsonFactory
+
+import techreborn.init.ModRecipes
 
 import java.util.function.Consumer
 
@@ -120,6 +123,10 @@ abstract class TechRebornRecipesProvider extends FabricRecipesProvider {
 		throw new IllegalArgumentException()
 	}
 
+	def offerGrinderRecipe(@DelegatesTo(value = MachineRecipeJsonFactory.class, strategy = Closure.DELEGATE_FIRST) Closure closure) {
+		MachineRecipeJsonFactory.create(ModRecipes.GRINDER, closure).offerTo(exporter)
+	}
+
     @Override
     protected Identifier getRecipeIdentifier(Identifier identifier) {
         return new Identifier("techreborn", super.getRecipeIdentifier(identifier).path)
diff --git a/src/datagen/groovy/techreborn/datagen/recipes/machine/IngredientBuilder.groovy b/src/datagen/groovy/techreborn/datagen/recipes/machine/IngredientBuilder.groovy
new file mode 100644
index 000000000..20159c902
--- /dev/null
+++ b/src/datagen/groovy/techreborn/datagen/recipes/machine/IngredientBuilder.groovy
@@ -0,0 +1,80 @@
+/*
+ * This file is part of TechReborn, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2020 TechReborn
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package techreborn.datagen.recipes.machine
+
+import net.minecraft.item.Item
+import net.minecraft.item.ItemConvertible
+import net.minecraft.item.ItemStack
+import net.minecraft.tag.Tag
+import reborncore.common.crafting.ingredient.RebornIngredient
+import reborncore.common.crafting.ingredient.StackIngredient
+import reborncore.common.crafting.ingredient.TagIngredient
+
+class IngredientBuilder {
+	private Tag.Identified<Item> tag
+	private List<ItemStack> stacks = new ArrayList<>()
+
+	private IngredientBuilder() {
+	}
+
+	static IngredientBuilder create() {
+		return new IngredientBuilder()
+	}
+
+	RebornIngredient build() {
+		if (tag != null) {
+			if (!stacks.isEmpty()) {
+				throw new IllegalStateException("Cannot have ingredient with tag and stack inputs")
+			}
+
+			return new TagIngredient(tag, getCount())
+		}
+
+		if (!stacks.isEmpty()) {
+			return new StackIngredient(stacks, getCount(), Optional.empty(), false)
+		}
+
+		throw new IllegalStateException()
+	}
+
+	def tag(Tag.Identified<Item> tag) {
+		this.tag = tag
+		return this
+	}
+
+	def item(ItemConvertible itemConvertible) {
+		return stack(new ItemStack(itemConvertible.asItem()))
+	}
+
+	def stack(ItemStack itemStack) {
+		stacks.add(itemStack)
+		return this
+	}
+
+
+	private Optional<Integer> getCount() {
+		return Optional.empty()
+	}
+}
diff --git a/src/datagen/groovy/techreborn/datagen/recipes/machine/MachineRecipeJsonFactory.groovy b/src/datagen/groovy/techreborn/datagen/recipes/machine/MachineRecipeJsonFactory.groovy
new file mode 100644
index 000000000..14f9a0f50
--- /dev/null
+++ b/src/datagen/groovy/techreborn/datagen/recipes/machine/MachineRecipeJsonFactory.groovy
@@ -0,0 +1,213 @@
+/*
+ * This file is part of TechReborn, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2020 TechReborn
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package techreborn.datagen.recipes.machine
+
+import com.google.gson.JsonObject
+import net.minecraft.data.server.recipe.RecipeJsonProvider
+import net.minecraft.item.ItemConvertible
+import net.minecraft.item.ItemStack
+import net.minecraft.recipe.RecipeSerializer
+import net.minecraft.tag.Tag
+import net.minecraft.util.Identifier
+import net.minecraft.util.registry.Registry
+import reborncore.common.crafting.RebornRecipe
+import reborncore.common.crafting.RebornRecipeType
+import reborncore.common.crafting.ingredient.RebornIngredient
+
+import java.util.function.Consumer
+
+class MachineRecipeJsonFactory<R extends RebornRecipe> {
+	private final RebornRecipeType<R> type
+
+	private final List<RebornIngredient> ingredients = new ArrayList<>()
+	private final List<ItemStack> outputs = new ArrayList<>()
+	private int power = -1
+	private int time = -1
+	private Identifier customId = null
+
+	protected MachineRecipeJsonFactory(RebornRecipeType<R> type) {
+		this.type = type
+	}
+
+	static <R extends RebornRecipe> MachineRecipeJsonFactory<R> create(RebornRecipeType<R> type) {
+		return new MachineRecipeJsonFactory<R>(type)
+	}
+
+	static <R extends RebornRecipe> MachineRecipeJsonFactory<R> create(RebornRecipeType<R> type, @DelegatesTo(value = MachineRecipeJsonFactory.class, strategy = Closure.DELEGATE_FIRST) Closure closure) {
+		def factory = new MachineRecipeJsonFactory<R>(type)
+		closure.setDelegate(factory)
+		closure.call(factory)
+		return factory
+	}
+
+	def ingredients(Object... objects) {
+		for (object in objects) {
+			if (object instanceof ItemConvertible) {
+				ingredient {
+					item object
+				}
+			} else if (object instanceof Tag.Identified) {
+				ingredient {
+					tag object
+				}
+			} else {
+				throw new UnsupportedOperationException()
+			}
+		}
+
+		return this
+	}
+
+	def ingredient(@DelegatesTo(value = IngredientBuilder.class, strategy = Closure.DELEGATE_FIRST) Closure closure) {
+		def builder = IngredientBuilder.create()
+		closure.setDelegate(builder)
+		closure.call(builder)
+		ingredients.add(builder.build())
+		return this
+	}
+
+	def outputs(Object... objects) {
+		for (object in objects) {
+			if (object instanceof ItemStack) {
+				output(object)
+			} else if (object instanceof ItemConvertible) {
+				output(new ItemStack(object.asItem()))
+			}
+		}
+
+		return this
+	}
+
+	def output(ItemStack stack) {
+		outputs.add(stack)
+		return this
+	}
+
+	def power(int power) {
+		this.power = power
+		return this
+	}
+
+	def time(int time) {
+		this.time = time
+		return this
+	}
+
+	def id(Identifier identifier) {
+		this.customId = identifier
+		return this
+	}
+
+	MachineRecipeJsonFactory id(String path) {
+		return id(new Identifier("techreborn", path))
+	}
+
+	/**
+	 * Override this method to support custom recipe types.
+	 */
+	protected R createRecipe(Identifier identifier) {
+		return new RebornRecipe(type, identifier, ingredients, outputs, power, time) as R
+	}
+
+	protected void validate() {
+		if (ingredients.isEmpty()) {
+			throw new IllegalStateException("recipe has no ingrendients")
+		}
+
+		if (outputs.isEmpty()) {
+			throw new IllegalStateException("recipe has no outputs")
+		}
+
+		if (power < 0) {
+			throw new IllegalStateException("recipe has no power value")
+		}
+
+		if (time < 0) {
+			throw new IllegalStateException("recipe has no time value")
+		}
+	}
+
+	void offerTo(Consumer<RecipeJsonProvider> exporter) {
+		validate()
+		exporter.accept(new MachineRecipeJsonProvider<R>(type, createRecipe(getIdentifier())))
+	}
+
+	def getIdentifier() {
+		if (customId) {
+			return customId
+		}
+
+		if (outputs.size() < 1) {
+			throw new IllegalStateException("Recipe has no outputs")
+		}
+
+		if (outputs.size() > 1) {
+			throw new IllegalStateException("Cannot compute default identifier for a recipe with more than one output. TODO might want to improve this?")
+		}
+
+		def outputId = Registry.ITEM.getId(outputs[0].item)
+		return new Identifier("techreborn", "${type.name().path}/${outputId.path}")
+	}
+
+	static class MachineRecipeJsonProvider<R extends RebornRecipe> implements RecipeJsonProvider {
+		private final RebornRecipeType<R> type
+		private final R recipe
+
+		MachineRecipeJsonProvider(RebornRecipeType<R> type, R recipe) {
+			this.type = type
+			this.recipe = recipe
+		}
+
+		@Override
+		JsonObject toJson() {
+			return type.toJson(recipe, false)
+		}
+
+		@Override
+		Identifier getRecipeId() {
+			return recipe.id
+		}
+
+		@Override
+		void serialize(JsonObject json) {
+			throw new UnsupportedOperationException()
+		}
+
+		@Override
+		RecipeSerializer<?> getSerializer() {
+			throw new UnsupportedOperationException()
+		}
+
+		@Override
+		JsonObject toAdvancementJson() {
+			return null
+		}
+
+		@Override
+		Identifier getAdvancementId() {
+			return null
+		}
+	}
+}
diff --git a/src/datagen/groovy/techreborn/datagen/recipes/machine/grinder/GrinderRecipesProvider.groovy b/src/datagen/groovy/techreborn/datagen/recipes/machine/grinder/GrinderRecipesProvider.groovy
new file mode 100644
index 000000000..9d48d6a6a
--- /dev/null
+++ b/src/datagen/groovy/techreborn/datagen/recipes/machine/grinder/GrinderRecipesProvider.groovy
@@ -0,0 +1,46 @@
+/*
+ * This file is part of TechReborn, licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2020 TechReborn
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package techreborn.datagen.recipes.machine.grinder
+
+import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator
+import net.minecraft.item.Items
+import techreborn.datagen.recipes.TechRebornRecipesProvider
+import techreborn.init.TRContent
+
+class GrinderRecipesProvider extends TechRebornRecipesProvider {
+	GrinderRecipesProvider(FabricDataGenerator dataGenerator) {
+		super(dataGenerator)
+	}
+
+	@Override
+	void generateRecipes() {
+//		offerGrinderRecipe {
+//			ingredients TRContent.ORES_TAG, Items.ACACIA_BOAT
+//			outputs Items.DIAMOND
+//			power 5
+//			time 200
+//		}
+	}
+}
\ No newline at end of file