diff --git a/src/generated/resources/.cache/cache b/src/generated/resources/.cache/cache
index a10dfbfd4..f79cecdda 100644
--- a/src/generated/resources/.cache/cache
+++ b/src/generated/resources/.cache/cache
@@ -18,7 +18,7 @@ a92c395c57209141d4cf78ad62facbaf3878a120 assets/create/blockstates/andesite_cobb
 11908c2f8603e61bec88010bc6d0890e6339c6b1 assets/create/blockstates/andesite_funnel.json
 398922758a6219544e5b85c91c9cf8a543b437e5 assets/create/blockstates/andesite_pillar.json
 1d2d8081581e07d9be4b382aede4f2de4401cc6b assets/create/blockstates/andesite_tunnel.json
-f9fa6aa530eb0891a74eadfbebc663172a57147a assets/create/blockstates/basin.json
+e555e3c2b2d3f01440e48db4ba88f7e00fd99b6f assets/create/blockstates/basin.json
 f25693a9429f6337149ff24f27900dc4eb82a7c2 assets/create/blockstates/belt.json
 cf9045eb16e5299a1d917c4cb536289f49411276 assets/create/blockstates/birch_window.json
 94a1a91403eb4b035fec48071e7fcae57a8a6abd assets/create/blockstates/birch_window_pane.json
@@ -1086,9 +1086,15 @@ b0f664dd6de3d0ee9afcb6223fbcd53b97fa0d65 assets/create/models/item/andesite_cobb
 8861d656eb6f61320b4d80eb079105705c762079 assets/create/models/item/andesite_cobblestone_wall.json
 7490819e7e5445019b6b8cb2538f12a5b6717a46 assets/create/models/item/andesite_funnel.json
 75b8b00c2418b9660d35a7fabd0774925cf1c02f assets/create/models/item/andesite_pillar.json
+<<<<<<< HEAD
 795541cf7205d90531a23cd5b388f93a03bbf925 assets/create/models/item/andesite_tunnel.json
 bf1fc6bdf7fca6f1958a2d3e96202c1cecb50669 assets/create/models/item/basin.json
 ffdb36349fa2e50451c208585162b6a8386ec494 assets/create/models/item/belt_connector.json
+=======
+c0e35daccfb398947532e9499d6bda963387cd9c assets/create/models/item/andesite_tunnel.json
+421e481b7fbca4c4a1080ed703401eb25375e087 assets/create/models/item/basin.json
+1da382e7e58eaa9788f5b1d92221ccac573e068f assets/create/models/item/belt_connector.json
+>>>>>>> mc1.15/dev
 9044243882cfd49a2827e1b910a4c9b0e46daa47 assets/create/models/item/birch_window.json
 d537fffaede521efa525fb2a7f9863fe6a80054e assets/create/models/item/birch_window_pane.json
 22632bd681c8a605f0845f7549770389a741156a assets/create/models/item/black_seat.json
@@ -1523,7 +1529,7 @@ d531f87f425d199aee4777a588c1cd6cab6f5173 data/create/advancements/recipes/create
 2eef3201017af03f6a2f0f015645e3ff5e25d9c1 data/create/advancements/recipes/create.base/crafting/curiosities/wand_of_symmetry.json
 d97d96e1b2ddd25df15fe1ef1c3d084f15bb9789 data/create/advancements/recipes/create.base/crafting/kinetics/adjustable_pulley.json
 92416ced6ede6965fd728e1c7336bb05a3e41ea2 data/create/advancements/recipes/create.base/crafting/kinetics/analog_lever.json
-2105b4f1fd9a170a100efc083a794fdb9e068924 data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json
+3e9753006da898d4569bbeabf95997e8c90847c8 data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json
 bec8c280b717306f87050b08a418feab53be71cb data/create/advancements/recipes/create.base/crafting/kinetics/basin.json
 5af08853632fb5970fe542b3ecbde0ad16d64714 data/create/advancements/recipes/create.base/crafting/kinetics/belt_connector.json
 80d87f1dde60adb5334e0cff25a9f0b7f67c1526 data/create/advancements/recipes/create.base/crafting/kinetics/black_seat.json
@@ -2470,6 +2476,8 @@ d9021504be855cd2d4d91503a82b84233052adb0 data/create/recipes/blasting/gold_ingot
 c323b106e88b7de77fea71ff12494abdbb818d15 data/create/recipes/chiseled_limestone_from_limestone_stonecutting.json
 da9a919b476954c1de34826aa7706bf6056a8f12 data/create/recipes/chiseled_scoria_from_scoria_stonecutting.json
 09faa4ddcf9f3907dcdb3ab3e8b68c1deb2486e5 data/create/recipes/chiseled_weathered_limestone_from_weathered_limestone_stonecutting.json
+c3cfdc9552a23e4749c42e71fbddd153b76ca708 data/create/recipes/compacting/ice.json
+5758a1804287c261e1c953bda599d8495ba7c40a data/create/recipes/compacting/temp_gabbro.json
 19526da3a59fc136654ff1bc93c0251581f397a9 data/create/recipes/crafting/appliances/dough.json
 7b5f863dda3d05a79cb85943a178eba0bd8a7dc7 data/create/recipes/crafting/appliances/slime_ball.json
 b159ba84428eee6ef6e23df1766f2a18f2c8a63e data/create/recipes/crafting/appliances/tree_fertilizer.json
@@ -2479,7 +2487,7 @@ b159ba84428eee6ef6e23df1766f2a18f2c8a63e data/create/recipes/crafting/appliances
 fcbc04d0a7eaf820a74bc7e4736a4a581e0a9dff data/create/recipes/crafting/curiosities/wand_of_symmetry.json
 696df0fe5f8e29220ea15527f8c119c39b418819 data/create/recipes/crafting/kinetics/adjustable_pulley.json
 88de51b451469698665b7319e5b9cfb9a87ae3e0 data/create/recipes/crafting/kinetics/analog_lever.json
-6912101930aae627820783c27358dcf2ff4016aa data/create/recipes/crafting/kinetics/attribute_filter.json
+cf1f3a6306d47025cebe153cf05949ef69ccbe5a data/create/recipes/crafting/kinetics/attribute_filter.json
 059d12526529b2896ed583555373afa31839a0de data/create/recipes/crafting/kinetics/basin.json
 dcf98e667d321fb4bd9fa6dfec7927a84cdbd5d6 data/create/recipes/crafting/kinetics/belt_connector.json
 1123903a11b13448b61cf8f8a5dc2e8013d39ac0 data/create/recipes/crafting/kinetics/black_seat.json
@@ -2717,6 +2725,7 @@ ddda28bb6efc43b7e3149756daf53e1664187283 data/create/recipes/dolomite_cobbleston
 500ecdfdcf34e9d26256948e206aab4f0b79e659 data/create/recipes/dolomite_cobblestone_wall_from_dolomite_cobblestone_stonecutting.json
 ff39e629b242ae91e23aec86b0a1f757dd938305 data/create/recipes/dolomite_pillar.json
 b4a8d14d9a20e812e0acb691b5b511a87e8b0576 data/create/recipes/dolomite_pillar_from_dolomite_stonecutting.json
+ae6698363e49f7cb5f2ed52c6b8805bebed31fa2 data/create/recipes/emptying/water_bottle.json
 0e11aa1accb71ed62e212f23a7069b7b7b4e8119 data/create/recipes/fancy_andesite_bricks_from_andesite_stonecutting.json
 8b86fc9a9416adeaab3f26192a73a481887675c3 data/create/recipes/fancy_andesite_bricks_slab.json
 c7b762b25c7a6705dba3e922e981be851ac4f36b data/create/recipes/fancy_andesite_bricks_slab_from_fancy_andesite_bricks_stonecutting.json
@@ -2902,6 +2911,7 @@ e7bfaa806d57573d060fac0a5e7a84f345b8bb85 data/create/recipes/mixing/andesite_all
 76939e4d3e5b615ae21d14c0ff7b917a622bcf80 data/create/recipes/mixing/chromatic_compound.json
 d9a3dff1288d675ab812eef1eb73cb27dcc71bd2 data/create/recipes/mixing/crushed_brass.json
 00b165ea38d834c7955440e87062004a8182c3f8 data/create/recipes/mixing/gunpowder.json
+35c4e8a765479861f307afb9ec650f912f92b998 data/create/recipes/mixing/temp_cobble.json
 1998c6f84f871d6da58ec29d729401d18f8f1aa1 data/create/recipes/mossy_andesite_from_andesite_stonecutting.json
 89929d9cb11b5c589b2ecfa821c61add1ef7b62b data/create/recipes/mossy_dark_scoria_from_dark_scoria_stonecutting.json
 4b8b1191dd3a21294293dc5ad237af89b849df28 data/create/recipes/mossy_diorite_from_diorite_stonecutting.json
diff --git a/src/generated/resources/assets/create/blockstates/basin.json b/src/generated/resources/assets/create/blockstates/basin.json
index 2bc6fd9d1..0e511666e 100644
--- a/src/generated/resources/assets/create/blockstates/basin.json
+++ b/src/generated/resources/assets/create/blockstates/basin.json
@@ -1,7 +1,22 @@
 {
   "variants": {
-    "": {
-      "model": "create:block/basin"
+    "facing=down": {
+      "model": "create:block/basin/block"
+    },
+    "facing=north": {
+      "model": "create:block/basin/block_directional",
+      "y": 180
+    },
+    "facing=south": {
+      "model": "create:block/basin/block_directional"
+    },
+    "facing=west": {
+      "model": "create:block/basin/block_directional",
+      "y": 90
+    },
+    "facing=east": {
+      "model": "create:block/basin/block_directional",
+      "y": 270
     }
   }
 }
\ No newline at end of file
diff --git a/src/generated/resources/assets/create/models/item/basin.json b/src/generated/resources/assets/create/models/item/basin.json
index 1dc14b2e8..a1ed06ff7 100644
--- a/src/generated/resources/assets/create/models/item/basin.json
+++ b/src/generated/resources/assets/create/models/item/basin.json
@@ -1,3 +1,3 @@
 {
-  "parent": "create:block/basin"
+  "parent": "create:block/basin/block"
 }
\ No newline at end of file
diff --git a/src/generated/resources/data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json b/src/generated/resources/data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json
index 5b1c63f80..e41c26b07 100644
--- a/src/generated/resources/data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json
+++ b/src/generated/resources/data/create/advancements/recipes/create.base/crafting/kinetics/attribute_filter.json
@@ -11,7 +11,7 @@
       "conditions": {
         "items": [
           {
-            "item": "create:andesite_alloy"
+            "tag": "forge:ingots/brass"
           }
         ]
       }
diff --git a/src/generated/resources/data/create/recipes/compacting/ice.json b/src/generated/resources/data/create/recipes/compacting/ice.json
new file mode 100644
index 000000000..98ae992e9
--- /dev/null
+++ b/src/generated/resources/data/create/recipes/compacting/ice.json
@@ -0,0 +1,14 @@
+{
+  "type": "create:compacting",
+  "ingredients": [
+    {
+      "item": "minecraft:ice"
+    }
+  ],
+  "results": [
+    {
+      "fluid": "minecraft:water",
+      "amount": 250
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/create/recipes/compacting/temp_gabbro.json b/src/generated/resources/data/create/recipes/compacting/temp_gabbro.json
new file mode 100644
index 000000000..c5474f6f5
--- /dev/null
+++ b/src/generated/resources/data/create/recipes/compacting/temp_gabbro.json
@@ -0,0 +1,18 @@
+{
+  "type": "create:compacting",
+  "ingredients": [
+    {
+      "item": "minecraft:cobblestone"
+    },
+    {
+      "fluidTag": "minecraft:lava",
+      "amount": 250
+    }
+  ],
+  "results": [
+    {
+      "item": "create:gabbro",
+      "count": 1
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/create/recipes/crafting/kinetics/attribute_filter.json b/src/generated/resources/data/create/recipes/crafting/kinetics/attribute_filter.json
index 15bd0d895..5e1cfd2cc 100644
--- a/src/generated/resources/data/create/recipes/crafting/kinetics/attribute_filter.json
+++ b/src/generated/resources/data/create/recipes/crafting/kinetics/attribute_filter.json
@@ -8,7 +8,7 @@
       "tag": "minecraft:wool"
     },
     "A": {
-      "tag": "forge:nuggets/copper"
+      "tag": "forge:nuggets/brass"
     }
   },
   "result": {
diff --git a/src/generated/resources/data/create/recipes/emptying/water_bottle.json b/src/generated/resources/data/create/recipes/emptying/water_bottle.json
new file mode 100644
index 000000000..1a34fd570
--- /dev/null
+++ b/src/generated/resources/data/create/recipes/emptying/water_bottle.json
@@ -0,0 +1,18 @@
+{
+  "type": "create:emptying",
+  "ingredients": [
+    {
+      "item": "minecraft:potion"
+    }
+  ],
+  "results": [
+    {
+      "item": "minecraft:glass_bottle",
+      "count": 1
+    },
+    {
+      "fluid": "minecraft:water",
+      "amount": 250
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/generated/resources/data/create/recipes/mixing/temp_cobble.json b/src/generated/resources/data/create/recipes/mixing/temp_cobble.json
new file mode 100644
index 000000000..f20d0e6cf
--- /dev/null
+++ b/src/generated/resources/data/create/recipes/mixing/temp_cobble.json
@@ -0,0 +1,19 @@
+{
+  "type": "create:mixing",
+  "ingredients": [
+    {
+      "fluidTag": "minecraft:water",
+      "amount": 250
+    },
+    {
+      "fluidTag": "minecraft:lava",
+      "amount": 250
+    }
+  ],
+  "results": [
+    {
+      "item": "minecraft:cobblestone",
+      "count": 1
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/main/java/com/simibubi/create/AllBlocks.java b/src/main/java/com/simibubi/create/AllBlocks.java
index b83263266..25951383a 100644
--- a/src/main/java/com/simibubi/create/AllBlocks.java
+++ b/src/main/java/com/simibubi/create/AllBlocks.java
@@ -73,6 +73,7 @@ import com.simibubi.create.content.contraptions.fluids.tank.FluidTankGenerator;
 import com.simibubi.create.content.contraptions.fluids.tank.FluidTankItem;
 import com.simibubi.create.content.contraptions.fluids.tank.FluidTankModel;
 import com.simibubi.create.content.contraptions.processing.BasinBlock;
+import com.simibubi.create.content.contraptions.processing.BasinGenerator;
 import com.simibubi.create.content.contraptions.processing.BasinMovementBehaviour;
 import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock;
 import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlockItem;
@@ -409,9 +410,10 @@ public class AllBlocks {
 
 	public static final BlockEntry<BasinBlock> BASIN = REGISTRATE.block("basin", BasinBlock::new)
 		.initialProperties(SharedProperties::stone)
-		.blockstate((ctx, prov) -> prov.simpleBlock(ctx.getEntry(), AssetLookup.standardModel(ctx, prov)))
+		.blockstate(new BasinGenerator()::generate)
 		.onRegister(addMovementBehaviour(new BasinMovementBehaviour()))
-		.simpleItem()
+		.item()
+		.transform(customItemModel("_", "block"))
 		.register();
 
 	public static final BlockEntry<BlazeBurnerBlock> BLAZE_BURNER =
diff --git a/src/main/java/com/simibubi/create/AllRecipeTypes.java b/src/main/java/com/simibubi/create/AllRecipeTypes.java
index 2e2e6515e..908e39e87 100644
--- a/src/main/java/com/simibubi/create/AllRecipeTypes.java
+++ b/src/main/java/com/simibubi/create/AllRecipeTypes.java
@@ -1,5 +1,6 @@
 package com.simibubi.create;
 
+import java.util.Optional;
 import java.util.function.Supplier;
 
 import com.simibubi.create.compat.jei.ConversionRecipe;
@@ -7,10 +8,13 @@ import com.simibubi.create.content.contraptions.components.crafter.MechanicalCra
 import com.simibubi.create.content.contraptions.components.crusher.CrushingRecipe;
 import com.simibubi.create.content.contraptions.components.fan.SplashingRecipe;
 import com.simibubi.create.content.contraptions.components.millstone.MillingRecipe;
+import com.simibubi.create.content.contraptions.components.mixer.CompactingRecipe;
 import com.simibubi.create.content.contraptions.components.mixer.MixingRecipe;
 import com.simibubi.create.content.contraptions.components.press.PressingRecipe;
 import com.simibubi.create.content.contraptions.components.saw.CuttingRecipe;
 import com.simibubi.create.content.contraptions.fluids.actors.FillingRecipe;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
+import com.simibubi.create.content.contraptions.processing.EmptyingRecipe;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipe;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeFactory;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipeSerializer;
@@ -25,22 +29,26 @@ import net.minecraft.item.crafting.IRecipeType;
 import net.minecraft.item.crafting.ShapedRecipe;
 import net.minecraft.util.ResourceLocation;
 import net.minecraft.util.registry.Registry;
+import net.minecraft.world.World;
 import net.minecraftforge.event.RegistryEvent;
 
 public enum AllRecipeTypes {
 
 	BLOCKZAPPER_UPGRADE(BlockzapperUpgradeRecipe.Serializer::new, IRecipeType.CRAFTING),
 	MECHANICAL_CRAFTING(MechanicalCraftingRecipe.Serializer::new),
-	
+
 	CONVERSION(processingSerializer(ConversionRecipe::new)),
 	CRUSHING(processingSerializer(CrushingRecipe::new)),
 	CUTTING(processingSerializer(CuttingRecipe::new)),
 	MILLING(processingSerializer(MillingRecipe::new)),
+	BASIN(processingSerializer(BasinRecipe::new)),
 	MIXING(processingSerializer(MixingRecipe::new)),
+	COMPACTING(processingSerializer(CompactingRecipe::new)),
 	PRESSING(processingSerializer(PressingRecipe::new)),
 	SANDPAPER_POLISHING(processingSerializer(SandPaperPolishingRecipe::new)),
 	SPLASHING(processingSerializer(SplashingRecipe::new)),
 	FILLING(processingSerializer(FillingRecipe::new)),
+	EMPTYING(processingSerializer(EmptyingRecipe::new)),
 
 	;
 
@@ -89,4 +97,9 @@ public enum AllRecipeTypes {
 	public <T extends IRecipeType<?>> T getType() {
 		return (T) type;
 	}
+
+	public <C extends IInventory, T extends IRecipe<C>> Optional<T> find(C inv, World world) {
+		return world.getRecipeManager()
+			.getRecipe(getType(), inv, world);
+	}
 }
diff --git a/src/main/java/com/simibubi/create/compat/jei/CreateJEI.java b/src/main/java/com/simibubi/create/compat/jei/CreateJEI.java
index 1495cbf79..d9055bdbd 100644
--- a/src/main/java/com/simibubi/create/compat/jei/CreateJEI.java
+++ b/src/main/java/com/simibubi/create/compat/jei/CreateJEI.java
@@ -23,10 +23,12 @@ import com.simibubi.create.compat.jei.category.PressingCategory;
 import com.simibubi.create.compat.jei.category.SawingCategory;
 import com.simibubi.create.compat.jei.category.SmokingViaFanCategory;
 import com.simibubi.create.compat.jei.category.SplashingCategory;
-import com.simibubi.create.content.contraptions.components.mixer.MixingRecipe;
 import com.simibubi.create.content.contraptions.components.press.MechanicalPressTileEntity;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
 import com.simibubi.create.content.logistics.block.inventories.AdjustableCrateScreen;
 import com.simibubi.create.content.schematics.block.SchematicannonScreen;
+import com.simibubi.create.foundation.config.AllConfigs;
+import com.simibubi.create.foundation.config.CRecipes;
 import com.simibubi.create.foundation.utility.Lang;
 
 import mezz.jei.api.IModPlugin;
@@ -95,44 +97,62 @@ public class CreateJEI implements IModPlugin {
 
 	@Override
 	public void registerCategories(IRecipeCategoryRegistration registration) {
-		registration
-				.addRecipeCategories(millingCategory, crushingCategory, splashingCategory, pressingCategory,
-						smokingCategory, blastingCategory, blockzapperCategory, mixingCategory, sawingCategory,
-						blockCuttingCategory, packingCategory, polishingCategory, mysteryConversionCategory,
-						mechanicalCraftingCategory);
+		registration.addRecipeCategories(millingCategory, crushingCategory, splashingCategory, pressingCategory,
+			smokingCategory, blastingCategory, blockzapperCategory, mixingCategory, sawingCategory,
+			blockCuttingCategory, packingCategory, polishingCategory, mysteryConversionCategory,
+			mechanicalCraftingCategory);
 	}
 
 	@Override
 	public void registerRecipes(IRecipeRegistration registration) {
+		CRecipes recipeConfig = AllConfigs.SERVER.recipes;
+
 		registration.addRecipes(findRecipes(AllRecipeTypes.MILLING), millingCategory.getUid());
 		registration.addRecipes(findRecipes(AllRecipeTypes.CRUSHING), crushingCategory.getUid());
-		registration.addRecipes(findRecipesByTypeExcluding(AllRecipeTypes.MILLING.getType(), AllRecipeTypes.CRUSHING.getType()),
-				crushingCategory.getUid());
+		registration.addRecipes(
+			findRecipesByTypeExcluding(AllRecipeTypes.MILLING.getType(), AllRecipeTypes.CRUSHING.getType()),
+			crushingCategory.getUid());
 		registration.addRecipes(findRecipes(AllRecipeTypes.SPLASHING), splashingCategory.getUid());
 		registration.addRecipes(findRecipes(AllRecipeTypes.PRESSING), pressingCategory.getUid());
 		registration.addRecipes(findRecipesById(AllRecipeTypes.BLOCKZAPPER_UPGRADE.serializer.getRegistryName()),
-				blockzapperCategory.getUid());
+			blockzapperCategory.getUid());
 		registration.addRecipes(findRecipesByType(IRecipeType.SMOKING), smokingCategory.getUid());
 		registration.addRecipes(findRecipesByTypeExcluding(IRecipeType.SMELTING, IRecipeType.SMOKING),
-				blastingCategory.getUid());
+			blastingCategory.getUid());
 		registration.addRecipes(findRecipes(AllRecipeTypes.MIXING), mixingCategory.getUid());
-		registration.addRecipes(findRecipes(r -> r.getSerializer() == IRecipeSerializer.CRAFTING_SHAPELESS
-				&& !MechanicalPressTileEntity.canCompress(r.getIngredients())).stream().map(MixingRecipe::convertShapeless)
-						.collect(Collectors.toList()),
+
+		if (recipeConfig.allowShapelessInMixer.get())
+			registration.addRecipes(findRecipes(r -> r.getSerializer() == IRecipeSerializer.CRAFTING_SHAPELESS
+				&& !MechanicalPressTileEntity.canCompress(r.getIngredients())).stream()
+					.map(BasinRecipe::convert)
+					.collect(Collectors.toList()),
 				mixingCategory.getUid());
+
 		registration.addRecipes(findRecipes(AllRecipeTypes.CUTTING), sawingCategory.getUid());
-		registration.addRecipes(
+
+		if (recipeConfig.allowStonecuttingOnSaw.get())
+			registration.addRecipes(
 				CondensedBlockCuttingRecipe.condenseRecipes(findRecipesByType(IRecipeType.STONECUTTING)),
 				blockCuttingCategory.getUid());
-		registration.addRecipes(findRecipes(
-				r -> (r instanceof ICraftingRecipe) && MechanicalPressTileEntity.canCompress(r.getIngredients())),
+
+		registration.addRecipes(findRecipes(AllRecipeTypes.COMPACTING), packingCategory.getUid());
+		if (recipeConfig.allowShapedSquareInPress.get())
+			registration.addRecipes(findRecipes(
+				r -> (r instanceof ICraftingRecipe) && MechanicalPressTileEntity.canCompress(r.getIngredients()))
+					.stream()
+					.map(BasinRecipe::convert)
+					.collect(Collectors.toList()),
 				packingCategory.getUid());
+
 		registration.addRecipes(findRecipes(AllRecipeTypes.SANDPAPER_POLISHING), polishingCategory.getUid());
 		registration.addRecipes(MysteriousItemConversionCategory.getRecipes(), mysteryConversionCategory.getUid());
 		registration.addRecipes(findRecipes(r -> (r.getType() == AllRecipeTypes.MECHANICAL_CRAFTING.type)),
-				mechanicalCraftingCategory.getUid());
-		registration.addRecipes(findRecipes(r -> (r.getType() == IRecipeType.CRAFTING 
-				&& r.getType() != AllRecipeTypes.MECHANICAL_CRAFTING.type) && (r instanceof ShapedRecipe)),
+			mechanicalCraftingCategory.getUid());
+
+		if (recipeConfig.allowRegularCraftingInCrafter.get())
+			registration.addRecipes(findRecipes(
+				r -> (r.getType() == IRecipeType.CRAFTING && r.getType() != AllRecipeTypes.MECHANICAL_CRAFTING.type)
+					&& (r instanceof ShapedRecipe)),
 				mechanicalCraftingCategory.getUid());
 	}
 
@@ -167,9 +187,8 @@ public class CreateJEI implements IModPlugin {
 		registration.addRecipeCatalyst(new ItemStack(AllBlocks.BASIN.get()), packingCategory.getUid());
 		registration.addRecipeCatalyst(AllItems.SAND_PAPER.asStack(), polishingCategory.getUid());
 		registration.addRecipeCatalyst(AllItems.RED_SAND_PAPER.asStack(), polishingCategory.getUid());
-		registration
-				.addRecipeCatalyst(new ItemStack(AllBlocks.MECHANICAL_CRAFTER.get()),
-						mechanicalCraftingCategory.getUid());
+		registration.addRecipeCatalyst(new ItemStack(AllBlocks.MECHANICAL_CRAFTER.get()),
+			mechanicalCraftingCategory.getUid());
 	}
 
 	@Override
@@ -183,30 +202,29 @@ public class CreateJEI implements IModPlugin {
 	}
 
 	private static List<IRecipe<?>> findRecipes(Predicate<IRecipe<?>> pred) {
-		return Minecraft.getInstance().world
-				.getRecipeManager()
-				.getRecipes()
-				.stream()
-				.filter(pred)
-				.collect(Collectors.toList());
+		return Minecraft.getInstance().world.getRecipeManager()
+			.getRecipes()
+			.stream()
+			.filter(pred)
+			.collect(Collectors.toList());
 	}
 
 	private static List<IRecipe<?>> findRecipesByType(IRecipeType<?> type) {
-		return Minecraft.getInstance().world
-				.getRecipeManager()
-				.getRecipes()
-				.stream()
-				.filter(r -> r.getType() == type)
-				.collect(Collectors.toList());
+		return Minecraft.getInstance().world.getRecipeManager()
+			.getRecipes()
+			.stream()
+			.filter(r -> r.getType() == type)
+			.collect(Collectors.toList());
 	}
 
 	private static List<IRecipe<?>> findRecipesById(ResourceLocation id) {
-		return Minecraft.getInstance().world
-				.getRecipeManager()
-				.getRecipes()
-				.stream()
-				.filter(r -> r.getSerializer().getRegistryName().equals(id))
-				.collect(Collectors.toList());
+		return Minecraft.getInstance().world.getRecipeManager()
+			.getRecipes()
+			.stream()
+			.filter(r -> r.getSerializer()
+				.getRegistryName()
+				.equals(id))
+			.collect(Collectors.toList());
 	}
 
 	private static List<IRecipe<?>> findRecipesByTypeExcluding(IRecipeType<?> type, IRecipeType<?> excludingType) {
@@ -214,10 +232,14 @@ public class CreateJEI implements IModPlugin {
 		List<IRecipe<?>> byExcludingType = findRecipesByType(excludingType);
 		byType.removeIf(recipe -> {
 			for (IRecipe<?> r : byExcludingType) {
-				ItemStack[] matchingStacks = recipe.getIngredients().get(0).getMatchingStacks();
+				ItemStack[] matchingStacks = recipe.getIngredients()
+					.get(0)
+					.getMatchingStacks();
 				if (matchingStacks.length == 0)
 					return true;
-				if (r.getIngredients().get(0).test(matchingStacks[0]))
+				if (r.getIngredients()
+					.get(0)
+					.test(matchingStacks[0]))
 					return true;
 			}
 			return false;
diff --git a/src/main/java/com/simibubi/create/compat/jei/category/BasinCategory.java b/src/main/java/com/simibubi/create/compat/jei/category/BasinCategory.java
new file mode 100644
index 000000000..73805cf3d
--- /dev/null
+++ b/src/main/java/com/simibubi/create/compat/jei/category/BasinCategory.java
@@ -0,0 +1,89 @@
+package com.simibubi.create.compat.jei.category;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.mojang.blaze3d.matrix.MatrixStack;
+import org.apache.commons.lang3.mutable.MutableInt;
+
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
+import com.simibubi.create.content.contraptions.processing.HeatCondition;
+import com.simibubi.create.foundation.gui.AllGuiTextures;
+import com.simibubi.create.foundation.item.ItemHelper;
+import com.simibubi.create.foundation.utility.Pair;
+
+import mezz.jei.api.constants.VanillaTypes;
+import mezz.jei.api.gui.IRecipeLayout;
+import mezz.jei.api.gui.drawable.IDrawable;
+import mezz.jei.api.gui.ingredient.IGuiItemStackGroup;
+import mezz.jei.api.ingredients.IIngredients;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.crafting.Ingredient;
+import net.minecraft.util.NonNullList;
+
+public class BasinCategory extends CreateRecipeCategory<BasinRecipe> {
+
+	public BasinCategory(String id, IDrawable icon, IDrawable background) {
+		super(id, icon, background);
+	}
+
+	@Override
+	public Class<? extends BasinRecipe> getRecipeClass() {
+		return BasinRecipe.class;
+	}
+
+	@Override
+	public void setIngredients(BasinRecipe recipe, IIngredients ingredients) {
+		ingredients.setInputIngredients(recipe.getIngredients());
+		ingredients.setOutput(VanillaTypes.ITEM, recipe.getRecipeOutput());
+	}
+
+	@Override
+	public void setRecipe(IRecipeLayout recipeLayout, BasinRecipe recipe, IIngredients ingredients) {
+		IGuiItemStackGroup itemStacks = recipeLayout.getItemStacks();
+		NonNullList<Ingredient> recipeIngredients = recipe.getIngredients();
+		List<Pair<Ingredient, MutableInt>> actualIngredients = ItemHelper.condenseIngredients(recipeIngredients);
+
+		int size = actualIngredients.size();
+		int xOffset = size < 3 ? (3 - size) * 19 / 2 : 0;
+		int yOffset = recipe.getRequiredHeat() != HeatCondition.NONE ? 30 : 10;
+
+		int i;
+		for (i = 0; i < actualIngredients.size(); i++) {
+			itemStacks.init(i, true, 16 + xOffset + (i % 3) * 19, 50 - (i / 3) * 19 + yOffset);
+			List<ItemStack> stacks = new ArrayList<>();
+			Pair<Ingredient, MutableInt> pair = actualIngredients.get(i);
+			Ingredient ingredient = pair.getFirst();
+			MutableInt amount = pair.getSecond();
+
+			for (ItemStack itemStack : ingredient.getMatchingStacks()) {
+				ItemStack stack = itemStack.copy();
+				stack.setCount(amount.getValue());
+				stacks.add(stack);
+			}
+
+			itemStacks.set(i, stacks);
+		}
+
+		itemStacks.init(i, false, 141, 50 + yOffset);
+		itemStacks.set(i, recipe.getRecipeOutput()
+			.getStack());
+	}
+
+	@Override
+	public void draw(BasinRecipe recipe, MatrixStack matrixStack, double mouseX, double mouseY) {
+		List<Pair<Ingredient, MutableInt>> actualIngredients = ItemHelper.condenseIngredients(recipe.getIngredients());
+
+		int size = actualIngredients.size();
+		int xOffset = size < 3 ? (3 - size) * 19 / 2 : 0;
+		HeatCondition requiredHeat = recipe.getRequiredHeat();
+		int yOffset = requiredHeat != HeatCondition.NONE ? 30 : 10;
+		for (int i = 0; i < size; i++)
+			AllGuiTextures.JEI_SLOT.draw(matrixStack, 16 + xOffset + (i % 3) * 19, 50 - (i / 3) * 19 + yOffset);
+
+		AllGuiTextures.JEI_SLOT.draw(matrixStack, 141, 50 + yOffset);
+		AllGuiTextures.JEI_DOWN_ARROW.draw(matrixStack, 136, 32 + yOffset);
+		AllGuiTextures.JEI_SHADOW.draw(matrixStack, 81, 57 + yOffset);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/compat/jei/category/MixingCategory.java b/src/main/java/com/simibubi/create/compat/jei/category/MixingCategory.java
index 46091b49f..fa72d5729 100644
--- a/src/main/java/com/simibubi/create/compat/jei/category/MixingCategory.java
+++ b/src/main/java/com/simibubi/create/compat/jei/category/MixingCategory.java
@@ -1,29 +1,13 @@
 package com.simibubi.create.compat.jei.category;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.lang3.mutable.MutableInt;
-
 import com.mojang.blaze3d.matrix.MatrixStack;
 import com.simibubi.create.AllBlocks;
 import com.simibubi.create.compat.jei.category.animations.AnimatedBlazeBurner;
 import com.simibubi.create.compat.jei.category.animations.AnimatedMixer;
-import com.simibubi.create.content.contraptions.components.mixer.MixingRecipe;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
 import com.simibubi.create.content.contraptions.processing.HeatCondition;
-import com.simibubi.create.foundation.gui.AllGuiTextures;
-import com.simibubi.create.foundation.item.ItemHelper;
-import com.simibubi.create.foundation.utility.Pair;
 
-import mezz.jei.api.constants.VanillaTypes;
-import mezz.jei.api.gui.IRecipeLayout;
-import mezz.jei.api.gui.ingredient.IGuiItemStackGroup;
-import mezz.jei.api.ingredients.IIngredients;
-import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.util.NonNullList;
-
-public class MixingCategory extends CreateRecipeCategory<MixingRecipe> {
+public class MixingCategory extends BasinCategory {
 
 	private AnimatedMixer mixer = new AnimatedMixer();
 	private AnimatedBlazeBurner heater = new AnimatedBlazeBurner();
@@ -34,63 +18,9 @@ public class MixingCategory extends CreateRecipeCategory<MixingRecipe> {
 	}
 
 	@Override
-	public Class<? extends MixingRecipe> getRecipeClass() {
-		return MixingRecipe.class;
-	}
-
-	@Override
-	public void setIngredients(MixingRecipe recipe, IIngredients ingredients) {
-		ingredients.setInputIngredients(recipe.getIngredients());
-		ingredients.setOutput(VanillaTypes.ITEM, recipe.getRecipeOutput());
-	}
-
-	@Override
-	public void setRecipe(IRecipeLayout recipeLayout, MixingRecipe recipe, IIngredients ingredients) {
-		IGuiItemStackGroup itemStacks = recipeLayout.getItemStacks();
-		NonNullList<Ingredient> recipeIngredients = recipe.getIngredients();
-		List<Pair<Ingredient, MutableInt>> actualIngredients = ItemHelper.condenseIngredients(recipeIngredients);
-
-		int size = actualIngredients.size();
-		int xOffset = size < 3 ? (3 - size) * 19 / 2 : 0;
-		int yOffset = recipe.getRequiredHeat() != HeatCondition.NONE ? 30 : 10;
-
-		int i;
-		for (i = 0; i < actualIngredients.size(); i++) {
-			itemStacks.init(i, true, 16 + xOffset + (i % 3) * 19, 50 - (i / 3) * 19 + yOffset);
-			List<ItemStack> stacks = new ArrayList<>();
-			Pair<Ingredient, MutableInt> pair = actualIngredients.get(i);
-			Ingredient ingredient = pair.getFirst();
-			MutableInt amount = pair.getSecond();
-
-			for (ItemStack itemStack : ingredient.getMatchingStacks()) {
-				ItemStack stack = itemStack.copy();
-				stack.setCount(amount.getValue());
-				stacks.add(stack);
-			}
-
-			itemStacks.set(i, stacks);
-		}
-
-		itemStacks.init(i, false, 141, 50 + yOffset);
-		itemStacks.set(i, recipe.getRecipeOutput()
-			.getStack());
-	}
-
-	@Override
-	public void draw(MixingRecipe recipe, MatrixStack matrixStack, double mouseX, double mouseY) {
-		List<Pair<Ingredient, MutableInt>> actualIngredients = ItemHelper.condenseIngredients(recipe.getIngredients());
-
-		int size = actualIngredients.size();
-		int xOffset = size < 3 ? (3 - size) * 19 / 2 : 0;
+	public void draw(BasinRecipe recipe, MatrixStack matrixStack, double mouseX, double mouseY) {
+		super.draw(recipe, matrixStack, mouseX, mouseY);
 		HeatCondition requiredHeat = recipe.getRequiredHeat();
-		int yOffset = requiredHeat != HeatCondition.NONE ? 30 : 10;
-		for (int i = 0; i < size; i++)
-			AllGuiTextures.JEI_SLOT.draw(matrixStack, 16 + xOffset + (i % 3) * 19, 50 - (i / 3) * 19 + yOffset);
-
-		AllGuiTextures.JEI_SLOT.draw(matrixStack, 141, 50 + yOffset);
-		AllGuiTextures.JEI_DOWN_ARROW.draw(matrixStack, 136, 32 + yOffset);
-		AllGuiTextures.JEI_SHADOW.draw(matrixStack, 81, 57 + yOffset);
-
 		if (requiredHeat != HeatCondition.NONE)
 			heater.withHeat(requiredHeat.visualizeAsBlazeBurner())
 				.draw(matrixStack, getBackground().getWidth() / 2 + 3, 55);
diff --git a/src/main/java/com/simibubi/create/compat/jei/category/PackingCategory.java b/src/main/java/com/simibubi/create/compat/jei/category/PackingCategory.java
index 7624568f1..d69e8d100 100644
--- a/src/main/java/com/simibubi/create/compat/jei/category/PackingCategory.java
+++ b/src/main/java/com/simibubi/create/compat/jei/category/PackingCategory.java
@@ -5,18 +5,16 @@ import java.util.Arrays;
 import com.mojang.blaze3d.matrix.MatrixStack;
 import com.simibubi.create.AllBlocks;
 import com.simibubi.create.compat.jei.category.animations.AnimatedPress;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
 import com.simibubi.create.foundation.gui.AllGuiTextures;
 
-import mezz.jei.api.constants.VanillaTypes;
 import mezz.jei.api.gui.IRecipeLayout;
 import mezz.jei.api.gui.ingredient.IGuiItemStackGroup;
 import mezz.jei.api.ingredients.IIngredients;
-import net.minecraft.item.crafting.ICraftingRecipe;
-import net.minecraft.item.crafting.IRecipe;
 import net.minecraft.item.crafting.Ingredient;
 import net.minecraft.util.NonNullList;
 
-public class PackingCategory extends CreateRecipeCategory<IRecipe<?>> {
+public class PackingCategory extends BasinCategory {
 
 	private AnimatedPress press = new AnimatedPress(true);
 
@@ -26,18 +24,12 @@ public class PackingCategory extends CreateRecipeCategory<IRecipe<?>> {
 	}
 
 	@Override
-	public Class<? extends IRecipe<?>> getRecipeClass() {
-		return ICraftingRecipe.class;
-	}
-
-	@Override
-	public void setIngredients(IRecipe<?> recipe, IIngredients ingredients) {
-		ingredients.setInputIngredients(recipe.getIngredients());
-		ingredients.setOutput(VanillaTypes.ITEM, recipe.getRecipeOutput());
-	}
-
-	@Override
-	public void setRecipe(IRecipeLayout recipeLayout, IRecipe<?> recipe, IIngredients ingredients) {
+	public void setRecipe(IRecipeLayout recipeLayout, BasinRecipe recipe, IIngredients ingredients) {
+		if (!recipe.convertedRecipe) {
+			super.setRecipe(recipeLayout, recipe, ingredients);
+			return;
+		}
+		
 		IGuiItemStackGroup itemStacks = recipeLayout.getItemStacks();
 		int i = 0;
 
@@ -56,16 +48,21 @@ public class PackingCategory extends CreateRecipeCategory<IRecipe<?>> {
 	}
 
 	@Override
-	public void draw(IRecipe<?> recipe, MatrixStack matrixStack, double mouseX, double mouseY) {
-		NonNullList<Ingredient> ingredients2 = recipe.getIngredients();
-		int size = ingredients2.size();
-		int rows = size == 4 ? 2 : 3;
-		for (int i = 0; i < size; i++) {
-			AllGuiTextures.JEI_SLOT.draw(matrixStack, (rows == 2 ? 26 : 17) + (i % rows) * 19, 50 - (i / rows) * 19);
+	public void draw(BasinRecipe recipe, MatrixStack matrixStack, double mouseX, double mouseY) {
+		if (!recipe.convertedRecipe) {
+			super.draw(recipe, matrixStack, mouseX, mouseY);
+			
+		} else {
+			NonNullList<Ingredient> ingredients2 = recipe.getIngredients();
+			int size = ingredients2.size();
+			int rows = size == 4 ? 2 : 3;
+			for (int i = 0; i < size; i++) 
+				AllGuiTextures.JEI_SLOT.draw(matrixStack, (rows == 2 ? 26 : 17) + (i % rows) * 19, 50 - (i / rows) * 19);
+			AllGuiTextures.JEI_SLOT.draw(matrixStack, 141, 50);
+			AllGuiTextures.JEI_DOWN_ARROW.draw(matrixStack, 136, 32);
+			AllGuiTextures.JEI_SHADOW.draw(matrixStack, 81, 57);
 		}
-		AllGuiTextures.JEI_SLOT.draw(matrixStack, 141, 50);
-		AllGuiTextures.JEI_DOWN_ARROW.draw(matrixStack, 136, 32);
-		AllGuiTextures.JEI_SHADOW.draw(matrixStack, 81, 57);
+		
 		press.draw(matrixStack, getBackground().getWidth() / 2 + 6, 30);
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/crafter/RecipeGridHandler.java b/src/main/java/com/simibubi/create/content/contraptions/components/crafter/RecipeGridHandler.java
index 55c04b434..20de1a509 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/crafter/RecipeGridHandler.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/crafter/RecipeGridHandler.java
@@ -15,6 +15,7 @@ import org.apache.commons.lang3.tuple.Pair;
 import com.google.common.base.Predicates;
 import com.simibubi.create.AllBlocks;
 import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.foundation.config.AllConfigs;
 import com.simibubi.create.foundation.utility.Pointing;
 
 import net.minecraft.block.BlockState;
@@ -139,16 +140,16 @@ public class RecipeGridHandler {
 	public static ItemStack tryToApplyRecipe(World world, GroupedItems items) {
 		items.calcStats();
 		CraftingInventory craftinginventory = new MechanicalCraftingInventory(items);
-		ItemStack result = world.getRecipeManager()
-			.getRecipe(IRecipeType.CRAFTING, craftinginventory, world)
-			.map(r -> r.getCraftingResult(craftinginventory))
-			.orElse(null);
-		if (result == null)
+		ItemStack result = null;
+		if (AllConfigs.SERVER.recipes.allowRegularCraftingInCrafter.get())
 			result = world.getRecipeManager()
-				.getRecipe(AllRecipeTypes.MECHANICAL_CRAFTING.getType(), craftinginventory, world)
+				.getRecipe(IRecipeType.CRAFTING, craftinginventory, world)
+				.map(r -> r.getCraftingResult(craftinginventory))
+				.orElse(null);
+		if (result == null)
+			result = AllRecipeTypes.MECHANICAL_CRAFTING.find(craftinginventory, world)
 				.map(r -> r.getCraftingResult(craftinginventory))
 				.orElse(null);
-
 		return result;
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/crusher/CrushingWheelControllerTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/crusher/CrushingWheelControllerTileEntity.java
index c6d79bada..cdbfb51a2 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/crusher/CrushingWheelControllerTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/crusher/CrushingWheelControllerTileEntity.java
@@ -207,11 +207,9 @@ public class CrushingWheelControllerTileEntity extends SmartTileEntity {
 	}
 
 	public Optional<ProcessingRecipe<RecipeWrapper>> findRecipe() {
-		Optional<ProcessingRecipe<RecipeWrapper>> crushingRecipe = world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.CRUSHING.getType(), wrapper, world);
+		Optional<ProcessingRecipe<RecipeWrapper>> crushingRecipe = AllRecipeTypes.CRUSHING.find(wrapper, world);
 		if (!crushingRecipe.isPresent())
-			crushingRecipe = world.getRecipeManager()
-				.getRecipe(AllRecipeTypes.MILLING.getType(), wrapper, world);
+			crushingRecipe = AllRecipeTypes.MILLING.find(wrapper, world);
 		return crushingRecipe;
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/millstone/MillstoneTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/millstone/MillstoneTileEntity.java
index e400ed028..84315ee04 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/millstone/MillstoneTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/millstone/MillstoneTileEntity.java
@@ -66,8 +66,7 @@ public class MillstoneTileEntity extends KineticTileEntity {
 
 		RecipeWrapper inventoryIn = new RecipeWrapper(inputInv);
 		if (lastRecipe == null || !lastRecipe.matches(inventoryIn, world)) {
-			Optional<MillingRecipe> recipe = world.getRecipeManager()
-				.getRecipe(AllRecipeTypes.MILLING.getType(), inventoryIn, world);
+			Optional<MillingRecipe> recipe = AllRecipeTypes.MILLING.find(inventoryIn, world);
 			if (!recipe.isPresent()) {
 				timer = 100;
 				sendData();
@@ -87,8 +86,7 @@ public class MillstoneTileEntity extends KineticTileEntity {
 		RecipeWrapper inventoryIn = new RecipeWrapper(inputInv);
 
 		if (lastRecipe == null || !lastRecipe.matches(inventoryIn, world)) {
-			Optional<MillingRecipe> recipe = world.getRecipeManager()
-				.getRecipe(AllRecipeTypes.MILLING.getType(), inventoryIn, world);
+			Optional<MillingRecipe> recipe = AllRecipeTypes.MILLING.find(inventoryIn, world);
 			if (!recipe.isPresent())
 				return;
 			lastRecipe = recipe.get();
@@ -154,8 +152,7 @@ public class MillstoneTileEntity extends KineticTileEntity {
 
 		if (lastRecipe != null && lastRecipe.matches(inventoryIn, world))
 			return true;
-		return world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.MILLING.getType(), inventoryIn, world)
+		return AllRecipeTypes.MILLING.find(inventoryIn, world)
 			.isPresent();
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/mixer/CompactingRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/CompactingRecipe.java
new file mode 100644
index 000000000..7e25e410e
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/CompactingRecipe.java
@@ -0,0 +1,13 @@
+package com.simibubi.create.content.contraptions.components.mixer;
+
+import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
+import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeParams;
+
+public class CompactingRecipe extends BasinRecipe {
+
+	public CompactingRecipe(ProcessingRecipeParams params) {
+		super(AllRecipeTypes.COMPACTING, params);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MechanicalMixerTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MechanicalMixerTileEntity.java
index ebbda2284..75979acbf 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MechanicalMixerTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MechanicalMixerTileEntity.java
@@ -1,18 +1,14 @@
 package com.simibubi.create.content.contraptions.components.mixer;
 
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Optional;
 
 import com.simibubi.create.AllRecipeTypes;
-import com.simibubi.create.AllTags;
 import com.simibubi.create.content.contraptions.components.press.MechanicalPressTileEntity;
 import com.simibubi.create.content.contraptions.processing.BasinOperatingTileEntity;
 import com.simibubi.create.content.contraptions.processing.BasinTileEntity;
-import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock;
-import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock.HeatLevel;
+import com.simibubi.create.foundation.advancement.AllTriggers;
+import com.simibubi.create.foundation.advancement.ITriggerable;
+import com.simibubi.create.foundation.config.AllConfigs;
 import com.simibubi.create.foundation.item.SmartInventory;
 import com.simibubi.create.foundation.utility.BlockHelper;
 import com.simibubi.create.foundation.utility.VecHelper;
@@ -22,13 +18,11 @@ import net.minecraft.inventory.IInventory;
 import net.minecraft.item.ItemStack;
 import net.minecraft.item.crafting.IRecipe;
 import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
 import net.minecraft.nbt.CompoundNBT;
 import net.minecraft.particles.ItemParticleData;
 import net.minecraft.particles.ParticleTypes;
 import net.minecraft.tileentity.TileEntityType;
 import net.minecraft.util.Direction.Axis;
-import net.minecraft.util.NonNullList;
 import net.minecraft.util.math.AxisAlignedBB;
 import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.math.vector.Vector3d;
@@ -100,13 +94,6 @@ public class MechanicalMixerTileEntity extends BasinOperatingTileEntity {
 		super.write(compound, clientPacket);
 	}
 
-	@Override
-	public void lazyTick() {
-		super.lazyTick();
-		if (world != null && world.isRemote && running && !basinItemInv.isPresent())
-			updateBasin();
-	}
-
 	@Override
 	public void tick() {
 		super.tick();
@@ -169,45 +156,11 @@ public class MechanicalMixerTileEntity extends BasinOperatingTileEntity {
 
 	@Override
 	protected <C extends IInventory> boolean matchStaticFilters(IRecipe<C> r) {
-		return (r.getSerializer() == IRecipeSerializer.CRAFTING_SHAPELESS || r.getType() == AllRecipeTypes.MIXING.type)
+		return ((r.getSerializer() == IRecipeSerializer.CRAFTING_SHAPELESS
+			&& AllConfigs.SERVER.recipes.allowShapelessInMixer.get()) || r.getType() == AllRecipeTypes.MIXING.type)
 			&& !MechanicalPressTileEntity.canCompress(r.getIngredients());
 	}
 
-	@Override
-	protected <C extends IInventory> boolean matchBasinRecipe(IRecipe<C> recipe) {
-		if (!super.matchBasinRecipe(recipe))
-			return false;
-
-		NonNullList<Ingredient> ingredients = recipe.getIngredients();
-		List<ItemStack> remainingItems = new ArrayList<>();
-		itemInputs.forEach(stack -> remainingItems.add(stack.copy()));
-		List<FluidStack> remainingFluids = new ArrayList<>();
-		fluidInputs.forEach(stack -> remainingFluids.add(stack.copy()));
-
-		// TODO: match fluid inputs
-
-		// Sort by leniency
-		List<Ingredient> sortedIngredients = new LinkedList<>(ingredients);
-		sortedIngredients.sort(Comparator.comparingInt(i -> i.getMatchingStacks().length));
-
-		Ingredients: for (Ingredient ingredient : sortedIngredients) {
-			for (ItemStack stack : remainingItems) {
-				if (stack.isEmpty())
-					continue;
-				if (ingredient.test(stack)) {
-					stack.shrink(1);
-					continue Ingredients;
-				}
-			}
-			return false;
-		}
-
-		if (!(recipe instanceof MixingRecipe))
-			return true;
-		return ((MixingRecipe) recipe).getRequiredHeat()
-			.testBlazeBurner(getHeatLevel());
-	}
-
 	@Override
 	public void startProcessingBasin() {
 		if (running && runningTicks <= 20)
@@ -240,13 +193,9 @@ public class MechanicalMixerTileEntity extends BasinOperatingTileEntity {
 	protected boolean isRunning() {
 		return running;
 	}
-
-	private HeatLevel getHeatLevel() {
-		if (world == null)
-			return HeatLevel.NONE;
-		BlockState state = world.getBlockState(pos.down(3));
-		if (BlockHelper.hasBlockStateProperty(state, BlazeBurnerBlock.HEAT_LEVEL))
-			return state.get(BlazeBurnerBlock.HEAT_LEVEL);
-		return AllTags.AllBlockTags.FAN_HEATERS.matches(state) ? HeatLevel.SMOULDERING : HeatLevel.NONE;
+	
+	@Override
+	protected Optional<ITriggerable> getProcessedRecipeTrigger() {
+		return Optional.of(AllTriggers.MIXER_MIX);
 	}
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MixingRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MixingRecipe.java
index c36564c57..12e1d7b0f 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MixingRecipe.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/mixer/MixingRecipe.java
@@ -1,98 +1,13 @@
 package com.simibubi.create.content.contraptions.components.mixer;
 
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
-
-import javax.annotation.Nonnull;
-
 import com.simibubi.create.AllRecipeTypes;
-import com.simibubi.create.content.contraptions.processing.ProcessingRecipe;
-import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder;
+import com.simibubi.create.content.contraptions.processing.BasinRecipe;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeParams;
-import com.simibubi.create.foundation.item.SmartInventory;
 
-import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipe;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.util.NonNullList;
-import net.minecraft.world.World;
-
-public class MixingRecipe extends ProcessingRecipe<SmartInventory> {
-
-	/**
-	 * For JEI purposes only
-	 */
-	public static MixingRecipe convertShapeless(IRecipe<?> recipe) {
-		return new ProcessingRecipeBuilder<>(MixingRecipe::new, recipe.getId())
-			.withItemIngredients(recipe.getIngredients())
-			.withSingleItemOutput(recipe.getRecipeOutput())
-			.build();
-	}
+public class MixingRecipe extends BasinRecipe {
 
 	public MixingRecipe(ProcessingRecipeParams params) {
 		super(AllRecipeTypes.MIXING, params);
 	}
 
-	@Override
-	protected int getMaxInputCount() {
-		return 9;
-	}
-
-	@Override
-	protected int getMaxOutputCount() {
-		return 1;// TODO increase
-	}
-
-	@Override
-	protected int getMaxFluidInputCount() {
-		return 2;
-	}
-
-	@Override
-	protected int getMaxFluidOutputCount() {
-		return 1;// TODO increase?
-	}
-	
-	@Override
-	protected boolean canRequireHeat() {
-		return true;
-	}
-
-	@Override
-	public boolean matches(SmartInventory inv, @Nonnull World worldIn) {
-		if (inv.isEmpty())
-			return false;
-
-		NonNullList<Ingredient> ingredients = this.getIngredients();
-		if (!ingredients.stream()
-			.allMatch(Ingredient::isSimple))
-			return false;
-
-		List<ItemStack> remaining = new ArrayList<>();
-		for (int slot = 0; slot < inv.getSizeInventory(); ++slot) {
-			ItemStack itemstack = inv.getStackInSlot(slot);
-			if (!itemstack.isEmpty()) {
-				remaining.add(itemstack.copy());
-			}
-		}
-
-		// sort by leniency
-		List<Ingredient> sortedIngredients = new LinkedList<>(ingredients);
-		sortedIngredients.sort(Comparator.comparingInt(i -> i.getMatchingStacks().length));
-		Ingredients: for (Ingredient ingredient : sortedIngredients) {
-			for (ItemStack stack : remaining) {
-				if (stack.isEmpty())
-					continue;
-				if (ingredient.test(stack)) {
-					stack.shrink(1);
-					continue Ingredients;
-				}
-			}
-			return false;
-		}
-		return true;
-	}
-
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/press/MechanicalPressTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/press/MechanicalPressTileEntity.java
index ba6475164..c6d0ebfc4 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/press/MechanicalPressTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/press/MechanicalPressTileEntity.java
@@ -10,6 +10,8 @@ import com.simibubi.create.content.contraptions.processing.BasinOperatingTileEnt
 import com.simibubi.create.content.contraptions.processing.BasinTileEntity;
 import com.simibubi.create.content.logistics.InWorldProcessing;
 import com.simibubi.create.foundation.advancement.AllTriggers;
+import com.simibubi.create.foundation.advancement.ITriggerable;
+import com.simibubi.create.foundation.config.AllConfigs;
 import com.simibubi.create.foundation.item.ItemHelper;
 import com.simibubi.create.foundation.item.SmartInventory;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
@@ -42,29 +44,12 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 
 	private static final Object compressingRecipesKey = new Object();
 	public List<ItemStack> pressedItems = new ArrayList<>();
-
-	public static class PressingInv extends RecipeWrapper {
-		public PressingInv() {
-			super(new ItemStackHandler(1));
-		}
-	}
-
-	enum Mode {
-		WORLD(1), BELT(19f / 16f), BASIN(22f / 16f)
-
-		;
-
-		float headOffset;
-
-		Mode(float headOffset) {
-			this.headOffset = headOffset;
-		}
-	}
-
-	private static final PressingInv pressingInv = new PressingInv();
 	public BeltProcessingBehaviour processingBehaviour;
 
+	public int prevRunningTicks;
 	public int runningTicks;
+	static final int CYCLE = 240;
+
 	public boolean running;
 	public Mode mode;
 	public boolean finished;
@@ -88,7 +73,7 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 		running = compound.getBoolean("Running");
 		mode = Mode.values()[compound.getInt("Mode")];
 		finished = compound.getBoolean("Finished");
-		runningTicks = compound.getInt("Ticks");
+		prevRunningTicks = runningTicks = compound.getInt("Ticks");
 		super.fromTag(state, compound, clientPacket);
 
 		if (clientPacket) {
@@ -106,8 +91,10 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 		compound.putInt("Ticks", runningTicks);
 		super.write(compound, clientPacket);
 
-		if (clientPacket)
+		if (clientPacket) {
 			compound.put("ParticleItems", NBTHelper.writeCompoundList(pressedItems, ItemStack::serializeNBT));
+			pressedItems.clear();
+		}
 	}
 
 	@Override
@@ -117,15 +104,12 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 	}
 
 	public float getRenderedHeadOffset(float partialTicks) {
-		if (running) {
-			if (runningTicks < 40) {
-				float num = (runningTicks - 1 + partialTicks) / 30f;
-				return MathHelper.clamp(num * num * num, 0, mode.headOffset);
-			}
-			return MathHelper.clamp(((60 - runningTicks) + 1 - partialTicks) / 20f * mode.headOffset, 0,
-				mode.headOffset);
-		}
-		return 0;
+		if (!running)
+			return 0;
+		float ticks = MathHelper.lerp(partialTicks, prevRunningTicks, runningTicks);
+		if (runningTicks < (CYCLE * 2) / 3)
+			return (float) MathHelper.clamp(Math.pow(ticks / CYCLE * 2, 3), 0, mode.headOffset);
+		return MathHelper.clamp((CYCLE - ticks) / CYCLE * 3 * mode.headOffset, 0, mode.headOffset);
 	}
 
 	public void start(Mode mode) {
@@ -151,50 +135,11 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 		if (!running || world == null)
 			return;
 
-		if (runningTicks == 30) {
-
-			if (inWorld()) {
-				AxisAlignedBB bb = new AxisAlignedBB(pos.down(1));
-				pressedItems.clear();
-				for (Entity entity : world.getEntitiesWithinAABBExcludingEntity(null, bb)) {
-					if (!(entity instanceof ItemEntity))
-						continue;
-
-					ItemEntity itemEntity = (ItemEntity) entity;
-
-					if (!world.isRemote) {
-						pressedItems.add(itemEntity.getItem());
-						sendData();
-						Optional<PressingRecipe> recipe = getRecipe(itemEntity.getItem());
-						if (recipe.isPresent()) {
-							InWorldProcessing.applyRecipeOn(itemEntity, recipe.get());
-							AllTriggers.triggerForNearbyPlayers(AllTriggers.BONK, world, pos, 4);
-						}
-					}
-				}
-			}
-
-			if (onBasin()) {
-				if (!world.isRemote) {
-					pressedItems.clear();
-					applyBasinRecipe();
-
-					Optional<BasinTileEntity> basin = getBasin();
-					SmartInventory inputs = basin.get()
-						.getInputInventory();
-					if (basin.isPresent()) {
-						for (int slot = 0; slot < inputs.getSlots(); slot++) {
-							ItemStack stackInSlot = inputs.getStackInSlot(slot);
-							if (stackInSlot.isEmpty())
-								continue;
-							pressedItems.add(stackInSlot);
-						}
-					}
-					sendData();
-				}
-
-			}
-
+		if (runningTicks == CYCLE / 2) {
+			if (inWorld())
+				applyPressingInWorld();
+			if (onBasin())
+				applyCompactingOnBasin();
 			if (!world.isRemote) {
 				world.playSound(null, getPos(), AllSoundEvents.MECHANICAL_PRESS_ITEM_BREAK.get(), SoundCategory.BLOCKS,
 					.5f, 1f);
@@ -203,105 +148,125 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 			}
 		}
 
-		if (!world.isRemote && runningTicks > 60) {
+		if (!world.isRemote && runningTicks > CYCLE) {
 			finished = true;
 			if (inWorld())
 				finished = world.isBlockPowered(pos);
 			running = false;
 
-			if (onBasin()) {
-				gatherInputs();
-				if (matchBasinRecipe(lastRecipe)) {
-					startProcessingBasin();
-				}
-			}
+			if (onBasin() && matchBasinRecipe(currentRecipe))
+				startProcessingBasin();
 
 			pressedItems.clear();
 			sendData();
 			return;
 		}
 
-		runningTicks++;
+		prevRunningTicks = runningTicks;
+		runningTicks += getRunningTickSpeed();
+		if (prevRunningTicks < CYCLE / 2 && runningTicks >= CYCLE / 2)
+			runningTicks = CYCLE / 2;
+	}
+
+	protected void applyCompactingOnBasin() {
+		if (world.isRemote)
+			return;
+		pressedItems.clear();
+		applyBasinRecipe();
+		Optional<BasinTileEntity> basin = getBasin();
+		SmartInventory inputs = basin.get()
+			.getInputInventory();
+		if (basin.isPresent()) {
+			for (int slot = 0; slot < inputs.getSlots(); slot++) {
+				ItemStack stackInSlot = inputs.getStackInSlot(slot);
+				if (stackInSlot.isEmpty())
+					continue;
+				pressedItems.add(stackInSlot);
+			}
+		}
+		sendData();
+	}
+
+	protected void applyPressingInWorld() {
+		AxisAlignedBB bb = new AxisAlignedBB(pos.down(1));
+		pressedItems.clear();
+		if (world.isRemote)
+			return;
+		for (Entity entity : world.getEntitiesWithinAABBExcludingEntity(null, bb)) {
+			if (!(entity instanceof ItemEntity))
+				continue;
+			ItemEntity itemEntity = (ItemEntity) entity;
+			pressedItems.add(itemEntity.getItem());
+			sendData();
+			Optional<PressingRecipe> recipe = getRecipe(itemEntity.getItem());
+			if (!recipe.isPresent())
+				continue;
+			InWorldProcessing.applyRecipeOn(itemEntity, recipe.get());
+			AllTriggers.triggerForNearbyPlayers(AllTriggers.BONK, world, pos, 4);
+		}
+	}
+
+	public int getRunningTickSpeed() {
+		if (getSpeed() == 0)
+			return 0;
+		return (int) MathHelper.lerp(MathHelper.clamp(Math.abs(getSpeed()) / 512f, 0, 1), 1, 60);
 	}
 
 	protected void spawnParticles() {
 		if (pressedItems.isEmpty())
 			return;
 
-		if (mode == Mode.BASIN) {
+		if (mode == Mode.BASIN)
 			pressedItems.forEach(stack -> makeCompactingParticleEffect(VecHelper.getCenterOf(pos.down(2)), stack));
-		}
-		if (mode == Mode.BELT) {
+		if (mode == Mode.BELT)
 			pressedItems.forEach(stack -> makePressingParticleEffect(VecHelper.getCenterOf(pos.down(2))
 				.add(0, 8 / 16f, 0), stack));
-		}
-		if (mode == Mode.WORLD) {
+		if (mode == Mode.WORLD)
 			pressedItems.forEach(stack -> makePressingParticleEffect(VecHelper.getCenterOf(pos.down(1))
 				.add(0, -1 / 4f, 0), stack));
-		}
 
 		pressedItems.clear();
 	}
 
 	public void makePressingParticleEffect(Vector3d pos, ItemStack stack) {
-		if (world != null && world.isRemote) {
-			for (int i = 0; i < 20; i++) {
-				Vector3d motion = VecHelper.offsetRandomly(Vector3d.ZERO, world.rand, .125f)
-					.mul(1, 0, 1);
-				world.addParticle(new ItemParticleData(ParticleTypes.ITEM, stack), pos.x, pos.y - .25f, pos.z, motion.x,
-					motion.y + .125f, motion.z);
-			}
+		if (world == null || !world.isRemote)
+			return;
+		for (int i = 0; i < 20; i++) {
+			Vector3d motion = VecHelper.offsetRandomly(Vector3d.ZERO, world.rand, .125f)
+				.mul(1, 0, 1);
+			world.addParticle(new ItemParticleData(ParticleTypes.ITEM, stack), pos.x, pos.y - .25f, pos.z, motion.x,
+				motion.y + .125f, motion.z);
 		}
 	}
 
 	public void makeCompactingParticleEffect(Vector3d pos, ItemStack stack) {
-		if (world != null && world.isRemote) {
-			for (int i = 0; i < 20; i++) {
-				Vector3d motion = VecHelper.offsetRandomly(Vector3d.ZERO, world.rand, .175f)
-					.mul(1, 0, 1);
-				world.addParticle(new ItemParticleData(ParticleTypes.ITEM, stack), pos.x, pos.y, pos.z, motion.x,
-					motion.y + .25f, motion.z);
-			}
+		if (world == null || !world.isRemote)
+			return;
+		for (int i = 0; i < 20; i++) {
+			Vector3d motion = VecHelper.offsetRandomly(Vector3d.ZERO, world.rand, .175f)
+				.mul(1, 0, 1);
+			world.addParticle(new ItemParticleData(ParticleTypes.ITEM, stack), pos.x, pos.y, pos.z, motion.x,
+				motion.y + .25f, motion.z);
 		}
 	}
 
+	private static final RecipeWrapper pressingInv = new RecipeWrapper(new ItemStackHandler(1));
+
 	public Optional<PressingRecipe> getRecipe(ItemStack item) {
 		pressingInv.setInventorySlotContents(0, item);
-		return world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.PRESSING.getType(), pressingInv, world);
+		return AllRecipeTypes.PRESSING.find(pressingInv, world);
 	}
 
 	public static boolean canCompress(NonNullList<Ingredient> ingredients) {
-		return (ingredients.size() == 4 || ingredients.size() == 9) && ItemHelper.condenseIngredients(ingredients)
-			.size() == 1;
+		return AllConfigs.SERVER.recipes.allowShapedSquareInPress.get()
+			&& (ingredients.size() == 4 || ingredients.size() == 9) && ItemHelper.condenseIngredients(ingredients)
+				.size() == 1;
 	}
 
 	@Override
 	protected <C extends IInventory> boolean matchStaticFilters(IRecipe<C> recipe) {
-		return recipe instanceof ICraftingRecipe && canCompress(recipe.getIngredients());
-	}
-
-	@Override
-	protected <C extends IInventory> boolean matchBasinRecipe(IRecipe<C> recipe) {
-		if (!super.matchBasinRecipe(recipe))
-			return false;
-
-		NonNullList<Ingredient> ingredients = recipe.getIngredients();
-		List<ItemStack> remainingItems = new ArrayList<>();
-		itemInputs.forEach(stack -> remainingItems.add(stack.copy()));
-
-		Ingredients: for (Ingredient ingredient : ingredients) {
-			for (ItemStack stack : remainingItems) {
-				if (stack.isEmpty())
-					continue;
-				if (ingredient.test(stack)) {
-					stack.shrink(1);
-					continue Ingredients;
-				}
-			}
-			return false;
-		}
-		return true;
+		return (recipe instanceof ICraftingRecipe && canCompress(recipe.getIngredients()))
+			|| recipe.getType() == AllRecipeTypes.COMPACTING.type;
 	}
 
 	@Override
@@ -311,7 +276,7 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 
 	@Override
 	public void startProcessingBasin() {
-		if (running && runningTicks <= 30)
+		if (running && runningTicks <= CYCLE / 2)
 			return;
 		super.startProcessingBasin();
 		start(Mode.BASIN);
@@ -330,4 +295,21 @@ public class MechanicalPressTileEntity extends BasinOperatingTileEntity {
 		return running;
 	}
 
+	@Override
+	protected Optional<ITriggerable> getProcessedRecipeTrigger() {
+		return Optional.of(AllTriggers.PRESS_COMPACT);
+	}
+
+	enum Mode {
+		WORLD(1), BELT(19f / 16f), BASIN(22f / 16f)
+
+		;
+
+		float headOffset;
+
+		Mode(float headOffset) {
+			this.headOffset = headOffset;
+		}
+	}
+
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/press/PressingRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/components/press/PressingRecipe.java
index b08c0e1c9..481acc8e0 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/press/PressingRecipe.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/press/PressingRecipe.java
@@ -3,21 +3,21 @@ package com.simibubi.create.content.contraptions.components.press;
 import javax.annotation.ParametersAreNonnullByDefault;
 
 import com.simibubi.create.AllRecipeTypes;
-import com.simibubi.create.content.contraptions.components.press.MechanicalPressTileEntity.PressingInv;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipe;
 import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeParams;
 
 import net.minecraft.world.World;
+import net.minecraftforge.items.wrapper.RecipeWrapper;
 
 @ParametersAreNonnullByDefault
-public class PressingRecipe extends ProcessingRecipe<MechanicalPressTileEntity.PressingInv> {
+public class PressingRecipe extends ProcessingRecipe<RecipeWrapper> {
 
 	public PressingRecipe(ProcessingRecipeParams params) {
 		super(AllRecipeTypes.PRESSING, params);
 	}
 
 	@Override
-	public boolean matches(PressingInv inv, World worldIn) {
+	public boolean matches(RecipeWrapper inv, World worldIn) {
 		if (inv.isEmpty())
 			return false;
 		return ingredients.get(0)
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/saw/SawTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/saw/SawTileEntity.java
index b10c82535..523256e06 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/saw/SawTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/saw/SawTileEntity.java
@@ -8,9 +8,11 @@ import java.util.List;
 import java.util.Random;
 import java.util.stream.Collectors;
 
+import com.google.common.base.Predicate;
 import com.simibubi.create.AllRecipeTypes;
 import com.simibubi.create.content.contraptions.components.actors.BlockBreakingKineticTileEntity;
 import com.simibubi.create.content.contraptions.processing.ProcessingInventory;
+import com.simibubi.create.foundation.config.AllConfigs;
 import com.simibubi.create.foundation.item.ItemHelper;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.belt.DirectBeltInputBehaviour;
@@ -259,8 +261,10 @@ public class SawTileEntity extends BlockBreakingKineticTileEntity {
 	}
 
 	private List<? extends IRecipe<?>> getRecipes() {
-		List<IRecipe<?>> startedSearch = RecipeFinder.get(cuttingRecipesKey, world,
-			RecipeConditions.isOfType(IRecipeType.STONECUTTING, AllRecipeTypes.CUTTING.getType()));
+		Predicate<IRecipe<?>> types = AllConfigs.SERVER.recipes.allowStonecuttingOnSaw.get()
+			? RecipeConditions.isOfType(IRecipeType.STONECUTTING, AllRecipeTypes.CUTTING.getType())
+			: RecipeConditions.isOfType(AllRecipeTypes.CUTTING.getType());
+		List<IRecipe<?>> startedSearch = RecipeFinder.get(cuttingRecipesKey, world, types);
 		return startedSearch.stream()
 			.filter(RecipeConditions.outputMatchesFilter(filtering))
 			.filter(RecipeConditions.firstIngredientMatches(inventory.getStackInSlot(0)))
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionCollider.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionCollider.java
index a78665c7f..e7b2744bf 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionCollider.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionCollider.java
@@ -27,6 +27,7 @@ import com.simibubi.create.foundation.utility.VecHelper;
 
 import net.minecraft.block.BlockState;
 import net.minecraft.block.CocoaBlock;
+import net.minecraft.client.entity.player.ClientPlayerEntity;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
 import net.minecraft.entity.player.PlayerEntity;
@@ -36,6 +37,7 @@ import net.minecraft.util.Direction.AxisDirection;
 import net.minecraft.util.ReuseableStream;
 import net.minecraft.util.math.AxisAlignedBB;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.math.shapes.IBooleanFunction;
 import net.minecraft.util.math.shapes.ISelectionContext;
 import net.minecraft.util.math.shapes.VoxelShape;
@@ -43,6 +45,9 @@ import net.minecraft.util.math.shapes.VoxelShapes;
 import net.minecraft.util.math.vector.Vector3d;
 import net.minecraft.world.World;
 import net.minecraft.world.gen.feature.template.Template.BlockInfo;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import net.minecraftforge.fml.DistExecutor;
 
 public class ContraptionCollider {
 
@@ -53,14 +58,20 @@ public class ContraptionCollider {
 		for (Iterator<WeakReference<ContraptionEntity>> iterator = list.iterator(); iterator.hasNext();) {
 			WeakReference<ContraptionEntity> weakReference = iterator.next();
 			ContraptionEntity contraptionEntity = weakReference.get();
-			if (contraptionEntity == null || !contraptionEntity.isAlive()) {
+			if (contraptionEntity == null) {
 				iterator.remove();
 				continue;
 			}
+			if (!contraptionEntity.isAlive())
+				continue;
 			collideEntities(contraptionEntity);
 		}
 	}
 
+	enum PlayerType {
+		NONE, CLIENT, REMOTE, SERVER
+	}
+
 	private static void collideEntities(ContraptionEntity contraptionEntity) {
 		World world = contraptionEntity.getEntityWorld();
 		Contraption contraption = contraptionEntity.getContraption();
@@ -86,8 +97,10 @@ public class ContraptionCollider {
 
 		for (Entity entity : world.getEntitiesWithinAABB((EntityType<?>) null, bounds.grow(2)
 			.expand(0, 32, 0), contraptionEntity::canCollideWith)) {
-			boolean player = entity instanceof PlayerEntity;
-			boolean serverPlayer = player && !world.isRemote;
+
+			PlayerType playerType = getPlayerType(entity);
+			if (playerType == PlayerType.REMOTE)
+				continue;
 
 			// Init matrix
 			if (rotation == null) {
@@ -198,7 +211,7 @@ public class ContraptionCollider {
 			totalResponse = rotation.transform(totalResponse);
 			rotation.transpose();
 
-			if (futureCollision.isTrue() && !serverPlayer) {
+			if (futureCollision.isTrue() && playerType != PlayerType.SERVER) {
 				if (motionResponse.y != entityMotion.y) {
 					entity.setMotion(entityMotion.mul(1, 0, 1)
 						.add(0, motionResponse.y, 0));
@@ -211,7 +224,7 @@ public class ContraptionCollider {
 				entity.fallDistance = 0;
 				entity.setOnGround(true);
 				contraptionEntity.collidingEntities.add(entity);
-				if (!serverPlayer)
+				if (playerType != PlayerType.SERVER)
 					contactPointMotion = contraptionEntity.getContactPointMotion(entityPosition);
 			}
 
@@ -235,7 +248,7 @@ public class ContraptionCollider {
 			if (!hardCollision && surfaceCollision.isFalse())
 				continue;
 
-			if (serverPlayer && entity instanceof ServerPlayerEntity) {
+			if (playerType == PlayerType.SERVER && entity instanceof ServerPlayerEntity) {
 				((ServerPlayerEntity) entity).connection.floatingTickCount = 0;
 				continue;
 			}
@@ -248,8 +261,15 @@ public class ContraptionCollider {
 				entityPosition.z + allowedMovement.z);
 			entity.setMotion(entityMotion);
 
-			if (!serverPlayer && player)
-				AllPackets.channel.sendToServer(new ClientMotionPacket(entityMotion, true));
+			if (playerType != PlayerType.CLIENT)
+				continue;
+			
+			double d0 = entity.getX() - entity.prevPosX - contactPointMotion.x;
+			double d1 = entity.getZ() - entity.prevPosZ - contactPointMotion.z;
+			float limbSwing = MathHelper.sqrt(d0 * d0 + d1 * d1) * 4.0F;
+			if (limbSwing > 1.0F)
+				limbSwing = 1.0F;
+			AllPackets.channel.sendToServer(new ClientMotionPacket(entityMotion, true, limbSwing));
 		}
 
 	}
@@ -295,6 +315,21 @@ public class ContraptionCollider {
 		return Vector3d;
 	}
 
+	private static PlayerType getPlayerType(Entity entity) {
+		if (!(entity instanceof PlayerEntity))
+			return PlayerType.NONE;
+		if (!entity.world.isRemote)
+			return PlayerType.SERVER;
+		MutableBoolean isClient = new MutableBoolean(false);
+		DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> isClient.setValue(isClientPlayerEntity(entity)));
+		return isClient.booleanValue() ? PlayerType.CLIENT : PlayerType.REMOTE;
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	private static boolean isClientPlayerEntity(Entity entity) {
+		return entity instanceof ClientPlayerEntity;
+	}
+
 	private static ReuseableStream<VoxelShape> getPotentiallyCollidedShapes(World world, Contraption contraption,
 		AxisAlignedBB localBB) {
 
@@ -394,10 +429,10 @@ public class ContraptionCollider {
 			BlockInfo blockInfo = contraption.blocks.get(pos);
 
 			if (AllMovementBehaviours.hasMovementBehaviour(blockInfo.state.getBlock())) {
-				MovementBehaviour movementBehaviour = AllMovementBehaviours.getMovementBehaviour(blockInfo.state.getBlock());
+				MovementBehaviour movementBehaviour =
+					AllMovementBehaviours.getMovementBehaviour(blockInfo.state.getBlock());
 				if (movementBehaviour instanceof BlockBreakingMovementBehaviour) {
-					BlockBreakingMovementBehaviour behaviour =
-						(BlockBreakingMovementBehaviour) movementBehaviour;
+					BlockBreakingMovementBehaviour behaviour = (BlockBreakingMovementBehaviour) movementBehaviour;
 					if (!behaviour.canBreak(world, colliderPos, collidedState)
 						&& !collidedState.getCollisionShape(world, pos)
 							.isEmpty()) {
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionEntity.java
index e619e3f92..3e9baf232 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionEntity.java
@@ -78,6 +78,7 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 	final List<Entity> collidingEntities = new ArrayList<>();
 	private boolean isSerializingFurnaceCart;
 	private boolean attachedExtraInventories;
+	private boolean prevPosInvalid;
 
 	private static final Ingredient FUEL_ITEMS = Ingredient.fromItems(Items.COAL, Items.CHARCOAL);
 	private static final DataParameter<Boolean> STALLED =
@@ -107,6 +108,7 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 		isSerializingFurnaceCart = false;
 		attachedExtraInventories = false;
 		forcedAngle = -1;
+		prevPosInvalid = true;
 	}
 
 	public static ContraptionEntity createMounted(World world, Contraption contraption, float initialAngle) {
@@ -289,15 +291,20 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 			remove();
 			return;
 		}
+		
+		prevPosX = getX();
+		prevPosY = getY();
+		prevPosZ = getZ();
+		prevPosInvalid = false;
 
 		if (!initialized)
 			contraptionInitialize();
-
 		checkController();
 
 		Entity mountedEntity = getRidingEntity();
 		if (mountedEntity != null) {
 			tickAsPassenger(mountedEntity);
+			super.tick();
 			return;
 		}
 
@@ -308,7 +315,8 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 		if (ContraptionCollider.collideBlocks(this))
 			getController().collided();
 
-		tickActors(getPositionVec().subtract(prevPosX, prevPosY, prevPosZ));
+		Vector3d movement = getPositionVec().subtract(prevPosX, prevPosY, prevPosZ);
+		tickActors(movement);
 
 		prevYaw = yaw;
 		prevPitch = pitch;
@@ -439,8 +447,6 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 				furnaceCart.deserializeNBT(nbt);
 			}
 		}
-
-		super.tick();
 	}
 
 	public void tickActors(Vector3d movementVector) {
@@ -846,6 +852,9 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 	}
 
 	public Vector3d getContactPointMotion(Vector3d globalContactPoint) {
+		if (prevPosInvalid)
+			return Vector3d.ZERO;
+
 		Vector3d positionVec = getPositionVec();
 		Vector3d conMotion = positionVec.subtract(getPrevPositionVec());
 		Vector3d conAngularMotion = getRotationVec().subtract(getPrevRotationVec());
@@ -906,5 +915,10 @@ public class ContraptionEntity extends Entity implements IEntityAdditionalSpawnD
 	public void setCoupledCart(UUID id) {
 		dataManager.set(COUPLED_CART, Optional.ofNullable(id));
 	}
+	
+	@Override
+	public boolean isOnePlayerRiding() {
+		return false;
+	}
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandler.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandler.java
index ecba01568..ec45acedc 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandler.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandler.java
@@ -4,6 +4,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
@@ -28,7 +29,8 @@ public class ContraptionHandler {
 		if (!(entity instanceof ContraptionEntity))
 			return;
 		try {
-			List<WeakReference<ContraptionEntity>> list = activeContraptions.get(world, ArrayList::new);
+			List<WeakReference<ContraptionEntity>> list =
+				activeContraptions.get(world, () -> Collections.synchronizedList(new ArrayList<>()));
 			ContraptionEntity contraption = (ContraptionEntity) entity;
 			list.add(new WeakReference<>(contraption));
 		} catch (ExecutionException e) {
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionInteractionHandler.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandlerClient.java
similarity index 72%
rename from src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionInteractionHandler.java
rename to src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandlerClient.java
index 433e1a004..bb5d8a183 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionInteractionHandler.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionHandlerClient.java
@@ -1,4 +1,5 @@
 package com.simibubi.create.content.contraptions.components.structureMovement;
+
 import org.apache.commons.lang3.mutable.MutableObject;
 
 import com.simibubi.create.content.contraptions.components.structureMovement.sync.ContraptionInteractionPacket;
@@ -9,6 +10,8 @@ import com.simibubi.create.foundation.utility.RaycastHelper.PredicateTraceResult
 import net.minecraft.block.BlockState;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.entity.player.ClientPlayerEntity;
+import net.minecraft.client.entity.player.RemoteClientPlayerEntity;
+import net.minecraft.nbt.CompoundNBT;
 import net.minecraft.util.Direction;
 import net.minecraft.util.Hand;
 import net.minecraft.util.math.AxisAlignedBB;
@@ -20,11 +23,38 @@ import net.minecraft.world.gen.feature.template.Template.BlockInfo;
 import net.minecraftforge.api.distmarker.Dist;
 import net.minecraftforge.api.distmarker.OnlyIn;
 import net.minecraftforge.client.event.InputEvent.ClickInputEvent;
+import net.minecraftforge.event.TickEvent.Phase;
+import net.minecraftforge.event.TickEvent.PlayerTickEvent;
 import net.minecraftforge.eventbus.api.SubscribeEvent;
 import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
 
 @EventBusSubscriber
-public class ContraptionInteractionHandler {
+public class ContraptionHandlerClient {
+
+	@SubscribeEvent
+	@OnlyIn(Dist.CLIENT)
+	public static void preventRemotePlayersWalkingAnimations(PlayerTickEvent event) {
+		if (event.phase == Phase.START)
+			return;
+		if (!(event.player instanceof RemoteClientPlayerEntity))
+			return;
+		RemoteClientPlayerEntity remotePlayer = (RemoteClientPlayerEntity) event.player;
+		CompoundNBT data = remotePlayer.getPersistentData();
+		if (!data.contains("LastOverrideLimbSwingUpdate"))
+			return;
+
+		int lastOverride = data.getInt("LastOverrideLimbSwingUpdate");
+		data.putInt("LastOverrideLimbSwingUpdate", lastOverride + 1);
+		if (lastOverride > 5) {
+			data.remove("LastOverrideLimbSwingUpdate");
+			data.remove("OverrideLimbSwing");
+			return;
+		}
+
+		float limbSwing = data.getFloat("OverrideLimbSwing");
+		remotePlayer.prevPosX = remotePlayer.getX() - (limbSwing / 4);
+		remotePlayer.prevPosZ = remotePlayer.getZ();
+	}
 
 	@SubscribeEvent
 	@OnlyIn(Dist.CLIENT)
@@ -38,11 +68,12 @@ public class ContraptionInteractionHandler {
 		if (!event.isUseItem())
 			return;
 		Vector3d origin = RaycastHelper.getTraceOrigin(player);
-		
+
 		double reach = mc.playerController.getBlockReachDistance();
-		if (mc.objectMouseOver != null && mc.objectMouseOver.getHitVec() != null) 
-			reach = Math.min(mc.objectMouseOver.getHitVec().distanceTo(origin), reach);
-		
+		if (mc.objectMouseOver != null && mc.objectMouseOver.getHitVec() != null)
+			reach = Math.min(mc.objectMouseOver.getHitVec()
+				.distanceTo(origin), reach);
+
 		Vector3d target = RaycastHelper.getTraceTarget(player, reach, origin);
 		for (ContraptionEntity contraptionEntity : mc.world.getEntitiesWithinAABB(ContraptionEntity.class,
 			new AxisAlignedBB(origin, target))) {
@@ -75,11 +106,10 @@ public class ContraptionInteractionHandler {
 			Hand hand = event.getHand();
 			Direction face = rayTraceResult.getFace();
 			BlockPos pos = rayTraceResult.getPos();
-			
+
 			if (!contraptionEntity.handlePlayerInteraction(player, pos, face, hand))
 				return;
-			AllPackets.channel.sendToServer(new ContraptionInteractionPacket(contraptionEntity, hand,
-				pos, face));
+			AllPackets.channel.sendToServer(new ContraptionInteractionPacket(contraptionEntity, hand, pos, face));
 			event.setCanceled(true);
 			event.setSwingHand(false);
 		}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/ClientMotionPacket.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/ClientMotionPacket.java
index 772a548c9..1a4c7307c 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/ClientMotionPacket.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/ClientMotionPacket.java
@@ -2,26 +2,31 @@ package com.simibubi.create.content.contraptions.components.structureMovement.sy
 
 import java.util.function.Supplier;
 
+import com.simibubi.create.foundation.networking.AllPackets;
 import com.simibubi.create.foundation.networking.SimplePacketBase;
 
 import net.minecraft.entity.player.ServerPlayerEntity;
 import net.minecraft.network.PacketBuffer;
 import net.minecraft.util.math.vector.Vector3d;
 import net.minecraftforge.fml.network.NetworkEvent.Context;
+import net.minecraftforge.fml.network.PacketDistributor;
 
 public class ClientMotionPacket extends SimplePacketBase {
 
 	private Vector3d motion;
 	private boolean onGround;
+	private float limbSwing;
 
-	public ClientMotionPacket(Vector3d motion, boolean onGround) {
+	public ClientMotionPacket(Vector3d motion, boolean onGround, float limbSwing) {
 		this.motion = motion;
 		this.onGround = onGround;
+		this.limbSwing = limbSwing;
 	}
 
 	public ClientMotionPacket(PacketBuffer buffer) {
 		motion = new Vector3d(buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
 		onGround = buffer.readBoolean();
+		limbSwing = buffer.readFloat();
 	}
 
 	@Override
@@ -30,6 +35,7 @@ public class ClientMotionPacket extends SimplePacketBase {
 		buffer.writeFloat((float) motion.y);
 		buffer.writeFloat((float) motion.z);
 		buffer.writeBoolean(onGround);
+		buffer.writeFloat(limbSwing);
 	}
 
 	@Override
@@ -47,6 +53,8 @@ public class ClientMotionPacket extends SimplePacketBase {
 					sender.fallDistance = 0;
 					sender.connection.floatingTickCount = 0;
 				}
+				AllPackets.channel.send(PacketDistributor.TRACKING_ENTITY.with(() -> sender),
+					new LimbSwingUpdatePacket(sender.getEntityId(), sender.getPositionVec(), limbSwing));
 			});
 		context.get()
 			.setPacketHandled(true);
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/LimbSwingUpdatePacket.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/LimbSwingUpdatePacket.java
new file mode 100644
index 000000000..c4f1f70a8
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/sync/LimbSwingUpdatePacket.java
@@ -0,0 +1,62 @@
+package com.simibubi.create.content.contraptions.components.structureMovement.sync;
+
+import java.util.function.Supplier;
+
+import com.simibubi.create.foundation.networking.SimplePacketBase;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.math.vector.Vector3d;
+import net.minecraftforge.fml.network.NetworkEvent.Context;
+
+public class LimbSwingUpdatePacket extends SimplePacketBase {
+
+	private int entityId;
+	private Vector3d position;
+	private float limbSwing;
+
+	public LimbSwingUpdatePacket(int entityId, Vector3d position, float limbSwing) {
+		this.entityId = entityId;
+		this.position = position;
+		this.limbSwing = limbSwing;
+	}
+
+	public LimbSwingUpdatePacket(PacketBuffer buffer) {
+		entityId = buffer.readInt();
+		position = new Vector3d(buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
+		limbSwing = buffer.readFloat();
+	}
+
+	@Override
+	public void write(PacketBuffer buffer) {
+		buffer.writeInt(entityId);
+		buffer.writeFloat((float) position.x);
+		buffer.writeFloat((float) position.y);
+		buffer.writeFloat((float) position.z);
+		buffer.writeFloat(limbSwing);
+	}
+
+	@Override
+	public void handle(Supplier<Context> context) {
+		context.get()
+			.enqueueWork(() -> {
+				ClientWorld world = Minecraft.getInstance().world;
+				if (world == null)
+					return;
+				Entity entity = world.getEntityByID(entityId);
+				if (entity == null)
+					return;
+				CompoundNBT data = entity.getPersistentData();
+				data.putInt("LastOverrideLimbSwingUpdate", 0);
+				data.putFloat("OverrideLimbSwing", limbSwing);
+				entity.setPositionAndRotationDirect(position.x, position.y, position.z, entity.rotationYaw,
+					entity.rotationPitch, 2, false);
+			});
+		context.get()
+			.setPacketHandled(true);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/FillingBySpout.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/FillingBySpout.java
index 59a3b3ada..4afe6f0d6 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/FillingBySpout.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/actors/FillingBySpout.java
@@ -24,8 +24,7 @@ public class FillingBySpout {
 
 	public static boolean canItemBeFilled(World world, ItemStack stack) {
 		wrapper.setInventorySlotContents(0, stack);
-		if (world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.FILLING.getType(), wrapper, world)
+		if (AllRecipeTypes.FILLING.find(wrapper, world)
 			.isPresent())
 			return true;
 
@@ -44,8 +43,7 @@ public class FillingBySpout {
 
 	public static int getRequiredAmountForItem(World world, ItemStack stack, FluidStack availableFluid) {
 		wrapper.setInventorySlotContents(0, stack);
-		Optional<IRecipe<RecipeWrapper>> recipe = world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.FILLING.getType(), wrapper, world);
+		Optional<IRecipe<RecipeWrapper>> recipe = AllRecipeTypes.FILLING.find(wrapper, world);
 		if (recipe.isPresent()) {
 			FillingRecipe fillingRecipe = (FillingRecipe) recipe.get();
 			FluidIngredient requiredFluid = fillingRecipe.getRequiredFluid();
@@ -71,8 +69,7 @@ public class FillingBySpout {
 		availableFluid.shrink(requiredAmount);
 
 		wrapper.setInventorySlotContents(0, stack);
-		Optional<IRecipe<RecipeWrapper>> recipe = world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.FILLING.getType(), wrapper, world);
+		Optional<IRecipe<RecipeWrapper>> recipe = AllRecipeTypes.FILLING.find(wrapper, world);
 		if (recipe.isPresent()) {
 			FillingRecipe fillingRecipe = (FillingRecipe) recipe.get();
 			FluidIngredient requiredFluid = fillingRecipe.getRequiredFluid();
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinBlock.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinBlock.java
index 5c70528bd..8268df9de 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinBlock.java
@@ -9,16 +9,23 @@ import com.simibubi.create.foundation.block.ITE;
 import com.simibubi.create.foundation.item.ItemHelper;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.filtering.FilteringBehaviour;
+import com.simibubi.create.foundation.utility.Iterate;
+import com.simibubi.create.foundation.utility.Pair;
 
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.item.ItemEntity;
 import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.BlockItemUseContext;
 import net.minecraft.item.ItemStack;
 import net.minecraft.item.ItemUseContext;
+import net.minecraft.state.DirectionProperty;
+import net.minecraft.state.StateContainer.Builder;
+import net.minecraft.state.properties.BlockStateProperties;
 import net.minecraft.tileentity.TileEntity;
 import net.minecraft.util.ActionResultType;
+import net.minecraft.util.Direction;
 import net.minecraft.util.Hand;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.BlockRayTraceResult;
@@ -26,14 +33,22 @@ import net.minecraft.util.math.shapes.ISelectionContext;
 import net.minecraft.util.math.shapes.VoxelShape;
 import net.minecraft.world.IBlockReader;
 import net.minecraft.world.World;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
+import net.minecraftforge.fluids.capability.IFluidHandler;
+import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
 import net.minecraftforge.items.IItemHandlerModifiable;
 import net.minecraftforge.items.ItemHandlerHelper;
 import net.minecraftforge.items.ItemStackHandler;
 
 public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchable {
 
+	public static final DirectionProperty FACING = BlockStateProperties.FACING_EXCEPT_UP;
+
 	public BasinBlock(Properties p_i48440_1_) {
 		super(p_i48440_1_);
+		setDefaultState(getDefaultState().with(FACING, Direction.DOWN));
 	}
 
 	@Override
@@ -41,6 +56,11 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 		return true;
 	}
 
+	@Override
+	protected void fillStateContainer(Builder<Block, BlockState> p_206840_1_) {
+		super.fillStateContainer(p_206840_1_.add(FACING));
+	}
+
 	@Override
 	public TileEntity createTileEntity(BlockState state, IBlockReader world) {
 		return AllTileEntities.BASIN.create();
@@ -54,12 +74,16 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 	@Override
 	public ActionResultType onUse(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn,
 		BlockRayTraceResult hit) {
-		if (!player.getHeldItem(handIn)
-			.isEmpty())
-			return ActionResultType.PASS;
+		ItemStack heldItem = player.getHeldItem(handIn);
 
 		try {
 			BasinTileEntity te = getTileEntity(worldIn, pos);
+			if (!heldItem.isEmpty()) {
+				if (tryEmptyItemIntoBasin(worldIn, player, handIn, heldItem, te))
+					return ActionResultType.SUCCESS;
+				return ActionResultType.PASS;
+			}
+
 			IItemHandlerModifiable inv = te.itemCapability.orElse(new ItemStackHandler(1));
 			for (int slot = 0; slot < inv.getSlots(); slot++) {
 				player.inventory.placeItemBackInInventory(worldIn, inv.getStackInSlot(slot));
@@ -72,6 +96,30 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 		return ActionResultType.SUCCESS;
 	}
 
+	protected boolean tryEmptyItemIntoBasin(World worldIn, PlayerEntity player, Hand handIn, ItemStack heldItem,
+		BasinTileEntity te) {
+		if (!EmptyingByBasin.canItemBeEmptied(worldIn, heldItem))
+			return false;
+
+		Pair<FluidStack, ItemStack> emptyItem = EmptyingByBasin.emptyItem(worldIn, heldItem, true);
+		LazyOptional<IFluidHandler> capability = te.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY);
+		IFluidHandler tank = capability.orElse(null);
+		FluidStack fluidStack = emptyItem.getFirst();
+
+		if (tank == null || fluidStack.getAmount() != tank.fill(fluidStack, FluidAction.SIMULATE))
+			return false;
+		if (worldIn.isRemote)
+			return true;
+
+		EmptyingByBasin.emptyItem(worldIn, heldItem, false);
+		tank.fill(fluidStack, FluidAction.EXECUTE);
+		if (heldItem.isEmpty())
+			player.setHeldItem(handIn, emptyItem.getSecond());
+		else
+			player.inventory.placeItemBackInInventory(worldIn, emptyItem.getSecond());
+		return true;
+	}
+
 	@Override
 	public void onLanded(IBlockReader worldIn, Entity entityIn) {
 		super.onLanded(worldIn, entityIn);
@@ -102,6 +150,11 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 		return AllShapes.BASIN_BLOCK_SHAPE;
 	}
 
+	@Override
+	public void onBlockAdded(BlockState state, World world, BlockPos pos, BlockState p_220082_4_, boolean p_220082_5_) {
+		updateDiagonalNeighbours(state, world, pos);
+	}
+
 	@Override
 	public VoxelShape getCollisionShape(BlockState state, IBlockReader reader, BlockPos pos, ISelectionContext ctx) {
 		if (ctx.getEntity() instanceof ItemEntity)
@@ -111,6 +164,7 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 
 	@Override
 	public void onReplaced(BlockState state, World worldIn, BlockPos pos, BlockState newState, boolean isMoving) {
+		updateDiagonalNeighbours(state, worldIn, pos);
 		if (!state.hasTileEntity() || state.getBlock() == newState.getBlock())
 			return;
 		TileEntityBehaviour.destroy(worldIn, pos, FilteringBehaviour.TYPE);
@@ -140,4 +194,35 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 		return BasinTileEntity.class;
 	}
 
+	@Override
+	public BlockState getStateForPlacement(BlockItemUseContext ctx) {
+		BlockState state = super.getStateForPlacement(ctx);
+		World world = ctx.getWorld();
+		BlockPos pos = ctx.getPos();
+		return updateDiagonalState(state, world, pos);
+	}
+
+	protected void updateDiagonalNeighbours(BlockState state, World world, BlockPos pos) {
+		for (Direction direction : Iterate.horizontalDirections) {
+			BlockPos toUpdate = pos.up()
+				.offset(direction);
+			BlockState stateToUpdate = world.getBlockState(toUpdate);
+			BlockState updated = updateDiagonalState(stateToUpdate, world, toUpdate);
+			if (stateToUpdate != updated && !world.isRemote)
+				world.setBlockState(toUpdate, updated);
+		}
+	}
+
+	public static BlockState updateDiagonalState(BlockState state, IBlockReader world, BlockPos pos) {
+		if (!(state.getBlock() instanceof BasinBlock))
+			return state;
+		for (Direction direction : Iterate.horizontalDirections) {
+			BlockState diagonaloutputBasin = world.getBlockState(pos.down()
+				.offset(direction));
+			if (diagonaloutputBasin.getBlock() instanceof BasinBlock)
+				return state.with(FACING, direction);
+		}
+		return state.with(FACING, Direction.DOWN);
+	}
+
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinGenerator.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinGenerator.java
new file mode 100644
index 000000000..fe96055ac
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinGenerator.java
@@ -0,0 +1,32 @@
+package com.simibubi.create.content.contraptions.processing;
+
+import com.simibubi.create.foundation.data.AssetLookup;
+import com.simibubi.create.foundation.data.SpecialBlockStateGen;
+import com.tterrag.registrate.providers.DataGenContext;
+import com.tterrag.registrate.providers.RegistrateBlockstateProvider;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraftforge.client.model.generators.ModelFile;
+
+public class BasinGenerator extends SpecialBlockStateGen {
+
+	@Override
+	protected int getXRotation(BlockState state) {
+		return 0;
+	}
+
+	@Override
+	protected int getYRotation(BlockState state) {
+		return horizontalAngle(state.get(BasinBlock.FACING));
+	}
+
+	@Override
+	public <T extends Block> ModelFile getModel(DataGenContext<Block, T> ctx, RegistrateBlockstateProvider prov,
+		BlockState state) {
+		if (state.get(BasinBlock.FACING).getAxis().isVertical())
+			return AssetLookup.partialBaseModel(ctx, prov);
+		return AssetLookup.partialBaseModel(ctx, prov, "directional");
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInputInventory.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInputInventory.java
deleted file mode 100644
index 6f105dd5e..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInputInventory.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.simibubi.create.content.contraptions.processing;
-
-import com.simibubi.create.foundation.item.SmartInventory;
-
-import net.minecraft.item.ItemStack;
-import net.minecraftforge.items.ItemHandlerHelper;
-
-public class BasinInputInventory extends SmartInventory {
-
-	public BasinInputInventory(int slots, BasinTileEntity te) {
-		super(slots, te);
-	}
-	
-	@Override
-	public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) {
-		// Only insert if no other slot already has a 'full' stack of this item
-		for (int i = 0; i < getSlots(); i++) {
-			ItemStack stackInSlot = getStackInSlot(i);
-			if (ItemHandlerHelper.canItemStacksStack(stack, stackInSlot)
-				&& stackInSlot.getCount() == getStackLimit(i, stackInSlot))
-				return stack;
-		}
-
-		return super.insertItem(slot, stack, simulate);
-	}
-
-}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInventory.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInventory.java
new file mode 100644
index 000000000..3e3ac469a
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinInventory.java
@@ -0,0 +1,23 @@
+package com.simibubi.create.content.contraptions.processing;
+
+import com.simibubi.create.foundation.item.SmartInventory;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.items.ItemHandlerHelper;
+
+public class BasinInventory extends SmartInventory {
+
+	public BasinInventory(int slots, BasinTileEntity te) {
+		super(slots, te, 16, true);
+	}
+	
+	@Override
+	public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) {
+		// Only insert if no other slot already has a stack of this item
+		for (int i = 0; i < getSlots(); i++) 
+			if (i != slot && ItemHandlerHelper.canItemStacksStack(stack, inv.getStackInSlot(i)))
+				return stack;
+		return super.insertItem(slot, stack, simulate);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinOperatingTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinOperatingTileEntity.java
index 26320f935..20383e4f4 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinOperatingTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinOperatingTileEntity.java
@@ -1,49 +1,29 @@
 package com.simibubi.create.content.contraptions.processing;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
-import com.simibubi.create.AllTileEntities;
 import com.simibubi.create.content.contraptions.base.KineticTileEntity;
 import com.simibubi.create.foundation.advancement.AllTriggers;
-import com.simibubi.create.foundation.advancement.SimpleTrigger;
-import com.simibubi.create.foundation.item.SmartInventory;
+import com.simibubi.create.foundation.advancement.ITriggerable;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.simple.DeferralBehaviour;
 import com.simibubi.create.foundation.utility.recipe.RecipeFinder;
 
 import net.minecraft.inventory.IInventory;
-import net.minecraft.item.ItemStack;
 import net.minecraft.item.crafting.IRecipe;
-import net.minecraft.item.crafting.Ingredient;
 import net.minecraft.tileentity.TileEntity;
 import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.NonNullList;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.fluids.FluidStack;
-import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
-import net.minecraftforge.fluids.capability.IFluidHandler;
-import net.minecraftforge.items.CapabilityItemHandler;
-import net.minecraftforge.items.IItemHandler;
-import net.minecraftforge.items.ItemHandlerHelper;
 
 public abstract class BasinOperatingTileEntity extends KineticTileEntity {
 
 	public DeferralBehaviour basinChecker;
 	public boolean basinRemoved;
-	protected IRecipe<?> lastRecipe;
-
-	protected LazyOptional<IItemHandler> basinItemInv = LazyOptional.empty();
-	protected List<ItemStack> itemInputs;
-	protected LazyOptional<IFluidHandler> basinFluidInv = LazyOptional.empty();
-	protected List<FluidStack> fluidInputs;
+	protected IRecipe<?> currentRecipe;
 
 	public BasinOperatingTileEntity(TileEntityType<?> typeIn) {
 		super(typeIn);
-		itemInputs = new ArrayList<>();
-		fluidInputs = new ArrayList<>();
 	}
 
 	@Override
@@ -58,29 +38,10 @@ public abstract class BasinOperatingTileEntity extends KineticTileEntity {
 		super.onSpeedChanged(prevSpeed);
 		if (getSpeed() == 0)
 			basinRemoved = true;
+		basinRemoved = false;
 		basinChecker.scheduleUpdate();
 	}
 
-	public void gatherInputs() {
-		itemInputs.clear();
-		basinItemInv.ifPresent(handler -> {
-			for (int slot = 0; slot < handler.getSlots(); ++slot) {
-				ItemStack itemstack = handler.getStackInSlot(slot);
-				if (!itemstack.isEmpty())
-					itemInputs.add(itemstack);
-			}
-		});
-
-		fluidInputs.clear();
-		basinFluidInv.ifPresent(handler -> {
-			for (int tank = 0; tank < handler.getTanks(); tank++) {
-				FluidStack fluidInTank = handler.getFluidInTank(tank);
-				if (!fluidInTank.isEmpty())
-					fluidInputs.add(fluidInTank);
-			}
-		});
-	}
-
 	@Override
 	public void tick() {
 		if (basinRemoved) {
@@ -100,28 +61,13 @@ public abstract class BasinOperatingTileEntity extends KineticTileEntity {
 			return true;
 		if (isRunning())
 			return false;
-
-		Optional<BasinTileEntity> basinTe = getBasin();
-		if (!basinTe.isPresent())
-			return true;
-		if (!basinItemInv.isPresent())
-			basinItemInv = basinTe.get()
-				.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY);
-		if (!basinFluidInv.isPresent())
-			basinFluidInv = basinTe.get()
-				.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY);
-		if (!basinFluidInv.isPresent() || !basinItemInv.isPresent())
-			return true;
-
 		if (world == null || world.isRemote)
 			return true;
 
-		gatherInputs();
 		List<IRecipe<?>> recipes = getMatchingRecipes();
 		if (recipes.isEmpty())
 			return true;
-
-		lastRecipe = recipes.get(0);
+		currentRecipe = recipes.get(0);
 		startProcessingBasin();
 		sendData();
 		return true;
@@ -135,58 +81,37 @@ public abstract class BasinOperatingTileEntity extends KineticTileEntity {
 		return true;
 	}
 
-	public void applyBasinRecipe() {
-		if (lastRecipe == null)
-			return;
-		if (!basinItemInv.isPresent() || !basinFluidInv.isPresent())
-			return;
-
+	protected <C extends IInventory> boolean matchBasinRecipe(IRecipe<C> recipe) {
+		if (recipe == null)
+			return false;
 		Optional<BasinTileEntity> basin = getBasin();
 		if (!basin.isPresent())
+			return false;
+		return BasinRecipe.match(basin.get(), recipe);
+	}
+	
+	protected void applyBasinRecipe() {
+		if (currentRecipe == null)
 			return;
-		SmartInventory inputs = basin.get().getInputInventory();
-		SmartInventory outputs = basin.get().getOutputInventory();
-		List<ItemStack> containers = new ArrayList<>();
-
-		NonNullList<Ingredient> ingredients = lastRecipe.getIngredients();
-		Ingredients: for (int i = 0; i < ingredients.size(); i++) {
-			Ingredient ingredient = ingredients.get(i);
-
-			for (int slot = 0; slot < inputs.getSlots(); slot++) {
-				if (!ingredient.test(inputs.extractItem(slot, 1, true)))
-					continue;
-				ItemStack extracted = inputs.extractItem(slot, 1, false);
-				if (extracted.hasContainerItem())
-					containers.add(extracted.getContainerItem()
-						.copy());
-				continue Ingredients;
-			}
-
-			// something wasn't found
+		
+		Optional<BasinTileEntity> optionalBasin = getBasin();
+		if (!optionalBasin.isPresent())
 			return;
-		}
-
-		if (world != null && !world.isRemote) {
-			SimpleTrigger trigger = AllTriggers.MIXER_MIX;
-			if (AllTileEntities.MECHANICAL_PRESS.is(this))
-				trigger = AllTriggers.PRESS_COMPACT;
-			AllTriggers.triggerForNearbyPlayers(trigger, world, pos, 4);
-		}
-
-		outputs.allowInsertion();
-		ItemHandlerHelper.insertItemStacked(outputs, lastRecipe.getRecipeOutput()
-			.copy(), false); // TODO only works for single item output
-		containers.forEach(stack -> ItemHandlerHelper.insertItemStacked(outputs, stack, false));
-		outputs.forbidInsertion();
-
+		BasinTileEntity basin = optionalBasin.get();
+		if (!BasinRecipe.apply(basin, currentRecipe))
+			return;
+		Optional<ITriggerable> processedRecipeTrigger = getProcessedRecipeTrigger();
+		if (world != null && !world.isRemote && processedRecipeTrigger.isPresent()) 
+			AllTriggers.triggerForNearbyPlayers(processedRecipeTrigger.get(), world, pos, 4);
+		basin.inputTank.sendDataImmediately();
+	
 		// Continue mixing
-		gatherInputs();
-		if (matchBasinRecipe(lastRecipe)) {
+		if (matchBasinRecipe(currentRecipe)) {
 			continueWithPreviousRecipe();
 			sendData();
 		}
 
-		getBasin().ifPresent(BasinTileEntity::notifyChangeOfContents);
+		basin.notifyChangeOfContents();
 	}
 
 	protected List<IRecipe<?>> getMatchingRecipes() {
@@ -210,28 +135,13 @@ public abstract class BasinOperatingTileEntity extends KineticTileEntity {
 			return Optional.empty();
 		return Optional.of((BasinTileEntity) basinTE);
 	}
+	
+	protected Optional<ITriggerable> getProcessedRecipeTrigger() {
+		return Optional.empty();
+	}
 
 	protected abstract <C extends IInventory> boolean matchStaticFilters(IRecipe<C> recipe);
 
-	protected <C extends IInventory> boolean matchBasinRecipe(IRecipe<C> recipe) {
-		if (recipe == null)
-			return false;
-
-		Optional<BasinTileEntity> basin = getBasin();
-		if (!basin.isPresent())
-			return false;
-		BasinTileEntity basinTileEntity = basin.get();
-		if (!basinTileEntity.getFilter()
-			.test(recipe.getRecipeOutput()))
-			return false;
-
-		NonNullList<Ingredient> ingredients = recipe.getIngredients();
-		if (!ingredients.stream()
-			.allMatch(ingredient -> (ingredient.isSimple() || ingredient.getMatchingStacks().length == 1)))
-			return false;
-		return true;
-	}
-
 	protected abstract Object getRecipeCacheKey();
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRecipe.java
new file mode 100644
index 000000000..6fc0a451d
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRecipe.java
@@ -0,0 +1,199 @@
+package com.simibubi.create.content.contraptions.processing;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeParams;
+import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock.HeatLevel;
+import com.simibubi.create.foundation.fluid.FluidIngredient;
+import com.simibubi.create.foundation.item.SmartInventory;
+import com.simibubi.create.foundation.tileEntity.behaviour.filtering.FilteringBehaviour;
+import com.simibubi.create.foundation.tileEntity.behaviour.fluid.SmartFluidTankBehaviour;
+import com.simibubi.create.foundation.tileEntity.behaviour.fluid.SmartFluidTankBehaviour.TankSegment;
+import com.simibubi.create.foundation.utility.Iterate;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.crafting.IRecipe;
+import net.minecraft.item.crafting.Ingredient;
+import net.minecraft.world.World;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
+import net.minecraftforge.fluids.capability.IFluidHandler;
+import net.minecraftforge.items.CapabilityItemHandler;
+import net.minecraftforge.items.IItemHandler;
+
+public class BasinRecipe extends ProcessingRecipe<SmartInventory> {
+
+	public static boolean match(BasinTileEntity basin, IRecipe<?> recipe) {
+		FilteringBehaviour filter = basin.getFilter();
+		if (filter == null || !filter.test(recipe.getRecipeOutput()))
+			return false;
+		return apply(basin, recipe, true);
+	}
+
+	public static boolean apply(BasinTileEntity basin, IRecipe<?> recipe) {
+		return apply(basin, recipe, false);
+	}
+
+	private static boolean apply(BasinTileEntity basin, IRecipe<?> recipe, boolean test) {
+		boolean isBasinRecipe = recipe instanceof BasinRecipe;
+		IItemHandler availableItems = basin.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
+			.orElse(null);
+		IFluidHandler availableFluids = basin.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY)
+			.orElse(null);
+
+		if (availableItems == null || availableFluids == null)
+			return false;
+
+		HeatLevel heat = basin.getHeatLevel();
+		if (isBasinRecipe && !((BasinRecipe) recipe).getRequiredHeat()
+			.testBlazeBurner(heat))
+			return false;
+
+		List<ItemStack> recipeOutputItems = new ArrayList<>();
+		List<FluidStack> recipeOutputFluids = new ArrayList<>();
+
+		List<Ingredient> ingredients = new LinkedList<>(recipe.getIngredients());
+		ingredients.sort(Comparator.comparingInt(i -> i.getMatchingStacks().length));
+		List<FluidIngredient> fluidIngredients =
+			isBasinRecipe ? ((BasinRecipe) recipe).getFluidIngredients() : Collections.emptyList();
+
+		for (boolean simulate : Iterate.trueAndFalse) {
+
+			if (!simulate && test)
+				return true;
+
+			int[] extractedItemsFromSlot = new int[availableItems.getSlots()];
+			int[] extractedFluidsFromTank = new int[availableFluids.getTanks()];
+
+			Ingredients: for (int i = 0; i < ingredients.size(); i++) {
+				Ingredient ingredient = ingredients.get(i);
+
+				for (int slot = 0; slot < availableItems.getSlots(); slot++) {
+					if (simulate && availableItems.getStackInSlot(slot)
+						.getCount() <= extractedItemsFromSlot[slot])
+						continue;
+					ItemStack extracted = availableItems.extractItem(slot, 1, true);
+					if (!ingredient.test(extracted))
+						continue;
+					if (!simulate)
+						availableItems.extractItem(slot, 1, false);
+					else if (extracted.hasContainerItem())
+						recipeOutputItems.add(extracted.getContainerItem()
+							.copy());
+					extractedItemsFromSlot[slot]++;
+					continue Ingredients;
+				}
+
+				// something wasn't found
+				return false;
+			}
+
+			boolean fluidsAffected = false;
+			FluidIngredients: for (int i = 0; i < fluidIngredients.size(); i++) {
+				FluidIngredient fluidIngredient = fluidIngredients.get(i);
+				int amountRequired = fluidIngredient.getRequiredAmount();
+
+				for (int tank = 0; tank < availableFluids.getTanks(); tank++) {
+					FluidStack fluidStack = availableFluids.getFluidInTank(tank);
+					if (simulate && fluidStack.getAmount() <= extractedFluidsFromTank[tank])
+						continue;
+					if (!fluidIngredient.test(fluidStack))
+						continue;
+					int drainedAmount = Math.min(amountRequired, fluidStack.getAmount());
+					if (!simulate) {
+						fluidStack.shrink(drainedAmount);
+						fluidsAffected = true;
+					}
+					amountRequired -= drainedAmount;
+					if (amountRequired != 0)
+						continue;
+					extractedFluidsFromTank[tank] += drainedAmount;
+					continue FluidIngredients;
+				}
+
+				// something wasn't found
+				return false;
+			}
+
+			if (fluidsAffected) {
+				basin.getBehaviour(SmartFluidTankBehaviour.INPUT)
+					.foreach(TankSegment::onFluidStackChanged);
+				basin.getBehaviour(SmartFluidTankBehaviour.OUTPUT)
+					.foreach(TankSegment::onFluidStackChanged);
+			}
+
+			if (simulate) {
+				if (recipe instanceof BasinRecipe) {
+					recipeOutputItems.addAll(((BasinRecipe) recipe).rollResults());
+					recipeOutputFluids.addAll(((BasinRecipe) recipe).getFluidResults());
+				} else
+					recipeOutputItems.add(recipe.getRecipeOutput());
+			}
+
+			if (!basin.acceptOutputs(recipeOutputItems, recipeOutputFluids, simulate))
+				return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * For JEI purposes only
+	 */
+	public boolean convertedRecipe;
+	
+	public static BasinRecipe convert(IRecipe<?> recipe) {
+		BasinRecipe basinRecipe = new ProcessingRecipeBuilder<>(BasinRecipe::new, recipe.getId())
+			.withItemIngredients(recipe.getIngredients())
+			.withSingleItemOutput(recipe.getRecipeOutput())
+			.build();
+		basinRecipe.convertedRecipe = true;
+		return basinRecipe;
+	}
+	
+	protected BasinRecipe(AllRecipeTypes type, ProcessingRecipeParams params) {
+		super(type, params);
+	}
+
+	public BasinRecipe(ProcessingRecipeParams params) {
+		this(AllRecipeTypes.BASIN, params);
+	}
+
+	@Override
+	protected int getMaxInputCount() {
+		return 9;
+	}
+
+	@Override
+	protected int getMaxOutputCount() {
+		return 4;
+	}
+
+	@Override
+	protected int getMaxFluidInputCount() {
+		return 2;
+	}
+
+	@Override
+	protected int getMaxFluidOutputCount() {
+		return 2;
+	}
+
+	@Override
+	protected boolean canRequireHeat() {
+		return true;
+	}
+
+	@Override
+	public boolean matches(SmartInventory inv, @Nonnull World worldIn) {
+		return false;
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRenderer.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRenderer.java
index 73f93d8d0..3948d220e 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRenderer.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinRenderer.java
@@ -79,16 +79,19 @@ public class BasinRenderer extends SmartTileEntityRenderer<BasinTileEntity> {
 				if (tankSegment.getRenderedFluid()
 					.isEmpty())
 					continue;
-				totalUnits += tankSegment.getTotalUnits(partialTicks);
+				float units = tankSegment.getTotalUnits(partialTicks);
+				if (units < 1)
+					continue;
+				totalUnits += units;
 				renderedFluids++;
 			}
 		}
 
 		if (renderedFluids == 0)
 			return 0;
-		if (totalUnits == 0)
+		if (totalUnits < 1)
 			return 0;
-		
+
 		float fluidLevel = MathHelper.clamp(totalUnits / 2000, 0, 1);
 
 		float xMin = 2 / 16f;
@@ -105,8 +108,11 @@ public class BasinRenderer extends SmartTileEntityRenderer<BasinTileEntity> {
 				FluidStack renderedFluid = tankSegment.getRenderedFluid();
 				if (renderedFluid.isEmpty())
 					continue;
-
-				float partial = tankSegment.getTotalUnits(partialTicks) / totalUnits;
+				float units = tankSegment.getTotalUnits(partialTicks);
+				if (units < 1)
+					continue;
+				
+				float partial = units / totalUnits;
 				xMax += partial * 12 / 16f;
 				FluidRenderer.renderTiledFluidBB(renderedFluid, xMin, yMin, zMin, xMax, yMax, zMax, buffer, ms, light,
 					false);
@@ -114,7 +120,7 @@ public class BasinRenderer extends SmartTileEntityRenderer<BasinTileEntity> {
 				xMin = xMax;
 			}
 		}
-		
+
 		return fluidLevel;
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinTileEntity.java
index 6da0b979b..1668d5161 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/processing/BasinTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/BasinTileEntity.java
@@ -5,6 +5,9 @@ import java.util.Optional;
 
 import javax.annotation.Nonnull;
 
+import com.simibubi.create.AllTags;
+import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock;
+import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock.HeatLevel;
 import com.simibubi.create.foundation.fluid.CombinedTankWrapper;
 import com.simibubi.create.foundation.item.SmartInventory;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
@@ -13,30 +16,37 @@ import com.simibubi.create.foundation.tileEntity.behaviour.ValueBoxTransform;
 import com.simibubi.create.foundation.tileEntity.behaviour.belt.DirectBeltInputBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.filtering.FilteringBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.fluid.SmartFluidTankBehaviour;
+import com.simibubi.create.foundation.utility.BlockHelper;
+import com.simibubi.create.foundation.utility.Iterate;
 import com.simibubi.create.foundation.utility.VecHelper;
 
 import net.minecraft.block.BlockState;
+import net.minecraft.item.ItemStack;
 import net.minecraft.nbt.CompoundNBT;
 import net.minecraft.tileentity.ITickableTileEntity;
 import net.minecraft.tileentity.TileEntity;
 import net.minecraft.tileentity.TileEntityType;
 import net.minecraft.util.Direction;
 import net.minecraft.util.math.vector.Vector3d;
+import net.minecraft.util.math.BlockPos;
 import net.minecraftforge.api.distmarker.Dist;
 import net.minecraftforge.api.distmarker.OnlyIn;
 import net.minecraftforge.common.capabilities.Capability;
 import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.fluids.FluidStack;
 import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
 import net.minecraftforge.fluids.capability.IFluidHandler;
+import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
 import net.minecraftforge.items.CapabilityItemHandler;
+import net.minecraftforge.items.IItemHandler;
 import net.minecraftforge.items.IItemHandlerModifiable;
+import net.minecraftforge.items.ItemHandlerHelper;
 import net.minecraftforge.items.wrapper.CombinedInvWrapper;
 
 public class BasinTileEntity extends SmartTileEntity implements ITickableTileEntity {
 
-	public BasinInputInventory inputInventory;
+	public BasinInventory inputInventory;
 	public SmartFluidTankBehaviour inputTank;
-
 	protected SmartInventory outputInventory;
 	protected SmartFluidTankBehaviour outputTank;
 
@@ -48,12 +58,11 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 
 	public BasinTileEntity(TileEntityType<? extends BasinTileEntity> type) {
 		super(type);
-		inputInventory = new BasinInputInventory(9, this);
-		inputInventory.withMaxStackSize(8)
-			.forbidExtraction();
-		outputInventory = new SmartInventory(9, this).forbidInsertion();
-		itemCapability = LazyOptional.of(() -> new CombinedInvWrapper(inputInventory, outputInventory));
+		inputInventory = new BasinInventory(9, this);
+		inputInventory.whenContentsChanged(() -> contentsChanged = true);
+		outputInventory = new BasinInventory(9, this).forbidInsertion();
 
+		itemCapability = LazyOptional.of(() -> new CombinedInvWrapper(inputInventory, outputInventory));
 		contentsChanged = true;
 	}
 
@@ -65,7 +74,8 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 			.forRecipes();
 		behaviours.add(filtering);
 
-		inputTank = new SmartFluidTankBehaviour(SmartFluidTankBehaviour.INPUT, this, 2, 1000, true).forbidExtraction();
+		inputTank = new SmartFluidTankBehaviour(SmartFluidTankBehaviour.INPUT, this, 2, 1000, true)
+			.whenFluidUpdates(() -> contentsChanged = true);
 		outputTank = new SmartFluidTankBehaviour(SmartFluidTankBehaviour.OUTPUT, this, 2, 1000, true).forbidInsertion();
 		behaviours.add(inputTank);
 		behaviours.add(outputTank);
@@ -112,6 +122,16 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 		return super.getCapability(cap, side);
 	}
 
+	@Override
+	public void notifyUpdate() {
+		super.notifyUpdate();
+	}
+
+	@Override
+	public void lazyTick() {
+		super.lazyTick();
+	}
+
 	@Override
 	public void tick() {
 		super.tick();
@@ -119,6 +139,17 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 			return;
 		contentsChanged = false;
 		getOperator().ifPresent(te -> te.basinChecker.scheduleUpdate());
+
+		for (Direction offset : Iterate.horizontalDirections) {
+			BlockPos toUpdate = pos.up()
+				.offset(offset);
+			BlockState stateToUpdate = world.getBlockState(toUpdate);
+			if (stateToUpdate.getBlock() instanceof BasinBlock && stateToUpdate.get(BasinBlock.FACING) == offset.getOpposite()) {
+				TileEntity te = world.getTileEntity(toUpdate);
+				if (te instanceof BasinTileEntity)
+					((BasinTileEntity) te).contentsChanged = true;
+			}
+		}
 	}
 
 	private Optional<BasinOperatingTileEntity> getOperator() {
@@ -152,11 +183,64 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 		return 256;
 	}
 
+	public boolean acceptOutputs(List<ItemStack> outputItems, List<FluidStack> outputFluids, boolean simulate) {
+		outputInventory.allowInsertion();
+		outputTank.allowInsertion();
+		boolean acceptOutputsInner = acceptOutputsInner(outputItems, outputFluids, simulate);
+		outputInventory.forbidInsertion();
+		outputTank.forbidInsertion();
+		return acceptOutputsInner;
+	}
+
+	private boolean acceptOutputsInner(List<ItemStack> outputItems, List<FluidStack> outputFluids, boolean simulate) {
+		BlockState blockState = getBlockState();
+		if (!(blockState.getBlock() instanceof BasinBlock))
+			return false;
+		Direction direction = blockState.get(BasinBlock.FACING);
+
+		IItemHandler targetInv = null;
+		IFluidHandler targetTank = null;
+
+		if (direction == Direction.DOWN) {
+			// No output basin, gather locally
+			targetInv = outputInventory;
+			targetTank = outputTank.getCapability()
+				.orElse(null);
+
+		} else {
+			// Output basin, try moving items to it
+			TileEntity te = world.getTileEntity(pos.down()
+				.offset(direction));
+			if (!(te instanceof BasinTileEntity))
+				return false;
+			targetInv = te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
+				.orElse(null);
+			targetTank = te.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY)
+				.orElse(null);
+		}
+
+		if (targetInv == null)
+			return false;
+		for (ItemStack itemStack : outputItems)
+			if (!ItemHandlerHelper.insertItemStacked(targetInv, itemStack.copy(), simulate)
+				.isEmpty())
+				return false;
+
+		if (targetTank == null)
+			return false;
+		for (FluidStack fluidStack : outputFluids)
+			if (targetTank.fill(fluidStack.copy(), simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE) != fluidStack
+				.getAmount())
+				return false;
+
+		return true;
+	}
+
 	class BasinValueBox extends ValueBoxTransform.Sided {
 
 		@Override
 		protected Vector3d getSouthLocation() {
-			return VecHelper.voxelSpace(8, 12, 16);
+			return VecHelper.voxelSpace(8, 12, 15.75);
 		}
 
 		@Override
@@ -171,4 +255,11 @@ public class BasinTileEntity extends SmartTileEntity implements ITickableTileEnt
 		inputInventory.deserializeNBT(compound.getCompound("InputItems"));
 		outputInventory.deserializeNBT(compound.getCompound("OutputItems"));
 	}
+
+	public HeatLevel getHeatLevel() {
+		BlockState state = world.getBlockState(pos.down(1));
+		if (BlockHelper.hasBlockStateProperty(state, BlazeBurnerBlock.HEAT_LEVEL))
+			return state.get(BlazeBurnerBlock.HEAT_LEVEL);
+		return AllTags.AllBlockTags.FAN_HEATERS.matches(state) ? HeatLevel.SMOULDERING : HeatLevel.NONE;
+	}
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingByBasin.java b/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingByBasin.java
new file mode 100644
index 000000000..041647651
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingByBasin.java
@@ -0,0 +1,75 @@
+package com.simibubi.create.content.contraptions.processing;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.foundation.utility.Pair;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.crafting.IRecipe;
+import net.minecraft.world.World;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
+import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
+import net.minecraftforge.fluids.capability.IFluidHandlerItem;
+import net.minecraftforge.items.ItemStackHandler;
+import net.minecraftforge.items.wrapper.RecipeWrapper;
+
+public class EmptyingByBasin {
+
+	static RecipeWrapper wrapper = new RecipeWrapper(new ItemStackHandler(1));
+
+	public static boolean canItemBeEmptied(World world, ItemStack stack) {
+		wrapper.setInventorySlotContents(0, stack);
+		if (AllRecipeTypes.EMPTYING.find(wrapper, world)
+			.isPresent())
+			return true;
+
+		LazyOptional<IFluidHandlerItem> capability =
+			stack.getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY);
+		IFluidHandlerItem tank = capability.orElse(null);
+		if (tank == null)
+			return false;
+		for (int i = 0; i < tank.getTanks(); i++) {
+			if (tank.getFluidInTank(i)
+				.getAmount() > 0)
+				return true;
+		}
+		return false;
+	}
+
+	public static Pair<FluidStack, ItemStack> emptyItem(World world, ItemStack stack, boolean simulate) {
+		FluidStack resultingFluid = FluidStack.EMPTY;
+		ItemStack resultingItem = ItemStack.EMPTY;
+
+		wrapper.setInventorySlotContents(0, stack);
+		Optional<IRecipe<RecipeWrapper>> recipe = AllRecipeTypes.EMPTYING.find(wrapper, world);
+		if (recipe.isPresent()) {
+			EmptyingRecipe emptyingRecipe = (EmptyingRecipe) recipe.get();
+			List<ItemStack> results = emptyingRecipe.rollResults();
+			if (!simulate)
+				stack.shrink(1);
+			resultingItem = results.isEmpty() ? ItemStack.EMPTY : results.get(0);
+			resultingFluid = emptyingRecipe.getResultingFluid();
+			return Pair.of(resultingFluid, resultingItem);
+		}
+
+		ItemStack split = stack.copy();
+		split.setCount(1);
+		LazyOptional<IFluidHandlerItem> capability =
+			split.getCapability(CapabilityFluidHandler.FLUID_HANDLER_ITEM_CAPABILITY);
+		IFluidHandlerItem tank = capability.orElse(null);
+		if (tank == null)
+			return Pair.of(resultingFluid, resultingItem);
+		resultingFluid = tank.drain(1000, simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE);
+		resultingItem = tank.getContainer()
+			.copy();
+		if (!simulate)
+			stack.shrink(1);
+
+		return Pair.of(resultingFluid, resultingItem);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingRecipe.java b/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingRecipe.java
new file mode 100644
index 000000000..fbed9d886
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/processing/EmptyingRecipe.java
@@ -0,0 +1,42 @@
+package com.simibubi.create.content.contraptions.processing;
+
+import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.content.contraptions.processing.ProcessingRecipeBuilder.ProcessingRecipeParams;
+
+import net.minecraft.world.World;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.items.wrapper.RecipeWrapper;
+
+public class EmptyingRecipe extends ProcessingRecipe<RecipeWrapper> {
+
+	public EmptyingRecipe(ProcessingRecipeParams params) {
+		super(AllRecipeTypes.EMPTYING, params);
+	}
+
+	@Override
+	public boolean matches(RecipeWrapper inv, World p_77569_2_) {
+		return ingredients.get(0).test(inv.getStackInSlot(0));
+	}
+
+	@Override
+	protected int getMaxInputCount() {
+		return 1;
+	}
+
+	@Override
+	protected int getMaxOutputCount() {
+		return 1;
+	}
+	
+	@Override
+	protected int getMaxFluidOutputCount() {
+		return 1;
+	}
+	
+	public FluidStack getResultingFluid() {
+		if (fluidResults.isEmpty())
+			throw new IllegalStateException("Emptying Recipe: " + id.toString() + " has no fluid output!");
+		return fluidResults.get(0);
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/logistics/InWorldProcessing.java b/src/main/java/com/simibubi/create/content/logistics/InWorldProcessing.java
index 03c450852..817c4b963 100644
--- a/src/main/java/com/simibubi/create/content/logistics/InWorldProcessing.java
+++ b/src/main/java/com/simibubi/create/content/logistics/InWorldProcessing.java
@@ -120,8 +120,7 @@ public class InWorldProcessing {
 
 	public static boolean isWashable(ItemStack stack, World world) {
 		splashingInv.setInventorySlotContents(0, stack);
-		Optional<SplashingRecipe> recipe = world.getRecipeManager()
-			.getRecipe(AllRecipeTypes.SPLASHING.getType(), splashingInv, world);
+		Optional<SplashingRecipe> recipe = AllRecipeTypes.SPLASHING.find(splashingInv, world);
 		return recipe.isPresent();
 	}
 
@@ -176,8 +175,7 @@ public class InWorldProcessing {
 	private static List<ItemStack> process(ItemStack stack, Type type, World world) {
 		if (type == Type.SPLASHING) {
 			splashingInv.setInventorySlotContents(0, stack);
-			Optional<SplashingRecipe> recipe = world.getRecipeManager()
-				.getRecipe(AllRecipeTypes.SPLASHING.getType(), splashingInv, world);
+			Optional<SplashingRecipe> recipe = AllRecipeTypes.SPLASHING.find(splashingInv, world);
 			if (recipe.isPresent())
 				return applyRecipeOn(stack, recipe.get());
 			return null;
diff --git a/src/main/java/com/simibubi/create/content/logistics/item/filter/ItemAttribute.java b/src/main/java/com/simibubi/create/content/logistics/item/filter/ItemAttribute.java
index fe95d1d0f..ad932b87e 100644
--- a/src/main/java/com/simibubi/create/content/logistics/item/filter/ItemAttribute.java
+++ b/src/main/java/com/simibubi/create/content/logistics/item/filter/ItemAttribute.java
@@ -127,10 +127,10 @@ public interface ItemAttribute {
 			this.test = test;
 		}
 
-		private static boolean testRecipe(ItemStack s, World w, IRecipeType<? extends IRecipe<IInventory>> smelting) {
+		private static boolean testRecipe(ItemStack s, World w, IRecipeType<? extends IRecipe<IInventory>> type) {
 			RECIPE_WRAPPER.setInventorySlotContents(0, s.copy());
 			return w.getRecipeManager()
-				.getRecipe(smelting, RECIPE_WRAPPER, w)
+				.getRecipe(type, RECIPE_WRAPPER, w)
 				.isPresent();
 		}
 
diff --git a/src/main/java/com/simibubi/create/foundation/config/CKinetics.java b/src/main/java/com/simibubi/create/foundation/config/CKinetics.java
index 3f83c62f2..31a728123 100644
--- a/src/main/java/com/simibubi/create/foundation/config/CKinetics.java
+++ b/src/main/java/com/simibubi/create/foundation/config/CKinetics.java
@@ -27,6 +27,8 @@ public class CKinetics extends ConfigBase {
 	public ConfigInt maxPistonPoles = i(64, 1, "maxPistonPoles", Comments.maxPistonPoles);
 	public ConfigInt maxRopeLength = i(128, 1, "maxRopeLength", Comments.maxRopeLength);
 	public ConfigInt maxCartCouplingLength = i(32, 1, "maxCartCouplingLength", Comments.maxCartCouplingLength);
+	
+	public CStress stressValues = nested(0, CStress::new, Comments.stress);
 
 	public ConfigGroup state = group(0, "stats", Comments.stats);
 	public ConfigFloat mediumSpeed = f(30, 0, 4096, "mediumSpeed", Comments.rpm, Comments.mediumSpeed);
@@ -37,8 +39,6 @@ public class CKinetics extends ConfigBase {
 	public ConfigFloat mediumCapacity = f(128, 0, 4096, "mediumCapacity", Comments.su, Comments.mediumCapacity);
 	public ConfigFloat highCapacity = f(512, 0, 65535, "highCapacity", Comments.su, Comments.highCapacity);
 
-	public CStress stressValues = nested(0, CStress::new, Comments.stress);
-
 	@Override
 	public String getName() {
 		return "kinetics";
diff --git a/src/main/java/com/simibubi/create/foundation/config/CRecipes.java b/src/main/java/com/simibubi/create/foundation/config/CRecipes.java
new file mode 100644
index 000000000..b7eba1242
--- /dev/null
+++ b/src/main/java/com/simibubi/create/foundation/config/CRecipes.java
@@ -0,0 +1,26 @@
+package com.simibubi.create.foundation.config;
+
+public class CRecipes extends ConfigBase {
+
+	public ConfigBool allowShapelessInMixer = b(true, "allowShapelessInMixer", Comments.allowShapelessInMixer);
+	public ConfigBool allowShapedSquareInPress = b(true, "allowShapedSquareInPress", Comments.allowShapedSquareInPress);
+	public ConfigBool allowRegularCraftingInCrafter = b(true, "allowRegularCraftingInCrafter", Comments.allowRegularCraftingInCrafter);
+	public ConfigBool allowStonecuttingOnSaw = b(true, "allowStonecuttingOnSaw", Comments.allowStonecuttingOnSaw);
+
+	@Override
+	public String getName() {
+		return "recipes";
+	}
+
+	private static class Comments {
+		static String allowShapelessInMixer =
+			"When true, allows any shapeless crafting recipes to be processed by a Mechanical Mixer + Basin.";
+		static String allowShapedSquareInPress =
+			"When true, allows any single-ingredient 2x2 or 3x3 crafting recipes to be processed by a Mechanical Press + Basin.";
+		static String allowRegularCraftingInCrafter =
+			"When true, allows any standard crafting recipes to be processed by Mechanical Crafters.";
+		static String allowStonecuttingOnSaw =
+			"When true, allows any stonecutting recipes to be processed by a Mechanical Saw.";
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/foundation/config/CServer.java b/src/main/java/com/simibubi/create/foundation/config/CServer.java
index 5534e8df7..8ce3783bd 100644
--- a/src/main/java/com/simibubi/create/foundation/config/CServer.java
+++ b/src/main/java/com/simibubi/create/foundation/config/CServer.java
@@ -6,6 +6,7 @@ public class CServer extends ConfigBase {
 	public ConfigInt tickrateSyncTimer =
 		i(20, 5, "tickrateSyncTimer", "[in Ticks]", Comments.tickrateSyncTimer, Comments.tickrateSyncTimer2);
 
+	public CRecipes recipes = nested(0, CRecipes::new, Comments.recipes);
 	public CKinetics kinetics = nested(0, CKinetics::new, Comments.kinetics);
 	public CFluids fluids = nested(0, CFluids::new, Comments.fluids);
 	public CLogistics logistics = nested(0, CLogistics::new, Comments.logistics);
@@ -19,6 +20,7 @@ public class CServer extends ConfigBase {
 	}
 
 	private static class Comments {
+		static String recipes = "Packmakers' control panel for internal recipe compat";
 		static String schematics = "Everything related to Schematic tools";
 		static String kinetics = "Parameters and abilities of Create's kinetic mechanisms";
 		static String fluids = "Create's liquid manipulation tools";
diff --git a/src/main/java/com/simibubi/create/foundation/data/recipe/CompactingRecipeGen.java b/src/main/java/com/simibubi/create/foundation/data/recipe/CompactingRecipeGen.java
new file mode 100644
index 000000000..97e135718
--- /dev/null
+++ b/src/main/java/com/simibubi/create/foundation/data/recipe/CompactingRecipeGen.java
@@ -0,0 +1,35 @@
+package com.simibubi.create.foundation.data.recipe;
+
+import com.simibubi.create.AllRecipeTypes;
+import com.simibubi.create.content.palettes.AllPaletteBlocks;
+
+import net.minecraft.data.DataGenerator;
+import net.minecraft.fluid.Fluids;
+import net.minecraft.item.Items;
+import net.minecraft.tags.FluidTags;
+
+public class CompactingRecipeGen extends ProcessingRecipeGen {
+
+	GeneratedRecipe
+
+	TEMPGABBRO = create("temp_gabbro", b -> b
+		.require(Items.COBBLESTONE)
+		.require(FluidTags.LAVA, 250)
+		.output(AllPaletteBlocks.GABBRO.get(), 1)),
+	
+	ICE = create("ice", b -> b
+		.require(Items.ICE)
+		.output(Fluids.WATER, 250))
+
+	;
+
+	public CompactingRecipeGen(DataGenerator p_i48262_1_) {
+		super(p_i48262_1_);
+	}
+
+	@Override
+	protected AllRecipeTypes getRecipeType() {
+		return AllRecipeTypes.COMPACTING;
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/foundation/data/recipe/EmptyingRecipeGen.java b/src/main/java/com/simibubi/create/foundation/data/recipe/EmptyingRecipeGen.java
new file mode 100644
index 000000000..9106e54a8
--- /dev/null
+++ b/src/main/java/com/simibubi/create/foundation/data/recipe/EmptyingRecipeGen.java
@@ -0,0 +1,33 @@
+package com.simibubi.create.foundation.data.recipe;
+
+import com.simibubi.create.AllRecipeTypes;
+
+import net.minecraft.data.DataGenerator;
+import net.minecraft.fluid.Fluids;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.potion.PotionUtils;
+import net.minecraft.potion.Potions;
+import net.minecraftforge.common.crafting.NBTIngredient;
+
+public class EmptyingRecipeGen extends ProcessingRecipeGen {
+
+	GeneratedRecipe
+
+	WATER_BOTTLE = create("water_bottle", b -> b
+		.require(NBTIngredient.fromStacks(PotionUtils.addPotionToItemStack(new ItemStack(Items.POTION), Potions.WATER)))
+		.output(Fluids.WATER, 250)
+		.output(Items.GLASS_BOTTLE))
+
+	;
+
+	public EmptyingRecipeGen(DataGenerator p_i48262_1_) {
+		super(p_i48262_1_);
+	}
+
+	@Override
+	protected AllRecipeTypes getRecipeType() {
+		return AllRecipeTypes.EMPTYING;
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/foundation/data/recipe/MixingRecipeGen.java b/src/main/java/com/simibubi/create/foundation/data/recipe/MixingRecipeGen.java
index 524846b60..31369b8b9 100644
--- a/src/main/java/com/simibubi/create/foundation/data/recipe/MixingRecipeGen.java
+++ b/src/main/java/com/simibubi/create/foundation/data/recipe/MixingRecipeGen.java
@@ -8,6 +8,7 @@ import com.simibubi.create.content.contraptions.processing.HeatCondition;
 import net.minecraft.block.Blocks;
 import net.minecraft.data.DataGenerator;
 import net.minecraft.item.Items;
+import net.minecraft.tags.FluidTags;
 import net.minecraft.tags.ItemTags;
 import net.minecraftforge.common.Tags;
 
@@ -15,6 +16,11 @@ public class MixingRecipeGen extends ProcessingRecipeGen {
 
 	GeneratedRecipe
 
+	TEMPCOBBLE = create("temp_cobble", b -> b
+		.require(FluidTags.WATER, 250)
+		.require(FluidTags.LAVA, 250)
+		.output(Blocks.COBBLESTONE, 1)),
+	
 	BRASS_INGOT = create("brass_ingot", b -> b.require(I.copper())
 		.require(I.zinc())
 		.output(AllItems.BRASS_INGOT.get(), 2)
diff --git a/src/main/java/com/simibubi/create/foundation/data/recipe/ProcessingRecipeGen.java b/src/main/java/com/simibubi/create/foundation/data/recipe/ProcessingRecipeGen.java
index a5daca809..094d59133 100644
--- a/src/main/java/com/simibubi/create/foundation/data/recipe/ProcessingRecipeGen.java
+++ b/src/main/java/com/simibubi/create/foundation/data/recipe/ProcessingRecipeGen.java
@@ -32,8 +32,10 @@ public abstract class ProcessingRecipeGen extends CreateRecipeProvider {
 		generators.add(new WashingRecipeGen(gen));
 		generators.add(new PolishingRecipeGen(gen));
 		generators.add(new MixingRecipeGen(gen));
+		generators.add(new CompactingRecipeGen(gen));
 		generators.add(new PressingRecipeGen(gen));
 		generators.add(new FillingRecipeGen(gen));
+		generators.add(new EmptyingRecipeGen(gen));
 		
 		gen.addProvider(new IDataProvider() {
 			
diff --git a/src/main/java/com/simibubi/create/foundation/data/recipe/StandardRecipeGen.java b/src/main/java/com/simibubi/create/foundation/data/recipe/StandardRecipeGen.java
index feb359af9..7ac1faa92 100644
--- a/src/main/java/com/simibubi/create/foundation/data/recipe/StandardRecipeGen.java
+++ b/src/main/java/com/simibubi/create/foundation/data/recipe/StandardRecipeGen.java
@@ -201,9 +201,9 @@ public class StandardRecipeGen extends CreateRecipeProvider {
 				.key('A', Tags.Items.NUGGETS_IRON)
 				.patternLine("ASA")),
 
-		ATTRIBUTE_FILTER = create(AllItems.ATTRIBUTE_FILTER).unlockedBy(I::andesite)
+		ATTRIBUTE_FILTER = create(AllItems.ATTRIBUTE_FILTER).unlockedByTag(I::brass)
 			.viaShaped(b -> b.key('S', ItemTags.WOOL)
-				.key('A', I.copperNugget())
+				.key('A', I.brassNugget())
 				.patternLine("ASA")),
 
 		BRASS_HAND = create(AllItems.BRASS_HAND).unlockedByTag(I::brass)
diff --git a/src/main/java/com/simibubi/create/foundation/fluid/CombinedTankWrapper.java b/src/main/java/com/simibubi/create/foundation/fluid/CombinedTankWrapper.java
index 2a5bb5661..67fce3854 100644
--- a/src/main/java/com/simibubi/create/foundation/fluid/CombinedTankWrapper.java
+++ b/src/main/java/com/simibubi/create/foundation/fluid/CombinedTankWrapper.java
@@ -62,6 +62,9 @@ public class CombinedTankWrapper implements IFluidHandler {
 
 	@Override
 	public int fill(FluidStack resource, FluidAction action) {
+		if (resource.isEmpty())
+			return 0;
+		
 		int filled = 0;
 		resource = resource.copy();
 
@@ -85,6 +88,9 @@ public class CombinedTankWrapper implements IFluidHandler {
 
 	@Override
 	public FluidStack drain(FluidStack resource, FluidAction action) {
+		if (resource.isEmpty())
+			return resource;
+		
 		FluidStack drained = FluidStack.EMPTY;
 		resource = resource.copy();
 
diff --git a/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java b/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java
index 774927b19..581e9c087 100644
--- a/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java
+++ b/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java
@@ -74,6 +74,9 @@ public class FluidHelper {
 		int amount = JSONUtils.getInt(json, "amount");
 		FluidStack stack = new FluidStack(fluid, amount);
 
+		if (!json.has("nbt")) 
+			return stack;
+			
 		try {
 			JsonElement element = json.get("nbt");
 			stack.setTag(JsonToNBT.getTagFromJson(
diff --git a/src/main/java/com/simibubi/create/foundation/item/SmartInventory.java b/src/main/java/com/simibubi/create/foundation/item/SmartInventory.java
index 7e5050848..19773a317 100644
--- a/src/main/java/com/simibubi/create/foundation/item/SmartInventory.java
+++ b/src/main/java/com/simibubi/create/foundation/item/SmartInventory.java
@@ -10,17 +10,29 @@ import net.minecraftforge.common.util.INBTSerializable;
 import net.minecraftforge.items.ItemStackHandler;
 import net.minecraftforge.items.wrapper.RecipeWrapper;
 
-public class SmartInventory extends RecipeWrapper implements IItemHandlerModifiableIntermediate, INBTSerializable<CompoundNBT> {
+public class SmartInventory extends RecipeWrapper
+	implements IItemHandlerModifiableIntermediate, INBTSerializable<CompoundNBT> {
 
-	private boolean extractionAllowed;
-	private boolean insertionAllowed;
-	private int stackSize;
+	protected boolean extractionAllowed;
+	protected boolean insertionAllowed;
+	protected boolean stackNonStackables;
+	protected int stackSize;
 
 	public SmartInventory(int slots, SyncedTileEntity te) {
-		super(new SyncedStackHandler(slots, te));
+		this(slots, te, 64, false);
+	}
+
+	public SmartInventory(int slots, SyncedTileEntity te, int stackSize, boolean stackNonStackables) {
+		super(new SyncedStackHandler(slots, te, stackNonStackables, stackSize));
+		this.stackNonStackables = stackNonStackables;
 		insertionAllowed = true;
 		extractionAllowed = true;
-		stackSize = 64;
+		this.stackSize = stackSize;
+	}
+
+	public SmartInventory whenContentsChanged(Runnable updateCallback) {
+		((SyncedStackHandler) inv).whenContentsChange(updateCallback);
+		return this;
 	}
 
 	public SmartInventory allowInsertion() {
@@ -43,11 +55,6 @@ public class SmartInventory extends RecipeWrapper implements IItemHandlerModifia
 		return this;
 	}
 
-	public SmartInventory withMaxStackSize(int stackSize) {
-		this.stackSize = stackSize;
-		return this;
-	}
-
 	@Override
 	public int getSlots() {
 		return inv.getSlots();
@@ -64,6 +71,11 @@ public class SmartInventory extends RecipeWrapper implements IItemHandlerModifia
 	public ItemStack extractItem(int slot, int amount, boolean simulate) {
 		if (!extractionAllowed)
 			return ItemStack.EMPTY;
+		if (stackNonStackables) {
+			ItemStack extractItem = inv.extractItem(slot, amount, true);
+			if (!extractItem.isEmpty() && extractItem.getMaxStackSize() < extractItem.getCount())
+				amount = extractItem.getMaxStackSize();
+		}
 		return inv.extractItem(slot, amount, simulate);
 	}
 
@@ -86,7 +98,7 @@ public class SmartInventory extends RecipeWrapper implements IItemHandlerModifia
 	public ItemStack getStackInSlot(int slot) {
 		return super.getStackInSlot(slot);
 	}
-	
+
 	public int getStackLimit(int slot, @Nonnull ItemStack stack) {
 		return Math.min(getSlotLimit(slot), stack.getMaxStackSize());
 	}
@@ -100,7 +112,7 @@ public class SmartInventory extends RecipeWrapper implements IItemHandlerModifia
 	public void deserializeNBT(CompoundNBT nbt) {
 		getInv().deserializeNBT(nbt);
 	}
-	
+
 	private SyncedStackHandler getInv() {
 		return (SyncedStackHandler) inv;
 	}
@@ -108,18 +120,34 @@ public class SmartInventory extends RecipeWrapper implements IItemHandlerModifia
 	private static class SyncedStackHandler extends ItemStackHandler {
 
 		private SyncedTileEntity te;
+		private boolean stackNonStackables;
+		private int stackSize;
+		private Runnable updateCallback;
 
-		public SyncedStackHandler(int slots, SyncedTileEntity te) {
+		public SyncedStackHandler(int slots, SyncedTileEntity te, boolean stackNonStackables, int stackSize) {
 			super(slots);
 			this.te = te;
+			this.stackNonStackables = stackNonStackables;
+			this.stackSize = stackSize;
 		}
 
 		@Override
 		protected void onContentsChanged(int slot) {
 			super.onContentsChanged(slot);
+			if (updateCallback != null)
+				updateCallback.run();
 			te.notifyUpdate();
 		}
 
+		@Override
+		public int getSlotLimit(int slot) {
+			return Math.min(stackNonStackables ? 64 : super.getSlotLimit(slot), stackSize);
+		}
+
+		public void whenContentsChange(Runnable updateCallback) {
+			this.updateCallback = updateCallback;
+		}
+
 	}
 
 	@Override
diff --git a/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java b/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java
index ed8de400c..b1e6a8a25 100644
--- a/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java
+++ b/src/main/java/com/simibubi/create/foundation/networking/AllPackets.java
@@ -10,6 +10,7 @@ import com.simibubi.create.content.contraptions.components.structureMovement.glu
 import com.simibubi.create.content.contraptions.components.structureMovement.sync.ClientMotionPacket;
 import com.simibubi.create.content.contraptions.components.structureMovement.sync.ContraptionInteractionPacket;
 import com.simibubi.create.content.contraptions.components.structureMovement.sync.ContraptionSeatMappingPacket;
+import com.simibubi.create.content.contraptions.components.structureMovement.sync.LimbSwingUpdatePacket;
 import com.simibubi.create.content.contraptions.components.structureMovement.train.MinecartCouplingCreationPacket;
 import com.simibubi.create.content.contraptions.components.structureMovement.train.MinecartCouplingSyncPacket;
 import com.simibubi.create.content.contraptions.components.structureMovement.train.PersistantDataPacket;
@@ -68,6 +69,7 @@ public enum AllPackets {
 	MINECART_COUPLING_SYNC(MinecartCouplingSyncPacket.class, MinecartCouplingSyncPacket::new),
 	CONTRAPTION_SEAT_MAPPING(ContraptionSeatMappingPacket.class, ContraptionSeatMappingPacket::new),
 	PERSISTANT_DATA(PersistantDataPacket.class, PersistantDataPacket::new),
+	LIMBSWING_UPDATE(LimbSwingUpdatePacket.class, LimbSwingUpdatePacket::new),
 	
 	;
 
diff --git a/src/main/java/com/simibubi/create/foundation/tileEntity/behaviour/fluid/SmartFluidTankBehaviour.java b/src/main/java/com/simibubi/create/foundation/tileEntity/behaviour/fluid/SmartFluidTankBehaviour.java
index 810f84b60..445ab0a91 100644
--- a/src/main/java/com/simibubi/create/foundation/tileEntity/behaviour/fluid/SmartFluidTankBehaviour.java
+++ b/src/main/java/com/simibubi/create/foundation/tileEntity/behaviour/fluid/SmartFluidTankBehaviour.java
@@ -34,6 +34,7 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 	protected LazyOptional<? extends IFluidHandler> capability;
 	protected boolean extractionAllowed;
 	protected boolean insertionAllowed;
+	protected Runnable fluidUpdateCallback;
 
 	private BehaviourType<SmartFluidTankBehaviour> behaviourType;
 
@@ -55,6 +56,13 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 			handlers[i] = tankSegment.tank;
 		}
 		capability = LazyOptional.of(() -> new InternalFluidHandler(handlers, enforceVariety));
+		fluidUpdateCallback = () -> {
+		};
+	}
+
+	public SmartFluidTankBehaviour whenFluidUpdates(Runnable fluidUpdateCallback) {
+		this.fluidUpdateCallback = fluidUpdateCallback;
+		return this;
 	}
 
 	public SmartFluidTankBehaviour allowInsertion() {
@@ -76,7 +84,7 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 		extractionAllowed = false;
 		return this;
 	}
-	
+
 	@Override
 	public void initialize() {
 		super.initialize();
@@ -84,7 +92,7 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 			return;
 		foreach(ts -> {
 			ts.fluidLevel.forceNextSync();
-			ts.onFluidStackChanged(ts.tank.getFluid());
+			ts.onFluidStackChanged();
 		});
 	}
 
@@ -94,8 +102,8 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 
 		if (syncCooldown > 0) {
 			syncCooldown--;
-			if (syncCooldown == 0 && queuedSync)
-				tileEntity.sendData();
+			if (syncCooldown == 0 && queuedSync) 
+				updateFluids();
 		}
 
 		foreach(te -> {
@@ -108,7 +116,7 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 	public void sendDataImmediately() {
 		syncCooldown = 0;
 		queuedSync = false;
-		tileEntity.sendData();
+		updateFluids();
 	}
 
 	public void sendDataLazily() {
@@ -116,11 +124,16 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 			queuedSync = true;
 			return;
 		}
-		tileEntity.sendData();
+		updateFluids();
 		queuedSync = false;
 		syncCooldown = SYNC_RATE;
 	}
 
+	protected void updateFluids() {
+		fluidUpdateCallback.run();
+		tileEntity.sendData();
+	}
+
 	@Override
 	public void remove() {
 		super.remove();
@@ -174,36 +187,36 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 			index.increment();
 		});
 	}
-	
+
 	class InternalFluidHandler extends CombinedTankWrapper {
-		
+
 		public InternalFluidHandler(IFluidHandler[] handlers, boolean enforceVariety) {
 			super(handlers);
 			if (enforceVariety)
 				enforceVariety();
 		}
-		
+
 		@Override
 		public int fill(FluidStack resource, FluidAction action) {
 			if (!insertionAllowed)
 				return 0;
 			return super.fill(resource, action);
 		}
-		
+
 		@Override
 		public FluidStack drain(FluidStack resource, FluidAction action) {
 			if (!extractionAllowed)
 				return FluidStack.EMPTY;
 			return super.drain(resource, action);
 		}
-		
+
 		@Override
 		public FluidStack drain(int maxDrain, FluidAction action) {
 			if (!extractionAllowed)
 				return FluidStack.EMPTY;
 			return super.drain(maxDrain, action);
 		}
-		
+
 	}
 
 	public class TankSegment {
@@ -213,14 +226,14 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 		protected FluidStack renderedFluid;
 
 		public TankSegment(int capacity) {
-			tank = new SmartFluidTank(1000, f -> onFluidStackChanged(f));
+			tank = new SmartFluidTank(1000, f -> onFluidStackChanged());
 			fluidLevel = LerpedFloat.linear()
 				.startWithValue(0)
 				.chase(0, .25, Chaser.EXP);
 			renderedFluid = FluidStack.EMPTY;
 		}
 
-		protected void onFluidStackChanged(FluidStack newFluidStack) {
+		public void onFluidStackChanged() {
 			if (!tileEntity.hasWorld())
 				return;
 			fluidLevel.chase(tank.getFluidAmount() / (float) tank.getCapacity(), .25, Chaser.EXP);
@@ -235,7 +248,7 @@ public class SmartFluidTankBehaviour extends TileEntityBehaviour {
 		public LerpedFloat getFluidLevel() {
 			return fluidLevel;
 		}
-		
+
 		public float getTotalUnits(float partialTicks) {
 			return fluidLevel.getValue(partialTicks) * tank.getCapacity();
 		}
diff --git a/src/main/resources/assets/create/models/block/basin/basin_canal.bbmodel b/src/main/resources/assets/create/models/block/basin/basin_canal.bbmodel
new file mode 100644
index 000000000..63122598a
--- /dev/null
+++ b/src/main/resources/assets/create/models/block/basin/basin_canal.bbmodel
@@ -0,0 +1 @@
+{"meta":{"format_version":"3.2","model_format":"java_block","box_uv":false},"name":"basin","parent":"block/block","ambientocclusion":true,"front_gui_light":false,"resolution":{"width":16,"height":16},"elements":[{"name":"Side1","from":[0,2,0],"to":[2,16,16],"autouv":0,"color":2,"locked":false,"origin":[8,24,8],"faces":{"north":{"uv":[14,0,16,14],"texture":0},"east":{"uv":[0,0,16,14],"texture":0},"south":{"uv":[0,0,2,14],"texture":0},"west":{"uv":[0,0,16,14],"texture":0},"up":{"uv":[0,0,2,16],"texture":1},"down":{"uv":[0,0,2,16],"texture":1}},"uuid":"e372420e-1abe-a2b9-ae96-df6b128888ad"},{"name":"BasinBottom","from":[2,0,2],"to":[14,2,14],"autouv":0,"color":0,"locked":false,"origin":[8,25,8],"faces":{"north":{"uv":[2,14,14,16],"texture":0},"east":{"uv":[2,14,14,16],"texture":0},"south":{"uv":[2,14,14,16],"texture":0},"west":{"uv":[2,14,14,16],"texture":0},"up":{"uv":[2,2,14,14],"texture":1},"down":{"uv":[2,2,14,14],"texture":1}},"uuid":"e82556b1-3f66-7c35-71b2-f33c0c4c1385"},{"name":"Side4","from":[2,2,0],"to":[14,16,2],"autouv":0,"color":1,"locked":false,"origin":[8,24,8],"faces":{"north":{"uv":[2,0,14,14],"texture":0},"east":{"uv":[0,0,0,0],"texture":null},"south":{"uv":[2,0,14,14],"texture":0},"west":{"uv":[0,0,0,0],"texture":null},"up":{"uv":[2,0,14,2],"texture":1},"down":{"uv":[2,14,14,16],"texture":1}},"uuid":"2e5cfd25-7f80-2f04-e080-b005bc67fecb"},{"name":"Side2","from":[2,2,14],"to":[14,16,16],"autouv":0,"color":4,"locked":false,"origin":[8,24,8],"faces":{"north":{"uv":[2,0,14,14],"texture":0},"east":{"uv":[0,0,0,0],"texture":null},"south":{"uv":[2,0,14,14],"texture":0},"west":{"uv":[0,0,0,0],"texture":null},"up":{"uv":[2,14,14,16],"texture":1},"down":{"uv":[2,0,14,2],"texture":1}},"uuid":"e007a6de-4ee0-52db-0763-61d4c3ab8a3f"},{"name":"Side3","from":[14,2,0],"to":[16,16,16],"autouv":0,"color":5,"locked":false,"origin":[8,40,8],"faces":{"north":{"uv":[0,0,2,14],"texture":0},"east":{"uv":[0,0,16,14],"texture":0},"south":{"uv":[14,0,16,14],"texture":0},"west":{"uv":[0,0,16,14],"texture":0},"up":{"uv":[14,0,16,16],"texture":1},"down":{"uv":[14,0,16,16],"texture":1}},"uuid":"c5b067a4-6114-bb79-a5bf-1321988a497c"},{"name":"Side2","from":[0,-14,-16],"to":[2,0,0],"autouv":0,"color":2,"locked":false,"origin":[8,8,-8],"faces":{"north":{"uv":[14,0,16,14],"texture":0},"east":{"uv":[0,0,16,14],"texture":0},"south":{"uv":[0,0,2,14],"texture":0},"west":{"uv":[0,0,16,14],"texture":0},"up":{"uv":[0,0,2,16],"texture":1},"down":{"uv":[0,0,2,16],"texture":1}},"uuid":"b641f651-2872-3ebc-e9e3-2dfbbb10e8e4"},{"name":"BasinBottom","from":[2,-16,-14],"to":[14,-14,-2],"autouv":0,"color":0,"locked":false,"origin":[8,9,-8],"faces":{"north":{"uv":[2,14,14,16],"texture":0},"east":{"uv":[2,14,14,16],"texture":0},"south":{"uv":[2,14,14,16],"texture":0},"west":{"uv":[2,14,14,16],"texture":0},"up":{"uv":[2,2,14,14],"texture":1},"down":{"uv":[2,2,14,14],"texture":1}},"uuid":"07500784-c31b-dc51-7e03-f0ec55ee6028"},{"name":"Side5","from":[2,-14,-16],"to":[14,0,-14],"autouv":0,"color":1,"locked":false,"origin":[8,8,-8],"faces":{"north":{"uv":[2,0,14,14],"texture":0},"east":{"uv":[0,0,0,0],"texture":null},"south":{"uv":[2,0,14,14],"texture":0},"west":{"uv":[0,0,0,0],"texture":null},"up":{"uv":[2,0,14,2],"texture":1},"down":{"uv":[2,14,14,16],"texture":1}},"uuid":"dd76fad0-e0ed-1147-e7bd-3870f72987da"},{"name":"Side3","from":[2,-14,-2],"to":[14,0,0],"autouv":0,"color":4,"locked":false,"origin":[8,8,-8],"faces":{"north":{"uv":[2,0,14,14],"texture":0},"east":{"uv":[0,0,0,0],"texture":null},"south":{"uv":[2,0,14,14],"texture":0},"west":{"uv":[0,0,0,0],"texture":null},"up":{"uv":[2,14,14,16],"texture":1},"down":{"uv":[2,0,14,2],"texture":1}},"uuid":"32636eab-627e-34f5-7be6-2a31400bd957"},{"name":"Side4","from":[14,-14,-16],"to":[16,0,0],"autouv":0,"color":5,"locked":false,"origin":[8,24,-8],"faces":{"north":{"uv":[0,0,2,14],"texture":0},"east":{"uv":[0,0,16,14],"texture":0},"south":{"uv":[14,0,16,14],"texture":0},"west":{"uv":[0,0,16,14],"texture":0},"up":{"uv":[14,0,16,16],"texture":1},"down":{"uv":[14,0,16,16],"texture":1}},"uuid":"06344246-e61c-e8fd-c700-dae02563797e"},{"name":"cube","from":[3,0,0],"to":[4,2,2],"autouv":0,"color":6,"locked":false,"origin":[11,8,8],"faces":{"north":{"uv":[0,0,10,2],"texture":2},"east":{"uv":[14,10,16,12],"texture":2},"south":{"uv":[14,10,16,11],"rotation":90,"texture":2},"west":{"uv":[14,10,16,12],"rotation":90,"texture":2},"up":{"uv":[14,10,15,12],"rotation":180,"texture":2},"down":{"uv":[15,10,16,12],"texture":2}},"uuid":"41346be7-9b03-225c-14be-3be6e1719311"},{"name":"cube","from":[3,0,-1],"to":[4,10,0],"autouv":0,"color":6,"locked":false,"origin":[11,8,7],"faces":{"north":{"uv":[6,12,16,13],"rotation":90,"texture":2},"east":{"uv":[6,12,16,13],"rotation":90,"texture":2},"south":{"uv":[6,12,16,13],"rotation":90,"texture":2},"west":{"uv":[6,12,16,13],"rotation":90,"texture":2},"up":{"uv":[6,12,7,13],"texture":2},"down":{"uv":[15,12,16,13],"texture":2}},"uuid":"fa4b61d1-8e47-aa7d-207c-025608871716"},{"name":"cube","from":[5,1,-6],"to":[11,2,3],"autouv":0,"color":4,"locked":false,"rotation":[-22.5,0,0],"origin":[8,9,-1],"faces":{"north":{"uv":[9,7,15,8],"texture":2},"east":{"uv":[0,0,9,1],"texture":2},"south":{"uv":[9,7,15,8],"texture":2},"west":{"uv":[0,0,9,1],"texture":2},"up":{"uv":[1,0,7,9],"texture":2},"down":{"uv":[1,0,7,9],"rotation":180,"texture":2}},"uuid":"c4b10bec-d6bb-cac8-b369-d588c98601ce"},{"name":"cube","from":[4,1,-6],"to":[5,9,3],"autouv":0,"color":4,"locked":false,"rotation":[-22.5,0,0],"origin":[8,9,-1],"faces":{"north":{"uv":[15,0,16,8],"texture":2},"east":{"uv":[0,0,8,9],"rotation":90,"texture":2},"south":{"uv":[8,0,9,8],"texture":2},"west":{"uv":[0,0,8,9],"rotation":90,"texture":2},"up":{"uv":[0,0,1,9],"rotation":180,"texture":2},"down":{"uv":[0,0,1,9],"texture":2}},"uuid":"655e4f5c-68aa-c6ca-ea54-6ee2f1eb0cbf"},{"name":"cube","from":[11,1,-6],"to":[12,9,3],"autouv":0,"color":4,"locked":false,"rotation":[-22.5,0,0],"origin":[15,9,-1],"faces":{"north":{"uv":[8,0,9,8],"texture":2},"east":{"uv":[0,0,8,9],"rotation":270,"texture":2},"south":{"uv":[15,0,16,8],"texture":2},"west":{"uv":[0,0,8,9],"rotation":90,"texture":2},"up":{"uv":[0,0,1,9],"rotation":180,"texture":2},"down":{"uv":[0,0,1,9],"texture":2}},"uuid":"cf5d8d2d-6849-5958-087c-a48f1ab945e5"},{"name":"cube","from":[5,8,-6],"to":[11,9,3],"autouv":0,"color":4,"locked":false,"rotation":[-22.5,0,0],"origin":[8,9,-1],"faces":{"north":{"uv":[9,0,15,1],"texture":2},"east":{"uv":[0,0,9,1],"texture":2},"south":{"uv":[9,0,15,1],"texture":2},"west":{"uv":[0,0,9,1],"texture":2},"up":{"uv":[1,0,7,9],"rotation":180,"texture":2},"down":{"uv":[1,0,7,9],"texture":2}},"uuid":"1655c96c-ae98-f14d-527c-6052c1fab719"},{"name":"cube","from":[5,2,-4],"to":[11,8,2],"autouv":0,"color":4,"locked":false,"rotation":[-22.5,0,0],"origin":[8,9,-1],"faces":{"north":{"uv":[9,1,15,7],"texture":2},"east":{"uv":[0,0,0,0],"texture":null},"south":{"uv":[9,1,15,7],"texture":2},"west":{"uv":[0,0,0,0],"texture":null},"up":{"uv":[0,0,0,0],"texture":null},"down":{"uv":[0,0,0,0],"texture":null}},"uuid":"a2eb892a-15a7-46c2-d526-c062c9c151ba"},{"name":"cube","from":[12,0,-1],"to":[13,10,0],"autouv":0,"color":6,"locked":false,"origin":[20,8,7],"faces":{"north":{"uv":[6,12,16,13],"rotation":90,"texture":2},"east":{"uv":[6,12,16,13],"rotation":90,"texture":2},"south":{"uv":[6,12,16,13],"rotation":90,"texture":2},"west":{"uv":[6,12,16,13],"rotation":90,"texture":2},"up":{"uv":[6,12,7,13],"texture":2},"down":{"uv":[15,12,16,13],"texture":2}},"uuid":"baaf226d-53b0-814c-ca53-75621435fc45"},{"name":"cube","from":[4,9,-1],"to":[12,10,0],"autouv":0,"color":6,"locked":false,"origin":[12,8,7],"faces":{"north":{"uv":[8,9,16,10],"texture":2},"east":{"uv":[0,0,0,0],"rotation":90,"texture":null},"south":{"uv":[8,9,16,10],"texture":2},"west":{"uv":[0,0,0,0],"rotation":90,"texture":null},"up":{"uv":[8,9,16,10],"texture":2},"down":{"uv":[0,0,0,0],"texture":null}},"uuid":"7aebcd85-9ae9-b4b0-9af3-a8a96d6e3709"},{"name":"cube","from":[12,0,0],"to":[13,2,2],"autouv":0,"color":6,"locked":false,"origin":[20,8,8],"faces":{"north":{"uv":[0,0,10,2],"texture":2},"east":{"uv":[14,10,16,12],"texture":2},"south":{"uv":[14,10,16,11],"rotation":90,"texture":2},"west":{"uv":[14,10,16,12],"rotation":90,"texture":2},"up":{"uv":[14,10,15,12],"rotation":180,"texture":2},"down":{"uv":[15,10,16,12],"texture":2}},"uuid":"25e35b80-a713-fd17-aac6-033104233acd"},{"name":"cube","from":[4,0,-1],"to":[12,1,2],"autouv":0,"color":6,"locked":false,"origin":[12,8,7],"faces":{"north":{"uv":[7,15,15,16],"texture":2},"east":{"uv":[14,10,16,12],"texture":2},"south":{"uv":[8,14,9,15],"rotation":90,"texture":2},"west":{"uv":[14,10,16,12],"rotation":90,"texture":2},"up":{"uv":[11,10,12,11],"rotation":180,"texture":2},"down":{"uv":[7,13,15,16],"texture":2}},"uuid":"18b2aeec-e7d6-9262-7e3d-995f9d7320ac"}],"outliner":[{"name":"Basins","uuid":"4822b3b4-2c19-aadd-c096-85534b94f809","export":true,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"origin":[8,8,8],"children":[{"name":"Basin top","uuid":"ef36da1c-a925-5b8d-5f56-b9b2136ca98f","export":true,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"origin":[8,8,8],"children":["e372420e-1abe-a2b9-ae96-df6b128888ad","e82556b1-3f66-7c35-71b2-f33c0c4c1385","2e5cfd25-7f80-2f04-e080-b005bc67fecb","e007a6de-4ee0-52db-0763-61d4c3ab8a3f","c5b067a4-6114-bb79-a5bf-1321988a497c"]},{"name":"Basin bottom","uuid":"52077833-3587-b2ad-4a2a-8a61173f4d54","export":true,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"origin":[8,8,8],"children":["b641f651-2872-3ebc-e9e3-2dfbbb10e8e4","07500784-c31b-dc51-7e03-f0ec55ee6028","dd76fad0-e0ed-1147-e7bd-3870f72987da","32636eab-627e-34f5-7be6-2a31400bd957","06344246-e61c-e8fd-c700-dae02563797e"]}]},{"name":"Canal","uuid":"c916ca14-dcb2-5442-2b8a-1fa3ea05e167","export":true,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"origin":[8,8,8],"children":[{"name":"Frame","uuid":"1dfe7732-4e1a-7674-3237-0ecf68c5c60e","export":true,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"origin":[8,8,8],"children":["41346be7-9b03-225c-14be-3be6e1719311","25e35b80-a713-fd17-aac6-033104233acd","18b2aeec-e7d6-9262-7e3d-995f9d7320ac","fa4b61d1-8e47-aa7d-207c-025608871716","7aebcd85-9ae9-b4b0-9af3-a8a96d6e3709","baaf226d-53b0-814c-ca53-75621435fc45"]},"c4b10bec-d6bb-cac8-b369-d588c98601ce","1655c96c-ae98-f14d-527c-6052c1fab719","a2eb892a-15a7-46c2-d526-c062c9c151ba","cf5d8d2d-6849-5958-087c-a48f1ab945e5","655e4f5c-68aa-c6ca-ea54-6ee2f1eb0cbf"]}],"textures":[{"path":"C:\\Users\\krypp\\Desktop\\create 1.15.2\\assets\\create\\textures\\block\\basin_side.png","name":"basin_side.png","folder":"block","namespace":"create","id":"1","particle":false,"mode":"bitmap","saved":true,"uuid":"a20a92f6-149c-8147-a096-ebf6df449714","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB4UlEQVQ4T2VTy27bMBAcAhVF2bAlpacATr/BQFs7pzy+I0Bv6cXH9Fd8aT4p7U8UObVoTMe1JUoHFrMUJSchIOxyuTuaHXLV19tbn2gNrswYsVzjNO19OnvnZF/VdR9vmwbq5uaLH49SEISBaJkVgRnfHxyYRys/6Hx1cXHlnatRFCWqqpJD7tPUIMsyiXHPFXMYt3YjOWq5PPfGZAA88ryA66hOJtMXLXDTNK4/j8BqPp97rQ3KskQqfSsMxR5N00BrxoPPtd1a1J0WarE490VRyIExRpIJlCQJ2rZFkmi0bdPb3W4nuQSo6wrq8vLas4BtaK07FsBoNMbhcBCgJHknPtkNbXhYawlw5SkGPzKIYvHvchMdE+5ZHER2AwOKmOel9DidFkjT8CZIfWgj6Ri81UEYeB/650cBh76DDjxXircQRIwMnKugFoulL8sTOWAbQXH6ursBMtHY7/+JRhRRxFMKm80T1Le7O//w42cnYigOtxEAuEiRf2VRLKb9/Okj1Gq18tbu8Pj4S0C4yIKCRfs69vxsMZt9QFFMoOIwxTngQHFgjgeLADFGezwzKr5XAv192vZaHL/jOAvUiP77kxzf7++ltgdgK0T+/WfTD08siAKz+Gx2KlO6Xq+l9j+hVCuQux00ywAAAABJRU5ErkJggg=="},{"path":"C:\\Users\\krypp\\Desktop\\create 1.15.2\\assets\\create\\textures\\block\\basin.png","name":"basin.png","folder":"block","namespace":"create","id":"12","particle":true,"mode":"bitmap","saved":true,"uuid":"e8bd46f1-4674-7125-0c6a-15be8f476bed","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABmElEQVQ4T41TyU7DMBB9dpykS8oFKIhFwMfBpcdy4sJXcuHAIhaxHgJp3cR2jGaiJG0poiNFHsfxy7w388TZ6akPowgU3U6HV4p+HDd5nUzyvHmnZzOYooAYjUb+8fGFD8Iw5LXXbYFoP9Wz5qIxpskPDnZbgKJo0ZUKIQUAIaCURFkCZenhnIVzrqqwn2BnZxPifDz2d/dPmEwydLsdKBUgzw1fEEIgigI452Gt4XOtC863t7Yw2OgvAhAFIYA4jvhDKQW89/AenNPfCdRayxUcH+1XAG9vn0jTFKUvIYSsLvxBgconIKpgONyEuLy48LcPz0i/vhAEci0KQRBgY5AsAaQpfKXbvxSiUDGFk8O9toKpniDPC1DlURyy8iSoMQ4lbUBCWhayanV/ESDLMuiZhpSSdaDH2gJKKb5YB7W46sJ2S+Hq+obVdc6srQEBL3Th/eODJ3EdDX61sR4k4vyfBr1egiz7bieRvPD6+smTSJEkA2g9XaEBQbfRjHJtptpItZnIQPOmWmUoNtO8nZctvWzrVXb+ARkgOfimiFHjAAAAAElFTkSuQmCC"},{"path":"C:\\Users\\krypp\\Documents\\Pixel Art\\Create Mod\\basin\\Basin Canal\\Basin Canal.png","name":"Basin Canal.png","folder":"Basin Canal","namespace":"","id":"2","particle":false,"mode":"bitmap","saved":true,"uuid":"4459ee5f-e243-a1a9-b39e-fd8adcac4b69","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABc0lEQVQ4T4VTTU/CQBB9TWi3WiGIMdGYcFD4J0iFhMQDB5Mm3PDSYzlx8Q/I3+FifwrxB5B4kNja3ZKsmdElwdBlLrMfndf33sw6syTR30WBUim4ngcKsz7xfd7b7p1+/0EL4cP3fXieB6UUFxVFASkLdLt3SNN0d86XAH/b6/XgjEaPmoqD4AxlqeC6HmcCIpDrq0u8pSmklKaWsxAC9wQQRRNNB1Toui7KsuSc5zkAjWazjuVyeRBgOBzCCcOB9jzBiOYvZq2URKdzawcYj5+YgRDE4Jc+5Sz7YqrnzbpdAnnQaDRYszHRZPKg3b6xmxjHsV6t3kEyyEwqojD0qZXWNh6bg9fFYs/9/xuHrbbELEmYgRkw7tjfwBG7owBhOGB5RhatSR5l8u4oQBzHB8d8vf7gDlUCvMznyKTcM5DeSJZLBKcCqtQIAlENEEUTq3nmspIBAWw2n9hut6yXdNdqtR0onbdaF9UMnqdTpmsLkvIDiqLjULkp6X8AAAAASUVORK5CYII="}]}
\ No newline at end of file
diff --git a/src/main/resources/assets/create/models/block/basin.json b/src/main/resources/assets/create/models/block/basin/block.json
similarity index 100%
rename from src/main/resources/assets/create/models/block/basin.json
rename to src/main/resources/assets/create/models/block/basin/block.json
diff --git a/src/main/resources/assets/create/models/block/basin/block_directional.json b/src/main/resources/assets/create/models/block/basin/block_directional.json
new file mode 100644
index 000000000..6a6e9a8ba
--- /dev/null
+++ b/src/main/resources/assets/create/models/block/basin/block_directional.json
@@ -0,0 +1,237 @@
+{
+	"credit": "Made with Blockbench",
+	"parent": "block/block",
+	"textures": {
+		"1": "create:block/basin_side",
+		"2": "create:block/basin_canal",
+		"12": "create:block/basin",
+		"particle": "create:block/basin"
+	},
+	"elements": [
+		{
+			"name": "Side1",
+			"from": [14, 2, 0],
+			"to": [16, 16, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [8, 24, 8]},
+			"faces": {
+				"north": {"uv": [0, 0, 2, 14], "texture": "#1"},
+				"east": {"uv": [0, 0, 16, 14], "texture": "#1"},
+				"south": {"uv": [14, 0, 16, 14], "texture": "#1"},
+				"west": {"uv": [0, 0, 16, 14], "texture": "#1"},
+				"up": {"uv": [0, 0, 2, 16], "rotation": 180, "texture": "#12"},
+				"down": {"uv": [0, 0, 2, 16], "rotation": 180, "texture": "#12"}
+			}
+		},
+		{
+			"name": "BasinBottom",
+			"from": [2, 0, 2],
+			"to": [14, 2, 14],
+			"rotation": {"angle": 0, "axis": "y", "origin": [8, 25, 8]},
+			"faces": {
+				"north": {"uv": [2, 14, 14, 16], "texture": "#1"},
+				"east": {"uv": [2, 14, 14, 16], "texture": "#1"},
+				"south": {"uv": [2, 14, 14, 16], "texture": "#1"},
+				"west": {"uv": [2, 14, 14, 16], "texture": "#1"},
+				"up": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#12"},
+				"down": {"uv": [2, 2, 14, 14], "rotation": 180, "texture": "#12"}
+			}
+		},
+		{
+			"name": "Side4",
+			"from": [2, 2, 14],
+			"to": [14, 16, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [8, 24, 8]},
+			"faces": {
+				"north": {"uv": [2, 0, 14, 14], "texture": "#1"},
+				"south": {"uv": [2, 0, 14, 14], "texture": "#1"},
+				"up": {"uv": [2, 0, 14, 2], "rotation": 180, "texture": "#12"},
+				"down": {"uv": [2, 14, 14, 16], "rotation": 180, "texture": "#12"}
+			}
+		},
+		{
+			"name": "Side2",
+			"from": [2, 2, 0],
+			"to": [14, 16, 2],
+			"rotation": {"angle": 0, "axis": "y", "origin": [8, 24, 8]},
+			"faces": {
+				"north": {"uv": [2, 0, 14, 14], "texture": "#1"},
+				"south": {"uv": [2, 0, 14, 14], "texture": "#1"},
+				"up": {"uv": [2, 14, 14, 16], "rotation": 180, "texture": "#12"},
+				"down": {"uv": [2, 0, 14, 2], "rotation": 180, "texture": "#12"}
+			}
+		},
+		{
+			"name": "Side3",
+			"from": [0, 2, 0],
+			"to": [2, 16, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [8, 40, 8]},
+			"faces": {
+				"north": {"uv": [14, 0, 16, 14], "texture": "#1"},
+				"east": {"uv": [0, 0, 16, 14], "texture": "#1"},
+				"south": {"uv": [0, 0, 2, 14], "texture": "#1"},
+				"west": {"uv": [0, 0, 16, 14], "texture": "#1"},
+				"up": {"uv": [14, 0, 16, 16], "rotation": 180, "texture": "#12"},
+				"down": {"uv": [14, 0, 16, 16], "rotation": 180, "texture": "#12"}
+			}
+		},
+		{
+			"from": [12, 0, 14],
+			"to": [13, 2, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [5, 8, 8]},
+			"faces": {
+				"north": {"uv": [14, 10, 16, 11], "rotation": 90, "texture": "#2"},
+				"east": {"uv": [14, 10, 16, 12], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [0, 0, 10, 2], "texture": "#2"},
+				"west": {"uv": [14, 10, 16, 12], "texture": "#2"},
+				"up": {"uv": [14, 10, 15, 12], "texture": "#2"},
+				"down": {"uv": [15, 10, 16, 12], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [3, 0, 14],
+			"to": [4, 2, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [-4, 8, 8]},
+			"faces": {
+				"north": {"uv": [14, 10, 16, 11], "rotation": 90, "texture": "#2"},
+				"east": {"uv": [14, 10, 16, 12], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [0, 0, 10, 2], "texture": "#2"},
+				"west": {"uv": [14, 10, 16, 12], "texture": "#2"},
+				"up": {"uv": [14, 10, 15, 12], "texture": "#2"},
+				"down": {"uv": [15, 10, 16, 12], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [4, 0, 14],
+			"to": [12, 1, 17],
+			"rotation": {"angle": 0, "axis": "y", "origin": [4, 8, 9]},
+			"faces": {
+				"north": {"uv": [8, 14, 9, 15], "rotation": 90, "texture": "#2"},
+				"east": {"uv": [14, 10, 16, 12], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [7, 15, 15, 16], "texture": "#2"},
+				"west": {"uv": [14, 10, 16, 12], "texture": "#2"},
+				"up": {"uv": [11, 10, 12, 11], "texture": "#2"},
+				"down": {"uv": [7, 13, 15, 16], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [12, 0, 16],
+			"to": [13, 10, 17],
+			"rotation": {"angle": 0, "axis": "y", "origin": [5, 8, 9]},
+			"faces": {
+				"north": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"east": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"west": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"up": {"uv": [6, 12, 7, 13], "rotation": 180, "texture": "#2"},
+				"down": {"uv": [15, 12, 16, 13], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [4, 9, 16],
+			"to": [12, 10, 17],
+			"rotation": {"angle": 0, "axis": "y", "origin": [4, 8, 9]},
+			"faces": {
+				"north": {"uv": [8, 9, 16, 10], "texture": "#2"},
+				"south": {"uv": [8, 9, 16, 10], "texture": "#2"},
+				"up": {"uv": [8, 9, 16, 10], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [3, 0, 16],
+			"to": [4, 10, 17],
+			"rotation": {"angle": 0, "axis": "y", "origin": [-4, 8, 9]},
+			"faces": {
+				"north": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"east": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"west": {"uv": [6, 12, 16, 13], "rotation": 90, "texture": "#2"},
+				"up": {"uv": [6, 12, 7, 13], "rotation": 180, "texture": "#2"},
+				"down": {"uv": [15, 12, 16, 13], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [5, 1, 13],
+			"to": [11, 2, 22],
+			"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 9, 17]},
+			"faces": {
+				"north": {"uv": [9, 7, 15, 8], "texture": "#2"},
+				"east": {"uv": [0, 0, 9, 1], "texture": "#2"},
+				"south": {"uv": [9, 7, 15, 8], "texture": "#2"},
+				"west": {"uv": [0, 0, 9, 1], "texture": "#2"},
+				"up": {"uv": [1, 0, 7, 9], "rotation": 180, "texture": "#2"},
+				"down": {"uv": [1, 0, 7, 9], "texture": "#2"}
+			}
+		},
+		{
+			"from": [5, 8, 13],
+			"to": [11, 9, 22],
+			"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 9, 17]},
+			"faces": {
+				"north": {"uv": [9, 0, 15, 1], "texture": "#2"},
+				"east": {"uv": [0, 0, 9, 1], "texture": "#2"},
+				"south": {"uv": [9, 0, 15, 1], "texture": "#2"},
+				"west": {"uv": [0, 0, 9, 1], "texture": "#2"},
+				"up": {"uv": [1, 0, 7, 9], "texture": "#2"},
+				"down": {"uv": [1, 0, 7, 9], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [5, 2, 14],
+			"to": [11, 8, 20],
+			"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 9, 17]},
+			"faces": {
+				"north": {"uv": [9, 1, 15, 7], "texture": "#2"},
+				"south": {"uv": [9, 1, 15, 7], "texture": "#2"}
+			}
+		},
+		{
+			"from": [4, 1, 13],
+			"to": [5, 9, 22],
+			"rotation": {"angle": 22.5, "axis": "x", "origin": [1, 9, 17]},
+			"faces": {
+				"north": {"uv": [15, 0, 16, 8], "texture": "#2"},
+				"east": {"uv": [0, 0, 8, 9], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [8, 0, 9, 8], "texture": "#2"},
+				"west": {"uv": [0, 0, 8, 9], "rotation": 270, "texture": "#2"},
+				"up": {"uv": [0, 0, 1, 9], "texture": "#2"},
+				"down": {"uv": [0, 0, 1, 9], "rotation": 180, "texture": "#2"}
+			}
+		},
+		{
+			"from": [11, 1, 13],
+			"to": [12, 9, 22],
+			"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 9, 17]},
+			"faces": {
+				"north": {"uv": [8, 0, 9, 8], "texture": "#2"},
+				"east": {"uv": [0, 0, 8, 9], "rotation": 90, "texture": "#2"},
+				"south": {"uv": [15, 0, 16, 8], "texture": "#2"},
+				"west": {"uv": [0, 0, 8, 9], "rotation": 90, "texture": "#2"},
+				"up": {"uv": [0, 0, 1, 9], "texture": "#2"},
+				"down": {"uv": [0, 0, 1, 9], "rotation": 180, "texture": "#2"}
+			}
+		}
+	],
+	"groups": [
+		{
+			"name": "Basins",
+			"origin": [8, 8, 8],
+			"children": [
+				{
+					"name": "Basin top",
+					"origin": [8, 8, 8],
+					"children": [0, 1, 2, 3, 4]
+				}
+			]
+		},
+		{
+			"name": "Canal",
+			"origin": [8, 8, 8],
+			"children": [
+				{
+					"name": "Frame",
+					"origin": [8, 8, 8],
+					"children": [5, 6, 7, 8, 9, 10]
+				}, 11, 12, 13, 14, 15]
+		}
+	]
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/create/textures/block/basin_canal.png b/src/main/resources/assets/create/textures/block/basin_canal.png
new file mode 100644
index 000000000..c300a2a1b
Binary files /dev/null and b/src/main/resources/assets/create/textures/block/basin_canal.png differ
diff --git a/src/main/resources/assets/create/textures/item/logistical_filter.png b/src/main/resources/assets/create/textures/item/logistical_filter.png
deleted file mode 100644
index a2989b1db..000000000
Binary files a/src/main/resources/assets/create/textures/item/logistical_filter.png and /dev/null differ
diff --git a/src/main/resources/assets/create/textures/item/property_filter.png b/src/main/resources/assets/create/textures/item/property_filter.png
index 935c38456..dd99191d0 100644
Binary files a/src/main/resources/assets/create/textures/item/property_filter.png and b/src/main/resources/assets/create/textures/item/property_filter.png differ