diff --git a/src/generated/resources/.cache/cache b/src/generated/resources/.cache/cache
index fd814fac2..e19bdeb90 100644
--- a/src/generated/resources/.cache/cache
+++ b/src/generated/resources/.cache/cache
@@ -142,7 +142,7 @@ fc652317e03b57c76e23a805da16a28d15254029 assets/create/blockstates/fancy_scoria_
 fc9ac0a7e7191b93516719455a17177fa6524ecc assets/create/blockstates/fancy_weathered_limestone_bricks_slab.json
 b2a7c321b1795f20e7433f81a55ce4683de081b8 assets/create/blockstates/fancy_weathered_limestone_bricks_stairs.json
 8e532856c3c2b4e4e59c65a2a81a694e35d14658 assets/create/blockstates/fancy_weathered_limestone_bricks_wall.json
-3d97226b5e8d8f70ed08e45e78db1faf78d5e28b assets/create/blockstates/fluid_pipe.json
+6106fc0a0f9d83da89c3e8af98e7c45232602c23 assets/create/blockstates/fluid_pipe.json
 f0eaab18e16c4f3f65ebf3b55b08f0dc445720fe assets/create/blockstates/fluid_tank.json
 5408d92ab02af86539ac42971d4033545970bb3a assets/create/blockstates/fluid_valve.json
 e9da1794b6ece7f9aa8bcb43d42c23a55446133b assets/create/blockstates/flywheel.json
@@ -337,7 +337,7 @@ e8b0a401c10d1ba67ed71ba31bd5f9bc28571b65 assets/create/blockstates/powered_toggl
 d06cd9a1101b18d306a786320aab12018b1325d6 assets/create/blockstates/purple_sail.json
 92957119abd5fbcca36a113b2a80255fd70fc303 assets/create/blockstates/purple_seat.json
 61035f8afe75ff7bbd291da5d8690bcbebe679eb assets/create/blockstates/purple_valve_handle.json
-8d7e653bfd9846e684a0d3725595714a19201017 assets/create/blockstates/radial_chassis.json
+ab2a1bf8f37f1c64d00538867f4c3a97195bd3c4 assets/create/blockstates/radial_chassis.json
 45877c4d90a7185c2f304edbd67379d800920439 assets/create/blockstates/red_sail.json
 da1b08387af7afa0855ee8d040f620c01f20660a assets/create/blockstates/red_seat.json
 722fc77bbf387af8a4016e42cbf9501d2b968881 assets/create/blockstates/red_valve_handle.json
@@ -402,16 +402,16 @@ a3a11524cd3515fc01d905767b4b7ea782adaf03 assets/create/blockstates/yellow_seat.j
 7f39521b211441f5c3e06d60c5978cebe16cacfb assets/create/blockstates/zinc_block.json
 b7181bcd8182b2f17088e5aa881f374c9c65470c assets/create/blockstates/zinc_ore.json
 666599fc27f1c67f992f989f5c9e707817e378f3 assets/create/lang/en_ud.json
-930cc08e0462936ca440d7271992bf8e624f5247 assets/create/lang/en_us.json
-98a913dd40c2f5e5d71d85b6a19fd6e5dd9f1a9d assets/create/lang/unfinished/de_de.json
-795fb3cff856391c09bd7065416cc02a07ce4e7d assets/create/lang/unfinished/fr_fr.json
-fb5db1f1f88e26443aee04e34bd7d776b59fecda assets/create/lang/unfinished/it_it.json
-82935bc99220883e49ccf4f380af030e024df115 assets/create/lang/unfinished/ja_jp.json
-8b25c7856a78ef247acaca6070d5e46d896f5b2b assets/create/lang/unfinished/ko_kr.json
-83bd4e5d968264dd4e5872146b159cb23d699cd3 assets/create/lang/unfinished/nl_nl.json
-11ab9394b65c0b756ca3693c6f1e2c569aff0095 assets/create/lang/unfinished/pt_br.json
-40216a791b1bca38e4230a0f66d8748560c98c22 assets/create/lang/unfinished/ru_ru.json
-548a60e23c0cadd2c34193eae1e3ee7f6d7cd043 assets/create/lang/unfinished/zh_cn.json
+e22cf28236c9df0468471aca71d88524a36f46e9 assets/create/lang/en_us.json
+da73c1c7d1114e86d74089a471ef5cffea3f5c8f assets/create/lang/unfinished/de_de.json
+ee900e9b93396f1d7bf375d11e23c6d1b9982ed3 assets/create/lang/unfinished/fr_fr.json
+be0a870d75ab60c97237c0d95dc24a4c1d15df79 assets/create/lang/unfinished/it_it.json
+02921005e9a71bf9936621648853b881eaa5086c assets/create/lang/unfinished/ja_jp.json
+6fb0968222207b9e4e7bfb9f1a1b1bb318a2bb5a assets/create/lang/unfinished/ko_kr.json
+f352fdfa063fd7c8d63ccb7b2aea64614cc88689 assets/create/lang/unfinished/nl_nl.json
+e3b801fd879a9c70c8c59e86183e5552215e3767 assets/create/lang/unfinished/pt_br.json
+c36b40ec465932a69e416861da6fdaed89b8afb1 assets/create/lang/unfinished/ru_ru.json
+6bba967e11433b8804fbb4ccada837cdfaf118ee assets/create/lang/unfinished/zh_cn.json
 487a511a01b2a4531fb672f917922312db78f958 assets/create/models/block/acacia_window.json
 b48060cba1a382f373a05bf0039054053eccf076 assets/create/models/block/acacia_window_pane_noside.json
 3066db1bf03cffa1a9c7fbacf47ae586632f4eb3 assets/create/models/block/acacia_window_pane_noside_alt.json
diff --git a/src/generated/resources/assets/create/lang/en_us.json b/src/generated/resources/assets/create/lang/en_us.json
index b753d54ce..8b837c6f5 100644
--- a/src/generated/resources/assets/create/lang/en_us.json
+++ b/src/generated/resources/assets/create/lang/en_us.json
@@ -1014,6 +1014,8 @@
 	"create.hint.upward_funnel": "can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "Update Bearing",
 	"create.hint.empty_bearing": "_Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "Deployer Item Overflow",
+	"create.hint.full_deployer": "It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "Hi :)",
 	"create.gui.config.overlay2": "This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/de_de.json b/src/generated/resources/assets/create/lang/unfinished/de_de.json
index c947f02b8..ccd032c68 100644
--- a/src/generated/resources/assets/create/lang/unfinished/de_de.json
+++ b/src/generated/resources/assets/create/lang/unfinished/de_de.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 1075",
+	"_": "Missing Localizations: 1077",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/fr_fr.json b/src/generated/resources/assets/create/lang/unfinished/fr_fr.json
index bfc287e06..91aeb5b4e 100644
--- a/src/generated/resources/assets/create/lang/unfinished/fr_fr.json
+++ b/src/generated/resources/assets/create/lang/unfinished/fr_fr.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 705",
+	"_": "Missing Localizations: 707",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/it_it.json b/src/generated/resources/assets/create/lang/unfinished/it_it.json
index b458630f2..a153d5f92 100644
--- a/src/generated/resources/assets/create/lang/unfinished/it_it.json
+++ b/src/generated/resources/assets/create/lang/unfinished/it_it.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 689",
+	"_": "Missing Localizations: 691",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/ja_jp.json b/src/generated/resources/assets/create/lang/unfinished/ja_jp.json
index f3ddb7f7c..222bf13c3 100644
--- a/src/generated/resources/assets/create/lang/unfinished/ja_jp.json
+++ b/src/generated/resources/assets/create/lang/unfinished/ja_jp.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 688",
+	"_": "Missing Localizations: 690",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/ko_kr.json b/src/generated/resources/assets/create/lang/unfinished/ko_kr.json
index 788ac2a38..0fb9bb836 100644
--- a/src/generated/resources/assets/create/lang/unfinished/ko_kr.json
+++ b/src/generated/resources/assets/create/lang/unfinished/ko_kr.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 689",
+	"_": "Missing Localizations: 691",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/nl_nl.json b/src/generated/resources/assets/create/lang/unfinished/nl_nl.json
index 97167bddb..c79d41995 100644
--- a/src/generated/resources/assets/create/lang/unfinished/nl_nl.json
+++ b/src/generated/resources/assets/create/lang/unfinished/nl_nl.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 1016",
+	"_": "Missing Localizations: 1018",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/pt_br.json b/src/generated/resources/assets/create/lang/unfinished/pt_br.json
index 23bf31c81..8e33ca4af 100644
--- a/src/generated/resources/assets/create/lang/unfinished/pt_br.json
+++ b/src/generated/resources/assets/create/lang/unfinished/pt_br.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 1082",
+	"_": "Missing Localizations: 1084",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/ru_ru.json b/src/generated/resources/assets/create/lang/unfinished/ru_ru.json
index e95a208df..20680a4b4 100644
--- a/src/generated/resources/assets/create/lang/unfinished/ru_ru.json
+++ b/src/generated/resources/assets/create/lang/unfinished/ru_ru.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 358",
+	"_": "Missing Localizations: 360",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/generated/resources/assets/create/lang/unfinished/zh_cn.json b/src/generated/resources/assets/create/lang/unfinished/zh_cn.json
index 68d9c51b1..46b8366ac 100644
--- a/src/generated/resources/assets/create/lang/unfinished/zh_cn.json
+++ b/src/generated/resources/assets/create/lang/unfinished/zh_cn.json
@@ -1,5 +1,5 @@
 {
-	"_": "Missing Localizations: 371",
+	"_": "Missing Localizations: 373",
 
 	"_": "->------------------------]  Game Elements  [------------------------<-",
 
@@ -1015,6 +1015,8 @@
 	"create.hint.upward_funnel": "UNLOCALIZED: can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "UNLOCALIZED: Update Bearing",
 	"create.hint.empty_bearing": "UNLOCALIZED: _Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "UNLOCALIZED: Deployer Item Overflow",
+	"create.hint.full_deployer": "UNLOCALIZED: It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "UNLOCALIZED: Hi :)",
 	"create.gui.config.overlay2": "UNLOCALIZED: This is a sample overlay",
diff --git a/src/main/java/com/simibubi/create/AllBlockPartials.java b/src/main/java/com/simibubi/create/AllBlockPartials.java
index d50e6dfbe..fd1244eac 100644
--- a/src/main/java/com/simibubi/create/AllBlockPartials.java
+++ b/src/main/java/com/simibubi/create/AllBlockPartials.java
@@ -9,7 +9,7 @@ import java.util.List;
 import java.util.Map;
 
 import com.mojang.blaze3d.matrix.MatrixStack;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour.AttachmentTypes;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour.AttachmentTypes;
 import com.simibubi.create.content.contraptions.processing.burner.BlazeBurnerBlock.HeatLevel;
 import com.simibubi.create.foundation.utility.AngleHelper;
 import com.simibubi.create.foundation.utility.Iterate;
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/deployer/DeployerTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/deployer/DeployerTileEntity.java
index 41de44765..9b3931623 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/deployer/DeployerTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/deployer/DeployerTileEntity.java
@@ -10,6 +10,7 @@ import com.simibubi.create.AllBlocks;
 import com.simibubi.create.content.contraptions.base.KineticTileEntity;
 import com.simibubi.create.content.curiosities.tools.SandPaperItem;
 import com.simibubi.create.foundation.advancement.AllTriggers;
+import com.simibubi.create.foundation.item.TooltipHelper;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.filtering.FilteringBehaviour;
 import com.simibubi.create.foundation.utility.NBTHelper;
@@ -32,11 +33,11 @@ import net.minecraft.util.math.RayTraceContext;
 import net.minecraft.util.math.RayTraceContext.BlockMode;
 import net.minecraft.util.math.RayTraceContext.FluidMode;
 import net.minecraft.util.math.vector.Vector3d;
+import net.minecraft.util.text.ITextComponent;
 import net.minecraft.world.server.ServerWorld;
 import net.minecraftforge.common.capabilities.Capability;
 import net.minecraftforge.common.util.Constants.NBT;
 import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.items.CapabilityItemHandler;
 import net.minecraftforge.items.IItemHandlerModifiable;
 
 public class DeployerTileEntity extends KineticTileEntity {
@@ -130,13 +131,22 @@ public class DeployerTileEntity extends KineticTileEntity {
 				return;
 			}
 
-			if (!filtering.test(stack)) {
-				if (!stack.isEmpty()) {
-					overflowItems.add(stack);
-					player.setHeldItem(Hand.MAIN_HAND, ItemStack.EMPTY);
-					sendData();
-					return;
+			boolean changed = false;
+			for (int i = 0; i < player.inventory.getSizeInventory(); i++) {
+				if (overflowItems.size() > 10)
+					break;
+				ItemStack item = player.inventory.getStackInSlot(i);
+				if (item.isEmpty())
+					continue;
+				if (item != stack || !filtering.test(item)) {
+					overflowItems.add(item);
+					player.inventory.setInventorySlotContents(i, ItemStack.EMPTY);
+					changed = true;
 				}
+			}
+
+			if (changed) {
+				sendData();
 				timer = getTimerSpeed() * 10;
 				return;
 			}
@@ -153,7 +163,7 @@ public class DeployerTileEntity extends KineticTileEntity {
 
 			if (redstoneLocked)
 				return;
-			
+
 			state = State.EXPANDING;
 			Vector3d movementVector = getMovementVector();
 			Vector3d rayOrigin = VecHelper.getCenterOf(pos)
@@ -266,7 +276,7 @@ public class DeployerTileEntity extends KineticTileEntity {
 		mode = NBTHelper.readEnum(compound, "Mode", Mode.class);
 		timer = compound.getInt("Timer");
 		redstoneLocked = compound.getBoolean("Powered");
-		
+
 		deferredInventoryList = compound.getList("Inventory", NBT.TAG_COMPOUND);
 		overflowItems = NBTHelper.readItemList(compound.getList("Overflow", NBT.TAG_COMPOUND));
 		if (compound.contains("HeldItem"))
@@ -289,7 +299,7 @@ public class DeployerTileEntity extends KineticTileEntity {
 		NBTHelper.writeEnum(compound, "State", state);
 		compound.putInt("Timer", timer);
 		compound.putBoolean("Powered", redstoneLocked);
-		
+
 		if (player != null) {
 			compound.put("HeldItem", player.getHeldItemMainhand()
 				.serializeNBT());
@@ -353,10 +363,21 @@ public class DeployerTileEntity extends KineticTileEntity {
 
 	@Override
 	public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
-		if (cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY && invHandler != null) {
+		if (isItemHandlerCap(cap) && invHandler != null) 
 			return invHandler.cast();
-		}
 		return super.getCapability(cap, side);
 	}
+	
+	@Override
+	public boolean addToTooltip(List<ITextComponent> tooltip, boolean isPlayerSneaking) {
+		if (super.addToTooltip(tooltip, isPlayerSneaking))
+			return true;
+		if (getSpeed() == 0)
+			return false;
+		if (overflowItems.isEmpty())
+			return false;
+		TooltipHelper.addHint(tooltip, "hint.full_deployer");
+		return true;
+	}
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/AbstractContraptionEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/AbstractContraptionEntity.java
index 3021cbf48..2db8c7069 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/AbstractContraptionEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/AbstractContraptionEntity.java
@@ -435,6 +435,15 @@ public abstract class AbstractContraptionEntity extends Entity implements IEntit
 		}
 	}
 
+	@SuppressWarnings("deprecation")
+	@Override
+	public void remove(boolean keepData) {
+		if (!world.isRemote && !removed && contraption != null) {
+			contraption.stop(world);
+		}
+		super.remove(keepData);
+	}
+
 	protected abstract StructureTransform makeStructureTransform();
 
 	@Override
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/Contraption.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/Contraption.java
index 3e692a233..2e7d1cc13 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/Contraption.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/Contraption.java
@@ -742,7 +742,6 @@ public abstract class Contraption {
 	}
 
 	public void addBlocksToWorld(World world, StructureTransform transform) {
-		stop(world);
 		for (boolean nonBrittles : Iterate.trueAndFalse) {
 			for (BlockInfo block : blocks.values()) {
 				if (nonBrittles == BlockMovementTraits.isBrittle(block.state))
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/TranslatingContraption.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/TranslatingContraption.java
index 58d77be16..062be299c 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/TranslatingContraption.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/TranslatingContraption.java
@@ -1,5 +1,6 @@
 package com.simibubi.create.content.contraptions.components.structureMovement;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -16,7 +17,7 @@ public abstract class TranslatingContraption extends Contraption {
 
 	public Set<BlockPos> getColliders(World world, Direction movementDirection) {
 		if (getBlocks() == null)
-			return null;
+			return Collections.EMPTY_SET;
 		if (cachedColliders == null || cachedColliderDirection != movementDirection) {
 			cachedColliders = new HashSet<>();
 			cachedColliderDirection = movementDirection;
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/LinearActuatorTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/LinearActuatorTileEntity.java
index fa789aad2..4ab65a0c9 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/LinearActuatorTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/LinearActuatorTileEntity.java
@@ -79,8 +79,10 @@ public abstract class LinearActuatorTileEntity extends KineticTileEntity impleme
 				else
 					sendData();
 				return;
+			} else {
+				if (getSpeed() != 0)
+					assemble();
 			}
-			assemble();
 			return;
 		}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/PistonContraption.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/PistonContraption.java
index 9c67056ee..33041f493 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/PistonContraption.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/piston/PistonContraption.java
@@ -53,9 +53,14 @@ public class PistonContraption extends TranslatingContraption {
 	public boolean assemble(World world, BlockPos pos) {
 		if (!collectExtensions(world, pos, orientation))
 			return false;
+		int count = blocks.size();
 		if (!searchMovedStructure(world, anchor, retract ? orientation.getOpposite() : orientation))
 			return false;
-		bounds = bounds.union(pistonExtensionCollisionBox);
+		if (blocks.size() == count) { // no new blocks added
+			bounds = pistonExtensionCollisionBox;
+		} else {
+			bounds = bounds.union(pistonExtensionCollisionBox);
+		}
 		startMoving(world);
 		return true;
 	}
@@ -112,8 +117,10 @@ public class PistonContraption extends TranslatingContraption {
 		anchor = pos.offset(direction, initialExtensionProgress + 1);
 		extensionLength = extensionsInBack + extensionsInFront;
 		initialExtensionProgress = extensionsInFront;
-		pistonExtensionCollisionBox = new AxisAlignedBB(end.offset(direction, -extensionsInFront)
-			.subtract(anchor));
+		pistonExtensionCollisionBox = new AxisAlignedBB(
+				BlockPos.ZERO.offset(direction, -1),
+				BlockPos.ZERO.offset(direction, -extensionLength - 1)).expand(1,
+						1, 1);
 
 		if (extensionLength == 0)
 			return false;
@@ -124,7 +131,7 @@ public class PistonContraption extends TranslatingContraption {
 			BlockPos relPos = pole.pos.offset(direction, -extensionsInFront);
 			BlockPos localPos = relPos.subtract(anchor);
 			getBlocks().put(localPos, new BlockInfo(localPos, pole.state, null));
-			pistonExtensionCollisionBox = pistonExtensionCollisionBox.union(new AxisAlignedBB(localPos));
+			//pistonExtensionCollisionBox = pistonExtensionCollisionBox.union(new AxisAlignedBB(localPos));
 		}
 
 		return true;
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/train/capability/MinecartController.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/train/capability/MinecartController.java
index 2c24738a0..134b38262 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/train/capability/MinecartController.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/train/capability/MinecartController.java
@@ -20,10 +20,16 @@ import com.simibubi.create.foundation.utility.Iterate;
 import com.simibubi.create.foundation.utility.NBTHelper;
 import com.simibubi.create.foundation.utility.VecHelper;
 
+import net.minecraft.block.BlockState;
+import net.minecraft.block.PoweredRailBlock;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.item.minecart.AbstractMinecartEntity;
+import net.minecraft.entity.item.minecart.MinecartEntity;
 import net.minecraft.nbt.CompoundNBT;
 import net.minecraft.nbt.NBTUtil;
+import net.minecraft.tags.BlockTags;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.math.vector.Vector3d;
 import net.minecraft.world.World;
 import net.minecraftforge.common.util.Constants.NBT;
@@ -79,8 +85,45 @@ public class MinecartController implements INBTSerializable<CompoundNBT> {
 				internalStall.booleanValue() || otherCart == null || !otherCart.isPresent() || otherCart.isStalled(false));
 
 		}));
-		if (!world.isRemote)
+		if (!world.isRemote) {
 			setStalled(internalStall.booleanValue(), true);
+			disassemble(cart);
+		}
+	}
+
+	private void disassemble(AbstractMinecartEntity cart) {
+		if (cart instanceof MinecartEntity) {
+			return;
+		}
+		List<Entity> passengers = cart.getPassengers();
+		if (passengers.isEmpty() || !(passengers.get(0) instanceof AbstractContraptionEntity)) {
+			return;
+		}
+		World world = cart.world;
+		int i = MathHelper.floor(cart.getX());
+		int j = MathHelper.floor(cart.getY());
+		int k = MathHelper.floor(cart.getZ());
+		if (world.getBlockState(new BlockPos(i, j - 1, k))
+				.isIn(BlockTags.RAILS)) {
+			--j;
+		}
+		BlockPos blockpos = new BlockPos(i, j, k);
+		BlockState blockstate = world.getBlockState(blockpos);
+		if (cart.canUseRail() && blockstate.isIn(BlockTags.RAILS)
+				&& blockstate.getBlock() instanceof PoweredRailBlock
+				&& ((PoweredRailBlock) blockstate.getBlock())
+						.isActivatorRail()) {
+			if (cart.isBeingRidden()) {
+				cart.removePassengers();
+			}
+
+			if (cart.getRollingAmplitude() == 0) {
+				cart.setRollingDirection(-cart.getRollingDirection());
+				cart.setRollingAmplitude(10);
+				cart.setDamage(50.0F);
+				cart.velocityChanged = true;
+			}
+		}
 	}
 
 	public boolean isFullyCoupled() {
diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/waterwheel/WaterWheelBlock.java b/src/main/java/com/simibubi/create/content/contraptions/components/waterwheel/WaterWheelBlock.java
index 710289902..62185b9ea 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/components/waterwheel/WaterWheelBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/components/waterwheel/WaterWheelBlock.java
@@ -13,6 +13,8 @@ import com.simibubi.create.foundation.utility.worldWrappers.WrappedWorld;
 import mcp.MethodsReturnNonnullByDefault;
 import net.minecraft.block.BlockRenderType;
 import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.block.BubbleColumnBlock;
 import net.minecraft.fluid.FluidState;
 import net.minecraft.fluid.Fluids;
 import net.minecraft.item.BlockItemUseContext;
@@ -84,18 +86,25 @@ public class WaterWheelBlock extends HorizontalKineticBlock implements ITE<Water
 		updateWheelSpeed(worldIn, pos);
 	}
 
-	private void updateFlowAt(BlockState state, IWorld world, BlockPos pos, Direction f) {
-		if (f.getAxis() == state.get(HORIZONTAL_FACING)
+	private void updateFlowAt(BlockState state, IWorld world, BlockPos pos, Direction side) {
+		if (side.getAxis() == state.get(HORIZONTAL_FACING)
 			.getAxis())
 			return;
 
-		FluidState fluid = world.getFluidState(pos.offset(f));
+		FluidState fluid = world.getFluidState(pos.offset(side));
 		Direction wf = state.get(HORIZONTAL_FACING);
 		boolean clockwise = wf.getAxisDirection() == AxisDirection.POSITIVE;
 		int clockwiseMultiplier = 2;
 
-		Vector3d vec = fluid.getFlow(world, pos.offset(f));
-		vec = vec.scale(f.getAxisDirection()
+		Vector3d vec = fluid.getFlow(world, pos.offset(side));
+		if (side.getAxis()
+			.isHorizontal()) {
+			BlockState adjacentBlock = world.getBlockState(pos.offset(side));
+			if (adjacentBlock.getBlock() == Blocks.BUBBLE_COLUMN.getBlock())
+				vec = new Vector3d(0, adjacentBlock.get(BubbleColumnBlock.DRAG) ? -1 : 1, 0);
+		}
+
+		vec = vec.scale(side.getAxisDirection()
 			.getOffset());
 		vec = new Vector3d(Math.signum(vec.x), Math.signum(vec.y), Math.signum(vec.z));
 		Vector3d flow = vec;
@@ -104,16 +113,16 @@ public class WaterWheelBlock extends HorizontalKineticBlock implements ITE<Water
 			double flowStrength = 0;
 
 			if (wf.getAxis() == Axis.Z) {
-				if (f.getAxis() == Axis.Y)
+				if (side.getAxis() == Axis.Y)
 					flowStrength = flow.x > 0 ^ !clockwise ? -flow.x * clockwiseMultiplier : -flow.x;
-				if (f.getAxis() == Axis.X)
+				if (side.getAxis() == Axis.X)
 					flowStrength = flow.y < 0 ^ !clockwise ? flow.y * clockwiseMultiplier : flow.y;
 			}
 
 			if (wf.getAxis() == Axis.X) {
-				if (f.getAxis() == Axis.Y)
+				if (side.getAxis() == Axis.Y)
 					flowStrength = flow.z < 0 ^ !clockwise ? flow.z * clockwiseMultiplier : flow.z;
-				if (f.getAxis() == Axis.Z)
+				if (side.getAxis() == Axis.Z)
 					flowStrength = flow.y > 0 ^ !clockwise ? -flow.y * clockwiseMultiplier : -flow.y;
 			}
 
@@ -124,7 +133,7 @@ public class WaterWheelBlock extends HorizontalKineticBlock implements ITE<Water
 			}
 
 			Integer flowModifier = AllConfigs.SERVER.kinetics.waterWheelFlowSpeed.get();
-			te.setFlow(f, (float) (flowStrength * flowModifier / 2f));
+			te.setFlow(side, (float) (flowStrength * flowModifier / 2f));
 		});
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FlowSource.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FlowSource.java
new file mode 100644
index 000000000..e2beb3a37
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FlowSource.java
@@ -0,0 +1,137 @@
+package com.simibubi.create.content.contraptions.fluids;
+
+import java.lang.ref.WeakReference;
+import java.util.function.Predicate;
+
+import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
+import com.simibubi.create.foundation.utility.BlockFace;
+
+import net.minecraft.tileentity.TileEntity;
+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;
+
+public abstract class FlowSource {
+
+	private static final LazyOptional<IFluidHandler> EMPTY = LazyOptional.empty();
+
+	BlockFace location;
+
+	public FlowSource(BlockFace location) {
+		this.location = location;
+	}
+
+	public FluidStack provideFluid(Predicate<FluidStack> extractionPredicate) {
+		IFluidHandler tank = provideHandler().orElse(null);
+		if (tank == null)
+			return FluidStack.EMPTY;
+		FluidStack immediateFluid = tank.drain(1, FluidAction.SIMULATE);
+		if (extractionPredicate.test(immediateFluid))
+			return immediateFluid;
+
+		for (int i = 0; i < tank.getTanks(); i++) {
+			FluidStack contained = tank.getFluidInTank(i);
+			if (contained.isEmpty())
+				continue;
+			if (!extractionPredicate.test(contained))
+				continue;
+			FluidStack toExtract = contained.copy();
+			toExtract.setAmount(1);
+			return tank.drain(toExtract, FluidAction.SIMULATE);
+		}
+
+		return FluidStack.EMPTY;
+	}
+
+	// Layer III. PFIs need active attention to prevent them from disengaging early
+	public void keepAlive() {}
+
+	public abstract boolean isEndpoint();
+
+	public void manageSource(World world) {}
+	
+	public void whileFlowPresent(World world, boolean pulling) {}
+
+	public LazyOptional<IFluidHandler> provideHandler() {
+		return EMPTY;
+	}
+
+	public static class FluidHandler extends FlowSource {
+		LazyOptional<IFluidHandler> fluidHandler;
+
+		public FluidHandler(BlockFace location) {
+			super(location);
+			fluidHandler = EMPTY;
+		}
+
+		public void manageSource(World world) {
+			if (fluidHandler.isPresent())
+				return;
+			TileEntity tileEntity = world.getTileEntity(location.getConnectedPos());
+			if (tileEntity != null)
+				fluidHandler = tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY,
+					location.getOppositeFace());
+		}
+
+		@Override
+		public LazyOptional<IFluidHandler> provideHandler() {
+			return fluidHandler;
+		}
+
+		@Override
+		public boolean isEndpoint() {
+			return true;
+		}
+	}
+
+	public static class OtherPipe extends FlowSource {
+		WeakReference<FluidTransportBehaviour> cached;
+
+		public OtherPipe(BlockFace location) {
+			super(location);
+		}
+
+		@Override
+		public void manageSource(World world) {
+			if (cached != null && cached.get() != null && !cached.get().tileEntity.isRemoved())
+				return;
+			cached = null;
+			FluidTransportBehaviour fluidTransportBehaviour =
+				TileEntityBehaviour.get(world, location.getConnectedPos(), FluidTransportBehaviour.TYPE);
+			if (fluidTransportBehaviour != null)
+				cached = new WeakReference<>(fluidTransportBehaviour);
+		}
+
+		@Override
+		public FluidStack provideFluid(Predicate<FluidStack> extractionPredicate) {
+			if (cached == null || cached.get() == null)
+				return FluidStack.EMPTY;
+			FluidTransportBehaviour behaviour = cached.get();
+			FluidStack providedOutwardFluid = behaviour.getProvidedOutwardFluid(location.getOppositeFace());
+			return extractionPredicate.test(providedOutwardFluid) ? providedOutwardFluid : FluidStack.EMPTY;
+		}
+
+		@Override
+		public boolean isEndpoint() {
+			return false;
+		}
+
+	}
+
+	public static class Blocked extends FlowSource {
+
+		public Blocked(BlockFace location) {
+			super(location);
+		}
+
+		@Override
+		public boolean isEndpoint() {
+			return false;
+		}
+
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidBottleItemHook.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidBottleItemHook.java
new file mode 100644
index 000000000..4b1d61d34
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidBottleItemHook.java
@@ -0,0 +1,58 @@
+package com.simibubi.create.content.contraptions.fluids;
+
+import com.simibubi.create.Create;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.item.GlassBottleItem;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.tags.FluidTags;
+import net.minecraft.util.ActionResultType;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.BlockRayTraceResult;
+import net.minecraft.util.math.RayTraceContext;
+import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.world.World;
+import net.minecraftforge.event.entity.player.PlayerInteractEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
+
+@EventBusSubscriber
+public class FluidBottleItemHook extends Item {
+
+	public FluidBottleItemHook(Properties p_i48487_1_) {
+		super(p_i48487_1_);
+	}
+
+	@SubscribeEvent
+	public static void preventWaterBottlesFromCreatesFluids(PlayerInteractEvent.RightClickItem event) {
+		ItemStack itemStack = event.getItemStack();
+		if (itemStack.isEmpty())
+			return;
+		if (!(itemStack.getItem() instanceof GlassBottleItem))
+			return;
+
+		World world = event.getWorld();
+		PlayerEntity player = event.getPlayer();
+		RayTraceResult raytraceresult = rayTrace(world, player, RayTraceContext.FluidMode.SOURCE_ONLY);
+		if (raytraceresult.getType() != RayTraceResult.Type.BLOCK)
+			return;
+		BlockPos blockpos = ((BlockRayTraceResult) raytraceresult).getPos();
+		if (!world.isBlockModifiable(player, blockpos))
+			return;
+
+		IFluidState fluidState = world.getFluidState(blockpos);
+		if (fluidState.isTagged(FluidTags.WATER) && fluidState.getFluid()
+			.getRegistryName()
+			.getNamespace()
+			.equals(Create.ID)) {
+			event.setCancellationResult(ActionResultType.PASS);
+			event.setCanceled(true);
+			return;
+		}
+
+		return;
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetwork.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetwork.java
index 32aad3077..8a3305e74 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetwork.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetwork.java
@@ -1,359 +1,263 @@
 package com.simibubi.create.content.contraptions.fluids;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
-import com.google.common.collect.ImmutableList;
+import javax.annotation.Nullable;
+
+import com.simibubi.create.content.contraptions.fluids.PipeConnection.Flow;
+import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.utility.BlockFace;
+import com.simibubi.create.foundation.utility.Iterate;
 import com.simibubi.create.foundation.utility.Pair;
 
-import net.minecraft.block.BlockState;
-import net.minecraft.tileentity.TileEntity;
 import net.minecraft.util.Direction;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IWorld;
 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;
 
 public class FluidNetwork {
 
-	BlockFace pumpLocation;
-	Map<BlockPos, Pair<Integer, Map<Direction, Boolean>>> pipeGraph;
-	List<FluidNetworkFlow> flows;
-	Set<FluidNetworkEndpoint> targets;
-	Set<BlockFace> rangeEndpoints;
-	Map<BlockFace, FluidStack> previousFlow;
+	private static int CYCLES_PER_TICK = 16;
+	
+	World world;
+	BlockFace start;
 
-	boolean connectToPumps;
-	int waitForUnloadedNetwork;
+	Supplier<LazyOptional<IFluidHandler>> sourceSupplier;
+	LazyOptional<IFluidHandler> source;
+	int transferSpeed;
 
-	public FluidNetwork() {
-		pipeGraph = new HashMap<>();
-		flows = new ArrayList<>();
-		targets = new HashSet<>();
-		rangeEndpoints = new HashSet<>();
-		previousFlow = new HashMap<>();
+	int pauseBeforePropagation;
+	List<BlockFace> queued;
+	Set<Pair<BlockFace, PipeConnection>> frontier;
+	Set<BlockPos> visited;
+	List<Pair<BlockFace, LazyOptional<IFluidHandler>>> targets;
+	Map<BlockPos, WeakReference<FluidTransportBehaviour>> cache;
+
+	public FluidNetwork(World world, BlockFace location, Supplier<LazyOptional<IFluidHandler>> sourceSupplier) {
+		this.world = world;
+		this.start = location;
+		this.sourceSupplier = sourceSupplier;
+		this.source = LazyOptional.empty();
+		this.frontier = new HashSet<>();
+		this.visited = new HashSet<>();
+		this.targets = new ArrayList<>();
+		this.cache = new HashMap<>();
+		this.queued = new ArrayList<>();
+		reset();
 	}
 
-	public boolean hasEndpoints() {
-		for (FluidNetworkFlow pipeFlow : flows)
-			if (pipeFlow.hasValidTargets())
-				return true;
-		return false;
-	}
-
-	public Collection<FluidNetworkEndpoint> getEndpoints(boolean pulling) {
-		if (!pulling) {
-			for (FluidNetworkFlow pipeFlow : flows)
-				return pipeFlow.outputEndpoints;
-			return Collections.emptySet();
-		}
-
-		List<FluidNetworkEndpoint> list = new ArrayList<>();
-		for (FluidNetworkFlow pipeFlow : flows) {
-			if (!pipeFlow.hasValidTargets())
-				continue;
-			list.add(pipeFlow.source);
-		}
-		return list;
-	}
-
-	public void tick(IWorld world, PumpTileEntity pumpTE) {
-		if (connectToPumps) {
-			connectToOtherFNs(world, pumpTE);
-			connectToPumps = false;
-		}
-	}
-
-	public void tickFlows(IWorld world, PumpTileEntity pumpTE, boolean pulling, float speed) {
-		if (connectToPumps)
+	public void tick() {
+		if (pauseBeforePropagation > 0) {
+			pauseBeforePropagation--;
 			return;
-		initFlows(pumpTE, pulling);
-		previousFlow.clear();
-		flows.forEach(ep -> ep.tick(world, speed));
-	}
+		}
+		
+		for (int cycle = 0; cycle < CYCLES_PER_TICK; cycle++) {
+			boolean shouldContinue = false;
+			for (Iterator<BlockFace> iterator = queued.iterator(); iterator.hasNext();) {
+				BlockFace blockFace = iterator.next();
+				if (!isPresent(blockFace))
+					continue;
+				PipeConnection pipeConnection = get(blockFace);
+				if (pipeConnection != null) {
+					if (blockFace.equals(start))
+						transferSpeed = (int) Math.max(1, pipeConnection.pressure.get(true) / 2f);
+					frontier.add(Pair.of(blockFace, pipeConnection));
+				}
+				iterator.remove();
+			}
+			
+//			drawDebugOutlines();
+			
+			for (Iterator<Pair<BlockFace, PipeConnection>> iterator = frontier.iterator(); iterator.hasNext();) {
+				Pair<BlockFace, PipeConnection> pair = iterator.next();
+				BlockFace blockFace = pair.getFirst();
+				PipeConnection pipeConnection = pair.getSecond();
+				
+				if (!pipeConnection.hasFlow())
+					continue;
+				Flow flow = pipeConnection.flow.get();
+				if (!flow.inbound) {
+					if (pipeConnection.comparePressure() >= 0)
+						iterator.remove();
+					continue;
+				}
+				if (!flow.complete)
+					continue;
+				
+				boolean canRemove = true;
+				for (Direction side : Iterate.directions) {
+					if (side == blockFace.getFace())
+						continue;
+					BlockFace adjacentLocation = new BlockFace(blockFace.getPos(), side);
+					PipeConnection adjacent = get(adjacentLocation);
+					if (adjacent == null)
+						continue;
+					if (!adjacent.hasFlow()) {
+						// Branch could potentially still appear
+						if (adjacent.hasPressure() && adjacent.pressure.getSecond() > 0)
+							canRemove = false;
+						continue;
+					}
+					Flow outFlow = adjacent.flow.get();
+					if (outFlow.inbound) {
+						if (adjacent.comparePressure() > 0)
+							canRemove = false;
+						continue;
+					}
+					if (!outFlow.complete) {
+						canRemove = false;
+						continue;
+					}
+					
+					if (adjacent.source.isPresent() && adjacent.source.get()
+						.isEndpoint()) {
+						targets.add(Pair.of(adjacentLocation, adjacent.source.get()
+							.provideHandler()));
+						continue;
+					}
+					
+					if (visited.add(adjacentLocation.getConnectedPos())) {
+						queued.add(adjacentLocation.getOpposite());
+						shouldContinue = true;
+					}
+				}
+				if (canRemove)
+					iterator.remove();
+			}
+			if (!shouldContinue)
+				break;
+		}
+		
+//		drawDebugOutlines();
 
-	private void initFlows(PumpTileEntity pumpTE, boolean pulling) {
+		if (!source.isPresent())
+			source = sourceSupplier.get();
+		if (!source.isPresent())
+			return;
 		if (targets.isEmpty())
 			return;
-		if (!flows.isEmpty())
-			return;
-		World world = pumpTE.getWorld();
-		if (pulling) {
-			targets.forEach(ne -> flows.add(new FluidNetworkFlow(this, ne, world, pulling)));
-		} else {
-			PumpEndpoint pumpEndpoint = new PumpEndpoint(pumpLocation.getOpposite(), pumpTE);
-			flows.add(new FluidNetworkFlow(this, pumpEndpoint, world, pulling));
-		}
-	}
-
-	public void connectToOtherFNs(IWorld world, PumpTileEntity pump) {
-		List<Pair<Integer, BlockPos>> frontier = new ArrayList<>();
-		Set<BlockPos> visited = new HashSet<>();
-		int maxDistance = FluidPropagator.getPumpRange() * 2;
-		frontier.add(Pair.of(-1, pumpLocation.getPos()));
-
-		while (!frontier.isEmpty()) {
-			Pair<Integer, BlockPos> entry = frontier.remove(0);
-			int distance = entry.getFirst();
-			BlockPos currentPos = entry.getSecond();
-
-			if (!world.isAreaLoaded(currentPos, 0))
+		for (Pair<BlockFace, LazyOptional<IFluidHandler>> pair : targets) {
+			if (pair.getSecond()
+				.isPresent())
 				continue;
-			if (visited.contains(currentPos))
+			PipeConnection pipeConnection = get(pair.getFirst());
+			if (pipeConnection == null)
 				continue;
-			visited.add(currentPos);
-
-			List<Direction> connections;
-			if (currentPos.equals(pumpLocation.getPos())) {
-				connections = ImmutableList.of(pumpLocation.getFace());
-			} else {
-				BlockState currentState = world.getBlockState(currentPos);
-				FluidPipeBehaviour pipe = FluidPropagator.getPipe(world, currentPos);
-				if (pipe == null)
-					continue;
-				connections = FluidPropagator.getPipeConnections(currentState, pipe);
-			}
-
-			for (Direction face : connections) {
-				BlockFace blockFace = new BlockFace(currentPos, face);
-				BlockPos connectedPos = blockFace.getConnectedPos();
-				BlockState connectedState = world.getBlockState(connectedPos);
-
-				if (connectedPos.equals(pumpLocation.getPos()))
-					continue;
-				if (!world.isAreaLoaded(connectedPos, 0))
-					continue;
-				if (PumpBlock.isPump(connectedState) && connectedState.get(PumpBlock.FACING)
-					.getAxis() == face.getAxis()) {
-					TileEntity tileEntity = world.getTileEntity(connectedPos);
-					if (tileEntity instanceof PumpTileEntity) {
-						PumpTileEntity otherPump = (PumpTileEntity) tileEntity;
-						if (otherPump.networks == null)
-							continue;
-
-						otherPump.networks.forEach(fn -> {
-							int nearest = Integer.MAX_VALUE;
-							BlockFace argNearest = null;
-							for (BlockFace pumpEndpoint : fn.rangeEndpoints) {
-								if (pumpEndpoint.isEquivalent(pumpLocation)) {
-									argNearest = pumpEndpoint;
-									break;
-								}
-								Pair<Integer, Map<Direction, Boolean>> pair =
-									pipeGraph.get(pumpEndpoint.getConnectedPos());
-								if (pair == null)
-									continue;
-								Integer distanceFromPump = pair.getFirst();
-								Map<Direction, Boolean> pipeConnections = pair.getSecond();
-
-								if (!pipeConnections.containsKey(pumpEndpoint.getOppositeFace()))
-									continue;
-								if (nearest <= distanceFromPump)
-									continue;
-								nearest = distanceFromPump;
-								argNearest = pumpEndpoint;
-
-							}
-							if (argNearest != null) {
-								InterPumpEndpoint endpoint = new InterPumpEndpoint(world, argNearest.getOpposite(),
-									pump, otherPump, pumpLocation, fn.pumpLocation);
-								targets.add(endpoint);
-								fn.targets.add(endpoint.opposite(world));
-							}
-						});
-
-					}
-					continue;
-				}
-				if (visited.contains(connectedPos))
-					continue;
-				if (distance > maxDistance)
-					continue;
-				FluidPipeBehaviour targetPipe = FluidPropagator.getPipe(world, connectedPos);
-				if (targetPipe == null)
-					continue;
-				if (targetPipe.isConnectedTo(connectedState, face.getOpposite()))
-					frontier.add(Pair.of(distance + 1, connectedPos));
-			}
-		}
-
-	}
-
-	public void assemble(IWorld world, PumpTileEntity pumpTE, BlockFace pumpLocation) {
-		Map<BlockFace, OpenEndedPipe> openEnds = pumpTE.getOpenEnds(pumpLocation.getFace());
-		openEnds.values()
-			.forEach(OpenEndedPipe::markStale);
-
-		this.pumpLocation = pumpLocation;
-		if (!collectEndpoint(world, pumpLocation, openEnds, 0)) {
-
-			List<Pair<Integer, BlockPos>> frontier = new ArrayList<>();
-			Set<BlockPos> visited = new HashSet<>();
-			int maxDistance = FluidPropagator.getPumpRange();
-			frontier.add(Pair.of(0, pumpLocation.getConnectedPos()));
-
-			while (!frontier.isEmpty()) {
-				Pair<Integer, BlockPos> entry = frontier.remove(0);
-				int distance = entry.getFirst();
-				BlockPos currentPos = entry.getSecond();
-
-				if (!world.isAreaLoaded(currentPos, 0))
-					continue;
-				if (visited.contains(currentPos))
-					continue;
-				visited.add(currentPos);
-				BlockState currentState = world.getBlockState(currentPos);
-				FluidPipeBehaviour pipe = FluidPropagator.getPipe(world, currentPos);
-				if (pipe == null)
-					continue;
-
-				for (Direction face : FluidPropagator.getPipeConnections(currentState, pipe)) {
-					if (!pipe.canTransferToward(FluidStack.EMPTY, world.getBlockState(currentPos), face, false))
-						continue;
-
-					BlockFace blockFace = new BlockFace(currentPos, face);
-					BlockPos connectedPos = blockFace.getConnectedPos();
-
-					if (connectedPos.equals(pumpLocation.getPos())) {
-						addEntry(blockFace.getPos(), blockFace.getFace(), true, distance);
-						continue;
-					}
-					if (!world.isAreaLoaded(connectedPos, 0))
-						continue;
-					if (collectEndpoint(world, blockFace, openEnds, distance))
-						continue;
-					FluidPipeBehaviour pipeBehaviour = FluidPropagator.getPipe(world, connectedPos);
-					if (pipeBehaviour == null)
-						continue;
-					if (visited.contains(connectedPos))
-						continue;
-					if (distance + 1 >= maxDistance) {
-						rangeEndpoints.add(blockFace);
-						addEntry(currentPos, face, false, distance);
-						FluidPropagator.showBlockFace(blockFace)
-							.lineWidth(1 / 8f)
-							.colored(0xff0000);
-						continue;
-					}
-
-					addConnection(connectedPos, currentPos, face.getOpposite(), distance);
-					frontier.add(Pair.of(distance + 1, connectedPos));
-				}
-			}
-		}
-
-		Set<BlockFace> staleEnds = new HashSet<>();
-		openEnds.entrySet()
-			.forEach(e -> {
-				if (e.getValue()
-					.isStale())
-					staleEnds.add(e.getKey());
+			pipeConnection.source.ifPresent(fs -> {
+				if (fs.isEndpoint())
+					pair.setSecond(fs.provideHandler());
 			});
-		staleEnds.forEach(openEnds::remove);
-
-		connectToPumps = true;
-	}
-
-	private FluidNetworkEndpoint reuseOrCreateOpenEnd(IWorld world, Map<BlockFace, OpenEndedPipe> openEnds,
-		BlockFace toCreate) {
-		OpenEndedPipe openEndedPipe = null;
-		if (openEnds.containsKey(toCreate)) {
-			openEndedPipe = openEnds.get(toCreate);
-			openEndedPipe.unmarkStale();
-		} else {
-			openEndedPipe = new OpenEndedPipe(toCreate);
-			openEnds.put(toCreate, openEndedPipe);
 		}
-		return new FluidNetworkEndpoint(world, toCreate, openEndedPipe.getCapability());
 
-	}
+		int flowSpeed = transferSpeed;
+		for (boolean simulate : Iterate.trueAndFalse) {
+			FluidAction action = simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE;
 
-	private boolean collectEndpoint(IWorld world, BlockFace blockFace, Map<BlockFace, OpenEndedPipe> openEnds,
-		int distance) {
-		BlockPos connectedPos = blockFace.getConnectedPos();
-		BlockState connectedState = world.getBlockState(connectedPos);
+			IFluidHandler handler = source.orElse(null);
+			if (handler == null)
+				return;
+			FluidStack transfer = handler.drain(flowSpeed, action);
+			if (transfer.isEmpty())
+				return;
 
-		// other pipe, no endpoint
-		FluidPipeBehaviour pipe = FluidPropagator.getPipe(world, connectedPos);
-		if (pipe != null && pipe.isConnectedTo(connectedState, blockFace.getOppositeFace()))
-			return false;
-		TileEntity tileEntity = world.getTileEntity(connectedPos);
+			List<Pair<BlockFace, LazyOptional<IFluidHandler>>> availableOutputs = new ArrayList<>(targets);
+			while (!availableOutputs.isEmpty() && transfer.getAmount() > 0) {
+				int dividedTransfer = transfer.getAmount() / availableOutputs.size();
+				int remainder = transfer.getAmount() % availableOutputs.size();
+
+				for (Iterator<Pair<BlockFace, LazyOptional<IFluidHandler>>> iterator =
+					availableOutputs.iterator(); iterator.hasNext();) {
+					Pair<BlockFace, LazyOptional<IFluidHandler>> pair = iterator.next();
+					int toTransfer = dividedTransfer;
+					if (remainder > 0) {
+						toTransfer++;
+						remainder--;
+					}
+
+					if (transfer.isEmpty())
+						break;
+					IFluidHandler targetHandler = pair.getSecond()
+						.orElse(null);
+					if (targetHandler == null) {
+						iterator.remove();
+						continue;
+					}
+
+					FluidStack divided = transfer.copy();
+					divided.setAmount(toTransfer);
+					int fill = targetHandler.fill(divided, action);
+					transfer.setAmount(transfer.getAmount() - fill);
+					if (fill < toTransfer)
+						iterator.remove();
+				}
 
-		// fluid handler endpoint
-		Direction face = blockFace.getFace();
-		if (tileEntity != null) {
-			LazyOptional<IFluidHandler> capability =
-				tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, face.getOpposite());
-			if (capability.isPresent()) {
-				targets.add(new FluidNetworkEndpoint(world, blockFace, capability));
-				addEntry(blockFace.getPos(), face, false, distance);
-				FluidPropagator.showBlockFace(blockFace)
-					.colored(0x00b7c2)
-					.lineWidth(1 / 8f);
-				return true;
 			}
+
+			flowSpeed -= transfer.getAmount();
+			transfer = FluidStack.EMPTY;
 		}
-
-		// open endpoint
-		if (PumpBlock.isPump(connectedState) && connectedState.get(PumpBlock.FACING)
-			.getAxis() == face.getAxis()) {
-			rangeEndpoints.add(blockFace);
-			addEntry(blockFace.getPos(), face, false, distance);
-			return true;
-		}
-		if (!FluidPropagator.isOpenEnd(world, blockFace.getPos(), face))
-			return false;
-
-		targets.add(reuseOrCreateOpenEnd(world, openEnds, blockFace));
-		addEntry(blockFace.getPos(), face, false, distance);
-		FluidPropagator.showBlockFace(blockFace)
-			.colored(0xb700c2)
-			.lineWidth(1 / 8f);
-		return true;
 	}
 
-	private void addConnection(BlockPos from, BlockPos to, Direction direction, int distance) {
-		addEntry(from, direction, true, distance);
-		addEntry(to, direction.getOpposite(), false, distance + 1);
-	}
+//	private void drawDebugOutlines() {
+//		FluidPropagator.showBlockFace(start)
+//			.lineWidth(1 / 8f)
+//			.colored(0xff0000);
+//		for (Pair<BlockFace, LazyOptional<IFluidHandler>> pair : targets)
+//			FluidPropagator.showBlockFace(pair.getFirst())
+//				.lineWidth(1 / 8f)
+//				.colored(0x00ff00);
+//		for (Pair<BlockFace, PipeConnection> pair : frontier)
+//			FluidPropagator.showBlockFace(pair.getFirst())
+//				.lineWidth(1 / 4f)
+//				.colored(0xfaaa33);
+//	}
 
-	private void addEntry(BlockPos pos, Direction direction, boolean outbound, int distance) {
-		if (!pipeGraph.containsKey(pos))
-			pipeGraph.put(pos, Pair.of(distance, new HashMap<>()));
-		pipeGraph.get(pos)
-			.getSecond()
-			.put(direction, outbound);
-	}
-
-	public void reAssemble(IWorld world, PumpTileEntity pumpTE, BlockFace pumpLocation) {
-		rangeEndpoints.clear();
+	public void reset() {
+		frontier.clear();
+		visited.clear();
 		targets.clear();
-		pipeGraph.clear();
-		assemble(world, pumpTE, pumpLocation);
+		queued.clear();
+		queued.add(start);
+		pauseBeforePropagation = 2;
 	}
 
-	public void remove(IWorld world) {
-		clearFlows(world, false);
+	@Nullable
+	private PipeConnection get(BlockFace location) {
+		BlockPos pos = location.getPos();
+		FluidTransportBehaviour fluidTransfer = getFluidTransfer(pos);
+		if (fluidTransfer == null)
+			return null;
+		return fluidTransfer.getConnection(location.getFace());
 	}
 
-	public void clearFlows(IWorld world, boolean saveState) {
-		for (FluidNetworkFlow networkFlow : flows) {
-			if (!networkFlow.getFluidStack()
-				.isEmpty())
-				networkFlow.addToSkippedConnections(world);
-			networkFlow.resetFlow(world);
+	private boolean isPresent(BlockFace location) {
+		return world.isAreaLoaded(location.getPos(), 0);
+	}
+
+	@Nullable
+	private FluidTransportBehaviour getFluidTransfer(BlockPos pos) {
+		WeakReference<FluidTransportBehaviour> weakReference = cache.get(pos);
+		FluidTransportBehaviour behaviour = weakReference != null ? weakReference.get() : null;
+		if (behaviour != null && behaviour.tileEntity.isRemoved())
+			behaviour = null;
+		if (behaviour == null) {
+			behaviour = TileEntityBehaviour.get(world, pos, FluidTransportBehaviour.TYPE);
+			if (behaviour != null)
+				cache.put(pos, new WeakReference<>(behaviour));
 		}
-		flows.clear();
+		return behaviour;
 	}
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkEndpoint.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkEndpoint.java
deleted file mode 100644
index e37e0b35c..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkEndpoint.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import com.simibubi.create.foundation.utility.BlockFace;
-
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.world.IWorld;
-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;
-
-public class FluidNetworkEndpoint {
-	BlockFace location;
-	protected LazyOptional<IFluidHandler> handler;
-
-	public FluidNetworkEndpoint(IWorld world, BlockFace location, LazyOptional<IFluidHandler> handler) {
-		this.location = location;
-		this.handler = handler;
-		this.handler.addListener($ -> onHandlerInvalidated(world));
-	}
-
-	protected void onHandlerInvalidated(IWorld world) {
-		IFluidHandler tank = handler.orElse(null);
-		if (tank != null)
-			return;
-		TileEntity tileEntity = world.getTileEntity(location.getConnectedPos());
-		if (tileEntity == null)
-			return;
-		LazyOptional<IFluidHandler> capability =
-			tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, location.getOppositeFace());
-		if (capability.isPresent()) {
-			handler = capability;
-			handler.addListener($ -> onHandlerInvalidated(world));
-		}
-	}
-
-	public FluidStack provideFluid() {
-		IFluidHandler tank = provideHandler().orElse(null);
-		if (tank == null)
-			return FluidStack.EMPTY;
-		return tank.drain(1, FluidAction.SIMULATE);
-	}
-
-	public LazyOptional<IFluidHandler> provideHandler() {
-		return handler;
-	}
-
-}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkFlow.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkFlow.java
deleted file mode 100644
index 1324ee41c..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidNetworkFlow.java
+++ /dev/null
@@ -1,306 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
-import com.simibubi.create.foundation.utility.BlockFace;
-import com.simibubi.create.foundation.utility.Iterate;
-
-import net.minecraft.block.BlockState;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.Direction;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IWorld;
-import net.minecraftforge.fluids.FluidStack;
-
-class FluidNetworkFlow {
-
-	@FunctionalInterface
-	static interface PipeFlowConsumer {
-		void accept(FluidPipeBehaviour pipe, Direction face, boolean inbound);
-	}
-
-	/**
-	 * 
-	 */
-	private final FluidNetwork activePipeNetwork;
-	FluidNetworkEndpoint source;
-	FluidStack fluidStack;
-	Set<BlockFace> flowPointers;
-
-	Set<FluidNetworkEndpoint> outputEndpoints;
-	boolean pumpReached;
-
-	boolean pulling;
-	float speed;
-
-	public FluidNetworkFlow(FluidNetwork activePipeNetwork, FluidNetworkEndpoint source, IWorld world,
-		boolean pulling) {
-		this.activePipeNetwork = activePipeNetwork;
-		this.source = source;
-		this.pulling = pulling;
-		flowPointers = new HashSet<>();
-		outputEndpoints = new HashSet<>();
-		fluidStack = FluidStack.EMPTY;
-		tick(world, 0);
-	}
-
-	void resetFlow(IWorld world) {
-		fluidStack = FluidStack.EMPTY;
-		flowPointers.clear();
-		outputEndpoints.clear();
-		pumpReached = false;
-		forEachPipeFlow(world, (pipe, face, inbound) -> pipe.removeFlow(this, face, inbound));
-	}
-
-	void addToSkippedConnections(IWorld world) {
-		forEachPipeFlow(world, (pipe, face, inbound) -> {
-			if (!pipe.getFluid().isFluidEqual(fluidStack))
-				return;
-			BlockFace blockFace = new BlockFace(pipe.getPos(), face);
-			this.activePipeNetwork.previousFlow.put(blockFace, pipe.getFluid());
-		});
-	}
-
-	void forEachPipeFlow(IWorld world, FluidNetworkFlow.PipeFlowConsumer consumer) {
-		Set<BlockFace> flowPointers = new HashSet<>();
-		flowPointers.add(getSource());
-
-		// Update all branches of this flow, and create new ones if necessary
-		while (!flowPointers.isEmpty()) {
-			List<BlockFace> toAdd = new ArrayList<>();
-			for (Iterator<BlockFace> iterator = flowPointers.iterator(); iterator.hasNext();) {
-				BlockFace flowPointer = iterator.next();
-				BlockPos currentPos = flowPointer.getPos();
-				FluidPipeBehaviour pipe = getPipeInTree(world, currentPos);
-				if (pipe == null) {
-					iterator.remove();
-					continue;
-				}
-				Map<Direction, Boolean> directions = this.activePipeNetwork.pipeGraph.get(currentPos)
-					.getSecond();
-				for (Entry<Direction, Boolean> entry : directions.entrySet()) {
-					boolean inbound = entry.getValue() != pulling;
-					Direction face = entry.getKey();
-					if (inbound && face != flowPointer.getFace())
-						continue;
-					consumer.accept(pipe, face, inbound);
-					if (inbound)
-						continue;
-					toAdd.add(new BlockFace(currentPos.offset(face), face.getOpposite()));
-				}
-				iterator.remove();
-			}
-			flowPointers.addAll(toAdd);
-		}
-	}
-
-	void tick(IWorld world, float speed) {
-		boolean skipping = speed == 0;
-		Map<BlockFace, FluidStack> previousFlow = this.activePipeNetwork.previousFlow;
-		if (skipping && previousFlow.isEmpty())
-			return;
-
-		this.speed = speed;
-		FluidStack provideFluid = source.provideFluid();
-		if (!fluidStack.isEmpty() && !fluidStack.isFluidEqual(provideFluid)) {
-			resetFlow(world);
-			return;
-		}
-
-		fluidStack = provideFluid.copy();
-
-		// There is currently no unfinished flow being followed
-		if (flowPointers.isEmpty()) {
-
-			// The fluid source has run out -> reset
-			if (fluidStack.isEmpty()) {
-				if (hasValidTargets())
-					resetFlow(world);
-				return;
-			}
-
-			// Keep the flows if all is well
-			if (hasValidTargets())
-				return;
-
-			// Start a new flow from or towards the pump
-			BlockFace source = getSource();
-			if (tryConnectTo(world, source.getOpposite()))
-				return;
-			flowPointers.add(source);
-		}
-
-		boolean skipped = false;
-		Set<BlockFace> pausedPointers = new HashSet<>();
-
-		do {
-			skipped = false;
-			List<BlockFace> toAdd = null;
-
-			// Update all branches of this flow, and create new ones if necessary
-			for (Iterator<BlockFace> iterator = flowPointers.iterator(); iterator.hasNext();) {
-				BlockFace flowPointer = iterator.next();
-				BlockPos currentPos = flowPointer.getPos();
-
-				if (pausedPointers.contains(flowPointer))
-					continue;
-
-				FluidPipeBehaviour pipe = getPipeInTree(world, currentPos);
-				if (pipe == null) {
-					iterator.remove();
-					continue;
-				}
-
-				Map<Direction, Boolean> directions = this.activePipeNetwork.pipeGraph.get(currentPos)
-					.getSecond();
-				boolean inboundComplete = false;
-				boolean allFlowsComplete = true;
-				BlockState state = world.getBlockState(currentPos);
-
-				// First loop only inbound flows of a pipe to see if they have reached the
-				// center
-				for (boolean inboundPass : Iterate.trueAndFalse) {
-					if (!inboundPass && !inboundComplete)
-						break;
-
-					// For all connections of the pipe tree of the pump
-					for (Entry<Direction, Boolean> entry : directions.entrySet()) {
-						Boolean awayFromPump = entry.getValue();
-						Direction direction = entry.getKey();
-						boolean inbound = awayFromPump != pulling;
-
-						if (inboundPass && direction != flowPointer.getFace())
-							continue;
-						if (!inboundPass && inbound)
-							continue;
-						if (!pipe.canTransferToward(fluidStack, state, direction, inbound))
-							continue;
-
-						BlockFace blockface = new BlockFace(currentPos, direction);
-
-						if (!pipe.hasStartedFlow(this, direction, inbound))
-							pipe.addFlow(this, direction, inbound, false);
-						if (skipping && canSkip(previousFlow, blockface)) {
-							pipe.skipFlow(direction, inbound);
-							FluidPropagator.showBlockFace(blockface)
-								.colored(0x0)
-								.lineWidth(1 / 8f);
-							skipped = true;
-						}
-
-						if (!pipe.hasCompletedFlow(direction, inbound)) {
-							allFlowsComplete = false;
-							continue;
-						}
-
-						if (inboundPass) {
-							inboundComplete = true;
-							continue;
-						}
-
-						// Outward pass, check if any target was reached
-						tryConnectTo(world, blockface);
-					}
-				}
-
-				if (!allFlowsComplete && !skipping)
-					continue;
-
-				// Create a new flow branch at each outward pipe connection
-				for (Entry<Direction, Boolean> entry : directions.entrySet()) {
-					if (entry.getValue() != pulling)
-						continue;
-					Direction face = entry.getKey();
-					if (!pipe.canTransferToward(fluidStack, state, face, false))
-						continue;
-					BlockFace addedBlockFace = new BlockFace(currentPos.offset(face), face.getOpposite());
-					if (skipping && !canSkip(previousFlow, addedBlockFace)) {
-						allFlowsComplete = false;
-						continue;
-					}
-					if (toAdd == null)
-						toAdd = new ArrayList<>();
-					toAdd.add(addedBlockFace);
-				}
-
-				if (!allFlowsComplete && skipping) {
-					pausedPointers.add(flowPointer);
-					continue;
-				}
-
-				iterator.remove();
-
-			} // End of branch loop
-
-			if (toAdd != null)
-				flowPointers.addAll(toAdd);
-
-		} while (skipping && skipped);
-	}
-
-	private boolean canSkip(Map<BlockFace, FluidStack> previousFlow, BlockFace blockface) {
-		return previousFlow.containsKey(blockface) && previousFlow.get(blockface)
-			.isFluidEqual(fluidStack);
-	}
-
-	private boolean tryConnectTo(IWorld world, BlockFace blockface) {
-		// Pulling flow, target is the pump
-		if (pulling) {
-			if (!this.activePipeNetwork.pumpLocation.getOpposite()
-				.equals(blockface))
-				return false;
-			pumpReached = true;
-			TileEntity targetTE = world.getTileEntity(this.activePipeNetwork.pumpLocation.getPos());
-			if (targetTE instanceof PumpTileEntity)
-				((PumpTileEntity) targetTE).setProvidedFluid(fluidStack);
-			FluidPropagator.showBlockFace(this.activePipeNetwork.pumpLocation)
-				.colored(0x799351)
-				.lineWidth(1 / 8f);
-			return true;
-		}
-
-		// Pushing flow, targets are the endpoints
-		for (FluidNetworkEndpoint networkEndpoint : this.activePipeNetwork.targets) {
-			if (!networkEndpoint.location.isEquivalent(blockface))
-				continue;
-			outputEndpoints.add(networkEndpoint);
-			FluidPropagator.showBlockFace(blockface)
-				.colored(0x799351)
-				.lineWidth(1 / 8f);
-			return !(networkEndpoint instanceof InterPumpEndpoint);
-		}
-
-		return false;
-	}
-
-	private BlockFace getSource() {
-		return pulling ? source.location : this.activePipeNetwork.pumpLocation.getOpposite();
-	}
-
-	private FluidPipeBehaviour getPipeInTree(IWorld world, BlockPos currentPos) {
-		if (!world.isAreaLoaded(currentPos, 0))
-			return null;
-		if (!this.activePipeNetwork.pipeGraph.containsKey(currentPos))
-			return null;
-		return TileEntityBehaviour.get(world, currentPos, FluidPipeBehaviour.TYPE);
-	}
-
-	boolean hasValidTargets() {
-		return pumpReached || !outputEndpoints.isEmpty();
-	}
-
-	public float getSpeed() {
-		return speed;
-	}
-
-	public FluidStack getFluidStack() {
-		return fluidStack;
-	}
-}
\ No newline at end of file
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeAttachmentBehaviour.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeAttachmentBehaviour.java
deleted file mode 100644
index 3353b3dc6..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeAttachmentBehaviour.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import com.simibubi.create.AllBlocks;
-import com.simibubi.create.content.contraptions.fluids.pipes.EncasedPipeBlock;
-import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
-import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
-import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType;
-
-import net.minecraft.block.BlockState;
-import net.minecraft.util.Direction;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockDisplayReader;
-
-public class FluidPipeAttachmentBehaviour extends BracketedTileEntityBehaviour {
-
-	public static BehaviourType<FluidPipeAttachmentBehaviour> TYPE = new BehaviourType<>();
-
-	public AttachmentTypes getAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state, Direction direction) {
-		if (!isPipeConnectedTowards(state, direction))
-			return AttachmentTypes.NONE;
-
-		BlockPos offsetPos = pos.offset(direction);
-		BlockState facingState = world.getBlockState(offsetPos);
-
-		if (facingState.getBlock() instanceof PumpBlock && facingState.get(PumpBlock.FACING)
-			.getAxis() == direction.getAxis())
-			return AttachmentTypes.NONE;
-
-		if (AllBlocks.ENCASED_FLUID_PIPE.has(facingState)
-			&& facingState.get(EncasedPipeBlock.FACING_TO_PROPERTY_MAP.get(direction.getOpposite())))
-			return AttachmentTypes.NONE;
-
-		if (FluidPropagator.hasFluidCapability(facingState, world, offsetPos, direction)
-			&& !AllBlocks.HOSE_PULLEY.has(facingState))
-			return AttachmentTypes.DRAIN;
-
-		return AttachmentTypes.RIM;
-	}
-
-	public boolean isPipeConnectedTowards(BlockState state, Direction direction) {
-		FluidPipeBehaviour fluidPipeBehaviour = tileEntity.getBehaviour(FluidPipeBehaviour.TYPE);
-		if (fluidPipeBehaviour == null)
-			return false;
-//		BlockState bracket = getBracket();
-//		if (bracket != Blocks.AIR.getDefaultState() && bracket.get(BracketBlock.FACING) == direction)
-//			return false;
-		return fluidPipeBehaviour.isConnectedTo(state, direction);
-	}
-
-	public static enum AttachmentTypes {
-		NONE, RIM, DRAIN;
-
-		public boolean hasModel() {
-			return this != NONE;
-		}
-	}
-
-	public FluidPipeAttachmentBehaviour(SmartTileEntity te) {
-		super(te);
-	}
-
-	@Override
-	public BehaviourType<?> getType() {
-		return TYPE;
-	}
-
-	@Override
-	public boolean canHaveBracket() {
-		BlockState blockState = tileEntity.getBlockState();
-		if (blockState.getBlock() instanceof PumpBlock)
-			return false;
-		if (blockState.getBlock() instanceof EncasedPipeBlock)
-			return false;
-		return true;
-	}
-
-}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeBehaviour.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeBehaviour.java
deleted file mode 100644
index 80cdc7665..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPipeBehaviour.java
+++ /dev/null
@@ -1,484 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Random;
-import java.util.Set;
-
-import javax.annotation.Nullable;
-
-import com.simibubi.create.AllSpecialTextures;
-import com.simibubi.create.CreateClient;
-import com.simibubi.create.content.contraptions.KineticDebugger;
-import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
-import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
-import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType;
-import com.simibubi.create.foundation.utility.Couple;
-import com.simibubi.create.foundation.utility.Iterate;
-import com.simibubi.create.foundation.utility.LerpedFloat;
-import com.simibubi.create.foundation.utility.LerpedFloat.Chaser;
-import com.simibubi.create.foundation.utility.NBTHelper;
-import com.simibubi.create.foundation.utility.Pair;
-import com.simibubi.create.foundation.utility.VecHelper;
-
-import net.minecraft.block.BlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.entity.Entity;
-import net.minecraft.fluid.Fluid;
-import net.minecraft.fluid.Fluids;
-import net.minecraft.nbt.CompoundNBT;
-import net.minecraft.nbt.ListNBT;
-import net.minecraft.particles.IParticleData;
-import net.minecraft.util.Direction;
-import net.minecraft.util.math.AxisAlignedBB;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.vector.Vector3d;
-import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.common.util.Constants.NBT;
-import net.minecraftforge.fluids.FluidStack;
-import net.minecraftforge.fml.DistExecutor;
-
-public abstract class FluidPipeBehaviour extends TileEntityBehaviour {
-
-	public static BehaviourType<FluidPipeBehaviour> TYPE = new BehaviourType<>();
-	public static final int MAX_PARTICLE_RENDER_DISTANCE = 20;
-	public static final int SPLASH_PARTICLE_AMOUNT = 1;
-	public static final float IDLE_PARTICLE_SPAWN_CHANCE = 1 / 800f;
-	public static final Random r = new Random();
-
-	// Direction -> (inboundflows{}, outwardflows{})
-	Map<Direction, Couple<PipeFlows>> allFlows;
-	FluidStack fluid;
-	Couple<FluidStack> collision;
-
-	public FluidPipeBehaviour(SmartTileEntity te) {
-		super(te);
-		allFlows = new IdentityHashMap<>();
-		fluid = FluidStack.EMPTY;
-	}
-
-	@Override
-	public BehaviourType<?> getType() {
-		return TYPE;
-	}
-
-	public void notifyNetwork() {
-		FluidPropagator.propagateChangedPipe(this.getWorld(), tileEntity.getPos(), tileEntity.getBlockState());
-	}
-
-	public boolean canTransferToward(FluidStack fluid, BlockState state, Direction direction, boolean inbound) {
-		return isConnectedTo(state, direction);
-	}
-
-	public abstract boolean isConnectedTo(BlockState state, Direction direction);
-
-	public float getRimRadius(BlockState state, Direction direction) {
-		return 1 / 4f + 1 / 64f;
-	}
-
-	public boolean hasStartedFlow(FluidNetworkFlow flow, Direction face, boolean inbound) {
-		return allFlows.containsKey(face) && allFlows.get(face)
-			.get(inbound)
-			.hasFlow(flow);
-	}
-
-	public boolean hasCompletedFlow(Direction face, boolean inbound) {
-		return allFlows.containsKey(face) && allFlows.get(face)
-			.get(inbound)
-			.isCompleted();
-	}
-
-	@Override
-	public void write(CompoundNBT compound, boolean client) {
-		compound.put("Fluid", fluid.writeToNBT(new CompoundNBT()));
-		ListNBT flows = new ListNBT();
-		for (Direction face : Iterate.directions)
-			for (boolean inbound : Iterate.trueAndFalse) {
-				LerpedFloat flowProgress = getFlowProgress(face, inbound);
-				if (flowProgress == null)
-					continue;
-				CompoundNBT nbt = new CompoundNBT();
-				NBTHelper.writeEnum(nbt, "Face", face);
-				nbt.putBoolean("In", inbound);
-				PipeFlows pipeFlows = allFlows.get(face)
-					.get(inbound);
-				Set<FluidNetworkFlow> participants = pipeFlows.participants;
-				nbt.putBoolean("Silent", participants == null || participants.isEmpty());
-				nbt.put("Progress", flowProgress.writeNBT());
-
-				if (client)
-					nbt.putFloat("Strength", pipeFlows.bestFlowStrength);
-
-				flows.add(nbt);
-			}
-		compound.put("Flows", flows);
-	}
-
-	@Override
-	public void read(CompoundNBT compound, boolean client) {
-		fluid = FluidStack.loadFluidStackFromNBT(compound.getCompound("Fluid"));
-
-		if (client) {
-			for (Direction face : Iterate.directions)
-				if (allFlows.containsKey(face))
-					allFlows.get(face)
-						.forEach(pf -> pf.progress = null);
-		}
-
-		NBTHelper.iterateCompoundList(compound.getList("Flows", NBT.TAG_COMPOUND), nbt -> {
-			Direction face = NBTHelper.readEnum(nbt, "Face", Direction.class);
-			boolean inbound = nbt.getBoolean("In");
-			LerpedFloat progress = createFlowProgress(0);
-			progress.readNBT(nbt.getCompound("Progress"), false);
-			addFlow(null, face, inbound, nbt.getBoolean("Silent"));
-			setFlowProgress(face, inbound, progress);
-			if (client)
-				setVisualFlowStrength(face, inbound, nbt.getFloat("Strength"));
-		});
-
-		if (!client)
-			return;
-
-		for (Direction face : Iterate.directions) {
-			if (!allFlows.containsKey(face))
-				return;
-			Couple<PipeFlows> couple = allFlows.get(face);
-			if (couple.get(true).progress == null && couple.get(false).progress == null)
-				allFlows.remove(face);
-			if (allFlows.isEmpty())
-				clear();
-		}
-	}
-
-	public void addFlow(@Nullable FluidNetworkFlow flow, Direction face, boolean inbound, boolean silent) {
-		if (flow != null) {
-			FluidStack fluid = flow.getFluidStack();
-			if (!this.fluid.isEmpty() && !fluid.isFluidEqual(this.fluid)) {
-				collision = Couple.create(this.fluid, fluid);
-				return;
-			}
-			this.fluid = fluid;
-		}
-
-		if (!allFlows.containsKey(face)) {
-			allFlows.put(face, Couple.create(PipeFlows::new));
-			if (inbound && !silent)
-				spawnSplashOnRim(face);
-		}
-
-		if (flow != null) {
-			PipeFlows flows = allFlows.get(face)
-				.get(inbound);
-			flows.addFlow(flow);
-			contentsChanged();
-		}
-	}
-
-	public void removeFlow(FluidNetworkFlow flow, Direction face, boolean inbound) {
-		if (!allFlows.containsKey(face))
-			return;
-		Couple<PipeFlows> couple = allFlows.get(face);
-		couple.get(inbound)
-			.removeFlow(flow);
-		contentsChanged();
-		if (!couple.get(true)
-			.isActive()
-			&& !couple.get(false)
-				.isActive())
-			allFlows.remove(face);
-		if (allFlows.isEmpty())
-			clear();
-	}
-
-	public void setVisualFlowStrength(Direction face, boolean inbound, float strength) {
-		if (!allFlows.containsKey(face))
-			return;
-		allFlows.get(face)
-			.get(inbound).bestFlowStrength = strength;
-	}
-
-	public void setFlowProgress(Direction face, boolean inbound, LerpedFloat progress) {
-		if (!allFlows.containsKey(face))
-			return;
-		allFlows.get(face)
-			.get(inbound).progress = progress;
-	}
-
-	public LerpedFloat getFlowProgress(Direction face, boolean inbound) {
-		if (!allFlows.containsKey(face))
-			return null;
-		return allFlows.get(face)
-			.get(inbound).progress;
-	}
-
-	public void skipFlow(Direction face, boolean inbound) {
-		if (!allFlows.containsKey(face))
-			return;
-		Couple<PipeFlows> couple = allFlows.get(face);
-		couple.get(inbound)
-			.skip();
-	}
-
-	public void clear() {
-		allFlows.clear();
-		fluid = FluidStack.EMPTY;
-		contentsChanged();
-	}
-
-	public void spawnSplashOnRim(Direction face) {
-		DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> spawnSplashOnRimInner(face));
-	}
-
-	public void spawnParticles() {
-		DistExecutor.runWhenOn(Dist.CLIENT, () -> this::spawnParticlesInner);
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	private void spawnParticlesInner() {
-		if (!isRenderEntityWithinDistance(tileEntity.getPos()))
-			return;
-		if (fluid.isEmpty())
-			return;
-
-		World world = Minecraft.getInstance().world;
-		BlockPos pos = tileEntity.getPos();
-		BlockState state = world.getBlockState(pos);
-
-		for (Direction face : Iterate.directions) {
-			boolean open = FluidPropagator.isOpenEnd(world, pos, face);
-			if (isConnectedTo(state, face)) {
-				if (open) {
-					spawnPouringLiquid(world, state, fluid, face, 1);
-					continue;
-				}
-				if (r.nextFloat() < IDLE_PARTICLE_SPAWN_CHANCE)
-					spawnRimParticles(world, state, fluid, face, 1);
-			}
-		}
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	private void spawnSplashOnRimInner(Direction face) {
-		if (!isRenderEntityWithinDistance(tileEntity.getPos()))
-			return;
-		if (fluid.isEmpty())
-			return;
-		World world = Minecraft.getInstance().world;
-		BlockPos pos = tileEntity.getPos();
-		BlockState state = world.getBlockState(pos);
-		spawnRimParticles(world, state, fluid, face, SPLASH_PARTICLE_AMOUNT);
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	private void spawnRimParticles(World world, BlockState state, FluidStack fluid, Direction side, int amount) {
-		BlockPos pos = tileEntity.getPos();
-		if (FluidPropagator.isOpenEnd(world, pos, side)) {
-			spawnPouringLiquid(world, state, fluid, side, amount);
-			return;
-		}
-
-		IParticleData particle = FluidFX.getDrippingParticle(fluid);
-		float rimRadius = getRimRadius(state, side);
-		FluidFX.spawnRimParticles(world, pos, side, amount, particle, rimRadius);
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	private void spawnPouringLiquid(World world, BlockState state, FluidStack fluid, Direction side, int amount) {
-		IParticleData particle = FluidFX.getFluidParticle(fluid);
-		float rimRadius = getRimRadius(state, side);
-		Vector3d directionVec = Vector3d.of(side.getDirectionVec());
-		BlockPos pos = tileEntity.getPos();
-
-		Couple<PipeFlows> couple = allFlows.get(side);
-		if (couple == null)
-			return;
-
-		couple.forEachWithContext((flow, inbound) -> {
-			if (flow.progress == null)
-				return;
-			FluidFX.spawnPouringLiquid(world, pos, amount, particle, rimRadius, directionVec, inbound);
-		});
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	public static boolean isRenderEntityWithinDistance(BlockPos pos) {
-		Entity renderViewEntity = Minecraft.getInstance()
-			.getRenderViewEntity();
-		if (renderViewEntity == null)
-			return false;
-		Vector3d center = VecHelper.getCenterOf(pos);
-		if (renderViewEntity.getPositionVec()
-			.distanceTo(center) > MAX_PARTICLE_RENDER_DISTANCE)
-			return false;
-		return true;
-	}
-
-	static AxisAlignedBB smallCenter = new AxisAlignedBB(BlockPos.ZERO).shrink(.25);
-
-	@Override
-	public void tick() {
-		super.tick();
-		boolean isRemote = getWorld().isRemote;
-
-		allFlows.values()
-			.forEach(c -> c.forEach(pf -> pf.tick(isRemote)));
-
-		if (isRemote) {
-			clientTick();
-			return;
-		}
-
-		if (collision != null) {
-			FluidReactions.handlePipeFlowCollision(getWorld(), tileEntity.getPos(), collision.getFirst(),
-				collision.getSecond());
-			collision = null;
-			return;
-		}
-	}
-
-	public Pair<Boolean, LerpedFloat> getStrogestFlow(Direction side) {
-		Couple<PipeFlows> couple = allFlows.get(side);
-		if (couple == null)
-			return null;
-
-		PipeFlows in = couple.get(true);
-		PipeFlows out = couple.get(false);
-		Couple<LerpedFloat> progress = couple.map(pf -> pf.progress);
-		boolean inboundStronger = false;
-
-		if (in.isCompleted() != out.isCompleted()) {
-			inboundStronger = in.isCompleted();
-		} else if ((progress.get(true) == null) != (progress.get(false) == null)) {
-			inboundStronger = progress.get(true) != null;
-		} else {
-			if (progress.get(true) != null)
-				inboundStronger = in.bestFlowStrength > out.bestFlowStrength;
-		}
-
-		return Pair.of(inboundStronger, progress.get(inboundStronger));
-	}
-
-	private void clientTick() {
-		spawnParticles();
-
-		if (!KineticDebugger.isActive())
-			return;
-		if (fluid.isEmpty())
-			return;
-		for (Entry<Direction, Couple<PipeFlows>> entry : allFlows.entrySet()) {
-			Direction face = entry.getKey();
-			Vector3d directionVec = Vector3d.of(face.getDirectionVec());
-			float size = 1 / 4f;
-			boolean extended = !isConnectedTo(tileEntity.getBlockState(), face.getOpposite());
-			float length = extended ? .75f : .5f;
-
-			entry.getValue()
-				.forEachWithContext((flow, inbound) -> {
-					if (flow.progress == null)
-						return;
-					float value = flow.progress.getValue();
-					Vector3d start = directionVec.scale(inbound ? .5 : .5f - length);
-					Vector3d offset = directionVec.scale(length * (inbound ? -1 : 1))
-						.scale(value);
-
-					Vector3d scale = new Vector3d(1, 1, 1).subtract(directionVec.scale(face.getAxisDirection()
-						.getOffset()))
-						.scale(size);
-					AxisAlignedBB bb =
-						new AxisAlignedBB(start, start.add(offset)).offset(VecHelper.getCenterOf(tileEntity.getPos()))
-							.grow(scale.x, scale.y, scale.z);
-
-					int color = 0x7fdbda;
-					if (!fluid.isEmpty()) {
-						Fluid fluid2 = fluid.getFluid();
-						if (fluid2 == Fluids.WATER)
-							color = 0x1D4D9B;
-						if (fluid2 == Fluids.LAVA)
-							color = 0xFF773D;
-					}
-
-					CreateClient.outliner.chaseAABB(Pair.of(this, face), bb)
-						.withFaceTexture(AllSpecialTextures.CUTOUT_CHECKERED)
-						.colored(color)
-						.lineWidth(1 / 16f);
-				});
-		}
-	}
-
-	private void contentsChanged() {
-		tileEntity.markDirty();
-		tileEntity.sendData();
-	}
-
-	private LerpedFloat createFlowProgress(double speed) {
-		return LerpedFloat.linear()
-			.startWithValue(0)
-			.chase(1, speed, Chaser.LINEAR);
-	}
-
-	public FluidStack getFluid() {
-		return fluid;
-	}
-
-	class PipeFlows {
-		LerpedFloat progress;
-		Set<FluidNetworkFlow> participants;
-		float bestFlowStrength;
-
-		void addFlow(FluidNetworkFlow flow) {
-			if (participants == null)
-				participants = new HashSet<>();
-			participants.add(flow);
-
-			if (progress == null) {
-				progress = createFlowProgress(flow.getSpeed());
-			}
-		}
-
-		boolean hasFlow(FluidNetworkFlow flow) {
-			return participants != null && participants.contains(flow);
-		}
-
-		void tick(boolean onClient) {
-			if (progress == null)
-				return;
-			if (!onClient) {
-				if (participants == null)
-					return;
-				bestFlowStrength = 0;
-				for (FluidNetworkFlow networkFlow : participants)
-					bestFlowStrength = Math.max(bestFlowStrength, networkFlow.getSpeed());
-				if (isCompleted())
-					return;
-				if (progress.updateChaseSpeed(bestFlowStrength))
-					contentsChanged();
-			}
-			progress.tickChaser();
-		}
-
-		void skip() {
-			progress = LerpedFloat.linear()
-				.startWithValue(1);
-		}
-
-		void removeFlow(FluidNetworkFlow flow) {
-			if (participants == null)
-				return;
-			participants.remove(flow);
-		}
-
-		boolean isActive() {
-			return participants != null && !participants.isEmpty();
-		}
-
-		boolean isCompleted() {
-			return progress != null && progress.getValue() == 1;
-		}
-
-	}
-
-}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPropagator.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPropagator.java
index 751cc967a..b153b1e68 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPropagator.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidPropagator.java
@@ -7,17 +7,15 @@ import java.util.Set;
 
 import javax.annotation.Nullable;
 
-import org.apache.commons.lang3.mutable.MutableObject;
-
 import com.simibubi.create.AllBlocks;
+import com.simibubi.create.content.contraptions.fluids.PipeConnection.Flow;
 import com.simibubi.create.content.contraptions.fluids.pipes.AxisPipeBlock;
 import com.simibubi.create.content.contraptions.fluids.pipes.FluidPipeBlock;
 import com.simibubi.create.foundation.config.AllConfigs;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
-import com.simibubi.create.foundation.utility.BlockFace;
 import com.simibubi.create.foundation.utility.BlockHelper;
 import com.simibubi.create.foundation.utility.Iterate;
-import com.simibubi.create.foundation.utility.outliner.Outline.OutlineParams;
+import com.simibubi.create.foundation.utility.Pair;
 
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
@@ -27,7 +25,6 @@ import net.minecraft.tileentity.TileEntity;
 import net.minecraft.util.Direction;
 import net.minecraft.util.Direction.Axis;
 import net.minecraft.util.Direction.AxisDirection;
-import net.minecraft.util.math.AxisAlignedBB;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.IBlockReader;
 import net.minecraft.world.IWorld;
@@ -36,6 +33,94 @@ import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
 
 public class FluidPropagator {
 
+	public static void propagateChangedPipe(IWorld world, BlockPos pipePos, BlockState pipeState) {
+		List<Pair<Integer, BlockPos>> frontier = new ArrayList<>();
+		Set<BlockPos> visited = new HashSet<>();
+		Set<Pair<PumpTileEntity, Direction>> discoveredPumps = new HashSet<>();
+
+		frontier.add(Pair.of(0, pipePos));
+
+		// Visit all connected pumps to update their network
+		while (!frontier.isEmpty()) {
+			Pair<Integer, BlockPos> pair = frontier.remove(0);
+			BlockPos currentPos = pair.getSecond();
+			if (visited.contains(currentPos))
+				continue;
+			visited.add(currentPos);
+			BlockState currentState = currentPos.equals(pipePos) ? pipeState : world.getBlockState(currentPos);
+			FluidTransportBehaviour pipe = getPipe(world, currentPos);
+			if (pipe == null)
+				continue;
+			pipe.wipePressure();
+
+			for (Direction direction : getPipeConnections(currentState, pipe)) {
+				BlockPos target = currentPos.offset(direction);
+				if (!world.isAreaLoaded(target, 0))
+					continue;
+
+				TileEntity tileEntity = world.getTileEntity(target);
+				BlockState targetState = world.getBlockState(target);
+				if (tileEntity instanceof PumpTileEntity) {
+					if (!AllBlocks.MECHANICAL_PUMP.has(targetState) || targetState.get(PumpBlock.FACING)
+						.getAxis() != direction.getAxis())
+						continue;
+					discoveredPumps.add(Pair.of((PumpTileEntity) tileEntity, direction.getOpposite()));
+					continue;
+				}
+				if (visited.contains(target))
+					continue;
+				FluidTransportBehaviour targetPipe = getPipe(world, target);
+				if (targetPipe == null)
+					continue;
+				Integer distance = pair.getFirst();
+				if (distance >= getPumpRange() && !targetPipe.hasAnyPressure())
+					continue;
+				if (targetPipe.canHaveFlowToward(targetState, direction.getOpposite()))
+					frontier.add(Pair.of(distance + 1, target));
+			}
+		}
+
+		discoveredPumps.forEach(pair -> pair.getFirst()
+			.updatePipesOnSide(pair.getSecond()));
+	}
+
+	public static void resetAffectedFluidNetworks(World world, BlockPos start, Direction side) {
+		List<BlockPos> frontier = new ArrayList<>();
+		Set<BlockPos> visited = new HashSet<>();
+		frontier.add(start);
+
+		while (!frontier.isEmpty()) {
+			BlockPos pos = frontier.remove(0);
+			if (visited.contains(pos))
+				continue;
+			visited.add(pos);
+			FluidTransportBehaviour pipe = getPipe(world, pos);
+			if (pipe == null)
+				continue;
+
+			for (Direction d : Iterate.directions) {
+				if (pos.equals(start) && d != side)
+					continue;
+				BlockPos target = pos.offset(d);
+				if (visited.contains(target))
+					continue;
+
+				PipeConnection connection = pipe.getConnection(d);
+				if (connection == null)
+					continue;
+				if (!connection.hasFlow())
+					continue;
+
+				Flow flow = connection.flow.get();
+				if (!flow.inbound)
+					continue;
+
+				connection.resetNetwork();
+				frontier.add(target);
+			}
+		}
+	}
+
 	public static Direction validateNeighbourChange(BlockState state, World world, BlockPos pos, Block otherBlock,
 		BlockPos neighborPos, boolean isMoving) {
 		if (world.isRemote)
@@ -59,15 +144,15 @@ public class FluidPropagator {
 		return null;
 	}
 
-	public static FluidPipeBehaviour getPipe(IBlockReader reader, BlockPos pos) {
-		return TileEntityBehaviour.get(reader, pos, FluidPipeBehaviour.TYPE);
+	public static FluidTransportBehaviour getPipe(IBlockReader reader, BlockPos pos) {
+		return TileEntityBehaviour.get(reader, pos, FluidTransportBehaviour.TYPE);
 	}
 
 	public static boolean isOpenEnd(IBlockReader reader, BlockPos pos, Direction side) {
 		BlockPos connectedPos = pos.offset(side);
 		BlockState connectedState = reader.getBlockState(connectedPos);
-		FluidPipeBehaviour pipe = FluidPropagator.getPipe(reader, connectedPos);
-		if (pipe != null && pipe.isConnectedTo(connectedState, side.getOpposite()))
+		FluidTransportBehaviour pipe = FluidPropagator.getPipe(reader, connectedPos);
+		if (pipe != null && pipe.canHaveFlowToward(connectedState, side.getOpposite()))
 			return false;
 		if (PumpBlock.isPump(connectedState) && connectedState.get(PumpBlock.FACING)
 			.getAxis() == side.getAxis())
@@ -81,52 +166,10 @@ public class FluidPropagator {
 		return true;
 	}
 
-	public static void propagateChangedPipe(IWorld world, BlockPos pipePos, BlockState pipeState) {
-		List<BlockPos> frontier = new ArrayList<>();
-		Set<BlockPos> visited = new HashSet<>();
-
-		frontier.add(pipePos);
-
-		// Visit all connected pumps to update their network
-		while (!frontier.isEmpty()) {
-			BlockPos currentPos = frontier.remove(0);
-			if (visited.contains(currentPos))
-				continue;
-			visited.add(currentPos);
-			BlockState currentState = currentPos.equals(pipePos) ? pipeState : world.getBlockState(currentPos);
-			FluidPipeBehaviour pipe = getPipe(world, currentPos);
-			if (pipe == null)
-				continue;
-			for (Direction direction : getPipeConnections(currentState, pipe)) {
-				BlockPos target = currentPos.offset(direction);
-				if (!world.isAreaLoaded(target, 0))
-					continue;
-
-				TileEntity tileEntity = world.getTileEntity(target);
-				BlockState targetState = world.getBlockState(target);
-				if (tileEntity instanceof PumpTileEntity) {
-					if (!AllBlocks.MECHANICAL_PUMP.has(targetState) || targetState.get(PumpBlock.FACING)
-						.getAxis() != direction.getAxis())
-						continue;
-					PumpTileEntity pump = (PumpTileEntity) tileEntity;
-					pump.updatePipesOnSide(direction.getOpposite());
-					continue;
-				}
-				if (visited.contains(target))
-					continue;
-				FluidPipeBehaviour targetPipe = getPipe(world, target);
-				if (targetPipe == null)
-					continue;
-				if (targetPipe.isConnectedTo(targetState, direction.getOpposite()))
-					frontier.add(target);
-			}
-		}
-	}
-
-	public static List<Direction> getPipeConnections(BlockState state, FluidPipeBehaviour pipe) {
+	public static List<Direction> getPipeConnections(BlockState state, FluidTransportBehaviour pipe) {
 		List<Direction> list = new ArrayList<>();
 		for (Direction d : Iterate.directions)
-			if (pipe.isConnectedTo(state, d))
+			if (pipe.canHaveFlowToward(state, d))
 				list.add(d);
 		return list;
 	}
@@ -135,37 +178,38 @@ public class FluidPropagator {
 		return AllConfigs.SERVER.fluids.mechanicalPumpRange.get();
 	}
 
-	@Deprecated // Remove after pipes are fixed; comment out for production
-	public static OutlineParams showBlockFace(BlockFace face) {
-		MutableObject<OutlineParams> params = new MutableObject<>(new OutlineParams());
+//	static AxisAlignedBB smallCenter = new AxisAlignedBB(BlockPos.ZERO).shrink(.25);
+//	
+//	@Deprecated 
+//	public static OutlineParams showBlockFace(BlockFace face) {
+//		MutableObject<OutlineParams> params = new MutableObject<>(new OutlineParams());
 //		DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> {
 //			Vector3d directionVec = new Vector3d(face.getFace()
 //				.getDirectionVec());
 //			Vector3d scaleVec = directionVec.scale(-.25f * face.getFace()
 //				.getAxisDirection()
 //				.getOffset());
-//			directionVec = directionVec.scale(.5f);
+//			directionVec = directionVec.scale(.45f);
 //			params.setValue(CreateClient.outliner.showAABB(face,
 //				FluidPropagator.smallCenter.offset(directionVec.add(new Vector3d(face.getPos())))
 //					.grow(scaleVec.x, scaleVec.y, scaleVec.z)
 //					.grow(1 / 16f)));
 //		});
-		return params.getValue();
-	}
+//		return params.getValue()
+//			.lineWidth(1 / 16f);
+//	}
 
-	static AxisAlignedBB smallCenter = new AxisAlignedBB(BlockPos.ZERO).shrink(.25);
-
-	public static boolean hasFluidCapability(BlockState state, IBlockReader world, BlockPos pos, Direction blockFace) {
-		if (!state.hasTileEntity())
-			return false;
+	public static boolean hasFluidCapability(IBlockReader world, BlockPos pos, Direction side) {
 		TileEntity tileEntity = world.getTileEntity(pos);
-		return tileEntity != null
-			&& tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, blockFace.getOpposite())
-				.isPresent();
+		return tileEntity != null && tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, side)
+			.isPresent();
 	}
 
 	@Nullable
 	public static Axis getStraightPipeAxis(BlockState state) {
+		if (state.getBlock() instanceof PumpBlock)
+			return state.get(PumpBlock.FACING)
+				.getAxis();
 		if (state.getBlock() instanceof AxisPipeBlock)
 			return state.get(AxisPipeBlock.AXIS);
 		if (!FluidPipeBlock.isPipe(state))
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidReactions.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidReactions.java
index 0aa9e93c7..945849fb0 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidReactions.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidReactions.java
@@ -1,12 +1,15 @@
 package com.simibubi.create.content.contraptions.fluids;
 
+import com.simibubi.create.AllFluids;
 import com.simibubi.create.foundation.fluid.FluidHelper;
 import com.simibubi.create.foundation.utility.BlockHelper;
 
+import net.minecraft.block.BlockState;
 import net.minecraft.block.Blocks;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.fluid.FluidState;
 import net.minecraft.fluid.Fluids;
+import net.minecraft.tags.FluidTags;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 import net.minecraftforge.fluids.FluidStack;
@@ -19,19 +22,41 @@ public class FluidReactions {
 		BlockHelper.destroyBlock(world, pos, 1);
 		if (f1 == Fluids.WATER && f2 == Fluids.LAVA || f2 == Fluids.WATER && f1 == Fluids.LAVA)
 			world.setBlockState(pos, Blocks.COBBLESTONE.getDefaultState());
+		else if (f1 == Fluids.LAVA && FluidHelper.hasBlockState(f2)) {
+			BlockState lavaInteraction = AllFluids.getLavaInteraction(FluidHelper.convertToFlowing(f2)
+				.getDefaultState());
+			if (lavaInteraction != null)
+				world.setBlockState(pos, lavaInteraction);
+		} else if (f2 == Fluids.LAVA && FluidHelper.hasBlockState(f1)) {
+			BlockState lavaInteraction = AllFluids.getLavaInteraction(FluidHelper.convertToFlowing(f1)
+				.getDefaultState());
+			if (lavaInteraction != null)
+				world.setBlockState(pos, lavaInteraction);
+		}
 	}
 
 	public static void handlePipeSpillCollision(World world, BlockPos pos, Fluid pipeFluid, FluidState worldFluid) {
 		Fluid pf = FluidHelper.convertToStill(pipeFluid);
 		Fluid wf = worldFluid.getFluid();
-		if (pf == Fluids.WATER && wf == Fluids.LAVA)
+		if (pf.isIn(FluidTags.WATER) && wf == Fluids.LAVA)
 			world.setBlockState(pos, Blocks.OBSIDIAN.getDefaultState());
-		if (pf == Fluids.WATER && wf == Fluids.FLOWING_LAVA)
+		else if (pf == Fluids.WATER && wf == Fluids.FLOWING_LAVA)
 			world.setBlockState(pos, Blocks.COBBLESTONE.getDefaultState());
 		else if (pf == Fluids.LAVA && wf == Fluids.WATER)
 			world.setBlockState(pos, Blocks.STONE.getDefaultState());
 		else if (pf == Fluids.LAVA && wf == Fluids.FLOWING_WATER)
 			world.setBlockState(pos, Blocks.COBBLESTONE.getDefaultState());
+
+		if (pf == Fluids.LAVA) {
+			BlockState lavaInteraction = AllFluids.getLavaInteraction(worldFluid);
+			if (lavaInteraction != null)
+				world.setBlockState(pos, lavaInteraction);
+		} else if (wf == Fluids.FLOWING_LAVA && FluidHelper.hasBlockState(pf)) {
+			BlockState lavaInteraction = AllFluids.getLavaInteraction(FluidHelper.convertToFlowing(pf)
+				.getDefaultState());
+			if (lavaInteraction != null)
+				world.setBlockState(pos, lavaInteraction);
+		}
 	}
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidTransportBehaviour.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidTransportBehaviour.java
new file mode 100644
index 000000000..2e76257ff
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/FluidTransportBehaviour.java
@@ -0,0 +1,266 @@
+package com.simibubi.create.content.contraptions.fluids;
+
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+import com.simibubi.create.AllBlocks;
+import com.simibubi.create.content.contraptions.fluids.pipes.EncasedPipeBlock;
+import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
+import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
+import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType;
+import com.simibubi.create.foundation.utility.Iterate;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.ILightReader;
+import net.minecraft.world.World;
+import net.minecraftforge.fluids.FluidStack;
+
+public abstract class FluidTransportBehaviour extends TileEntityBehaviour {
+
+	public static BehaviourType<FluidTransportBehaviour> TYPE = new BehaviourType<>();
+
+	enum UpdatePhase {
+		WAIT_FOR_PUMPS, // Do not run Layer II logic while pumps could still be distributing pressure
+		FLIP_FLOWS, // Do not cut any flows until all pipes had a chance to reverse them
+		IDLE; // Operate normally
+	}
+
+	Map<Direction, PipeConnection> interfaces;
+	UpdatePhase phase;
+
+	public FluidTransportBehaviour(SmartTileEntity te) {
+		super(te);
+		phase = UpdatePhase.WAIT_FOR_PUMPS;
+	}
+
+	public boolean canPullFluidFrom(FluidStack fluid, BlockState state, Direction direction) {
+		return true;
+	}
+
+	public abstract boolean canHaveFlowToward(BlockState state, Direction direction);
+
+	@Override
+	public void initialize() {
+		super.initialize();
+		createConnectionData();
+	}
+
+	@Override
+	public void tick() {
+		super.tick();
+		World world = getWorld();
+		BlockPos pos = getPos();
+		boolean onClient = world.isRemote;
+		Collection<PipeConnection> connections = interfaces.values();
+
+		// Do not provide a lone pipe connection with its own flow input
+		PipeConnection singleSource = null;
+
+//		if (onClient) {
+//			connections.forEach(connection -> {
+//				connection.visualizeFlow(pos);
+//				connection.visualizePressure(pos);
+//			});
+//		}
+
+		if (phase == UpdatePhase.WAIT_FOR_PUMPS) {
+			phase = UpdatePhase.FLIP_FLOWS;
+			return;
+		}
+
+		if (!onClient) {
+			boolean sendUpdate = false;
+			for (PipeConnection connection : connections) {
+				sendUpdate |= connection.flipFlowsIfPressureReversed();
+				connection.manageSource(world, pos);
+			}
+			if (sendUpdate)
+				tileEntity.notifyUpdate();
+		}
+
+		if (phase == UpdatePhase.FLIP_FLOWS) {
+			phase = UpdatePhase.IDLE;
+			return;
+		}
+
+		if (!onClient) {
+			FluidStack availableFlow = FluidStack.EMPTY;
+			FluidStack collidingFlow = FluidStack.EMPTY;
+
+			for (PipeConnection connection : connections) {
+				FluidStack fluidInFlow = connection.getProvidedFluid();
+				if (fluidInFlow.isEmpty())
+					continue;
+				if (availableFlow.isEmpty()) {
+					singleSource = connection;
+					availableFlow = fluidInFlow;
+					continue;
+				}
+				if (availableFlow.isFluidEqual(fluidInFlow)) {
+					singleSource = null;
+					availableFlow = fluidInFlow;
+					continue;
+				}
+				collidingFlow = fluidInFlow;
+				break;
+			}
+
+			if (!collidingFlow.isEmpty()) {
+				FluidReactions.handlePipeFlowCollision(world, pos, availableFlow, collidingFlow);
+				return;
+			}
+
+			boolean sendUpdate = false;
+			for (PipeConnection connection : connections) {
+				FluidStack internalFluid = singleSource != connection ? availableFlow : FluidStack.EMPTY;
+				Predicate<FluidStack> extractionPredicate =
+					extracted -> canPullFluidFrom(extracted, tileEntity.getBlockState(), connection.side);
+				sendUpdate |= connection.manageFlows(world, pos, internalFluid, extractionPredicate);
+			}
+
+			if (sendUpdate)
+				tileEntity.notifyUpdate();
+		}
+
+		for (PipeConnection connection : connections)
+			connection.tickFlowProgress(world, pos);
+	}
+
+	@Override
+	public void read(CompoundNBT nbt, boolean clientPacket) {
+		super.read(nbt, clientPacket);
+		if (interfaces == null)
+			interfaces = new IdentityHashMap<>();
+		for (Direction face : Iterate.directions)
+			if (nbt.contains(face.getName()))
+				interfaces.computeIfAbsent(face, d -> new PipeConnection(d));
+
+		// Invalid data (missing/outdated). Defer init to runtime
+		if (interfaces.isEmpty()) {
+			interfaces = null;
+			return;
+		}
+
+		interfaces.values()
+			.forEach(connection -> connection.deserializeNBT(nbt, clientPacket));
+	}
+
+	@Override
+	public void write(CompoundNBT nbt, boolean clientPacket) {
+		super.write(nbt, clientPacket);
+		if (clientPacket)
+			createConnectionData();
+		if (interfaces == null)
+			return;
+
+		interfaces.values()
+			.forEach(connection -> connection.serializeNBT(nbt, clientPacket));
+	}
+
+	public FluidStack getProvidedOutwardFluid(Direction side) {
+		createConnectionData();
+		if (!interfaces.containsKey(side))
+			return FluidStack.EMPTY;
+		return interfaces.get(side)
+			.provideOutboundFlow();
+	}
+
+	@Nullable
+	public PipeConnection getConnection(Direction side) {
+		createConnectionData();
+		return interfaces.get(side);
+	}
+
+	public boolean hasAnyPressure() {
+		createConnectionData();
+		for (PipeConnection pipeConnection : interfaces.values())
+			if (pipeConnection.hasPressure())
+				return true;
+		return false;
+	}
+
+	@Nullable
+	public PipeConnection.Flow getFlow(Direction side) {
+		createConnectionData();
+		if (!interfaces.containsKey(side))
+			return null;
+		return interfaces.get(side).flow.orElse(null);
+	}
+
+	public void addPressure(Direction side, boolean inbound, float pressure) {
+		createConnectionData();
+		if (!interfaces.containsKey(side))
+			return;
+		interfaces.get(side)
+			.addPressure(inbound, pressure);
+		tileEntity.sendData();
+	}
+
+	public void wipePressure() {
+		if (interfaces != null)
+			for (Direction d : Iterate.directions) {
+				if (!canHaveFlowToward(tileEntity.getBlockState(), d))
+					interfaces.remove(d);
+				else
+					interfaces.computeIfAbsent(d, PipeConnection::new);
+			}
+		phase = UpdatePhase.WAIT_FOR_PUMPS;
+		createConnectionData();
+		interfaces.values()
+			.forEach(PipeConnection::wipePressure);
+		tileEntity.sendData();
+	}
+
+	private void createConnectionData() {
+		if (interfaces != null)
+			return;
+		interfaces = new IdentityHashMap<>();
+		for (Direction d : Iterate.directions)
+			if (canHaveFlowToward(tileEntity.getBlockState(), d))
+				interfaces.put(d, new PipeConnection(d));
+	}
+
+	public AttachmentTypes getRenderedRimAttachment(ILightReader world, BlockPos pos, BlockState state,
+		Direction direction) {
+		if (!canHaveFlowToward(state, direction))
+			return AttachmentTypes.NONE;
+
+		BlockPos offsetPos = pos.offset(direction);
+		BlockState facingState = world.getBlockState(offsetPos);
+
+		if (facingState.getBlock() instanceof PumpBlock && facingState.get(PumpBlock.FACING)
+			.getAxis() == direction.getAxis())
+			return AttachmentTypes.NONE;
+
+		if (AllBlocks.ENCASED_FLUID_PIPE.has(facingState)
+			&& facingState.get(EncasedPipeBlock.FACING_TO_PROPERTY_MAP.get(direction.getOpposite())))
+			return AttachmentTypes.NONE;
+
+		if (FluidPropagator.hasFluidCapability(world, offsetPos, direction.getOpposite())
+			&& !AllBlocks.HOSE_PULLEY.has(facingState))
+			return AttachmentTypes.DRAIN;
+
+		return AttachmentTypes.RIM;
+	}
+
+	public static enum AttachmentTypes {
+		NONE, RIM, DRAIN;
+
+		public boolean hasModel() {
+			return this != NONE;
+		}
+	}
+
+	@Override
+	public BehaviourType<?> getType() {
+		return TYPE;
+	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpEndpoint.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpEndpoint.java
deleted file mode 100644
index 62904f74b..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpEndpoint.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import java.lang.ref.WeakReference;
-
-import com.simibubi.create.foundation.utility.BlockFace;
-import com.simibubi.create.foundation.utility.Couple;
-import com.simibubi.create.foundation.utility.Iterate;
-import com.simibubi.create.foundation.utility.Pair;
-
-import net.minecraft.world.IWorld;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.fluids.FluidStack;
-import net.minecraftforge.fluids.capability.IFluidHandler;
-
-public class InterPumpEndpoint extends FluidNetworkEndpoint {
-
-	Couple<Pair<BlockFace, WeakReference<PumpTileEntity>>> pumps;
-
-	private InterPumpEndpoint(IWorld world, BlockFace location, LazyOptional<IFluidHandler> handler) {
-		super(world, location, handler);
-	}
-
-	public InterPumpEndpoint(IWorld world, BlockFace location, PumpTileEntity source, PumpTileEntity interfaced,
-		BlockFace sourcePos, BlockFace interfacedPos) {
-		this(world, location, LazyOptional.empty());
-		handler = LazyOptional.of(() -> new InterPumpFluidHandler(this));
-		pumps = Couple.create(Pair.of(sourcePos, new WeakReference<>(source)),
-			Pair.of(interfacedPos, new WeakReference<>(interfaced)));
-	}
-
-	public InterPumpEndpoint opposite(IWorld world) {
-		InterPumpEndpoint interPumpEndpoint = new InterPumpEndpoint(world, this.location.getOpposite(), handler);
-		interPumpEndpoint.pumps = pumps.copy();
-		return interPumpEndpoint;
-	}
-
-	public Couple<Pair<BlockFace, WeakReference<PumpTileEntity>>> getPumps() {
-		return pumps;
-	}
-
-	public boolean isPulling(boolean first) {
-		Pair<BlockFace, WeakReference<PumpTileEntity>> pair = getPumps().get(first);
-		PumpTileEntity pumpTileEntity = pair.getSecond()
-			.get();
-		if (pumpTileEntity == null || pumpTileEntity.isRemoved())
-			return false;
-		return pumpTileEntity.isPullingOnSide(pumpTileEntity.isFront(pair.getFirst()
-			.getFace()));
-	}
-
-	public int getTransferSpeed(boolean first) {
-		PumpTileEntity pumpTileEntity = getPumps().get(first)
-			.getSecond()
-			.get();
-		if (pumpTileEntity == null || pumpTileEntity.isRemoved())
-			return 0;
-		return pumpTileEntity.getFluidTransferSpeed();
-	}
-
-	@Override
-	public LazyOptional<IFluidHandler> provideHandler() {
-		if (isPulling(true) == isPulling(false))
-			return LazyOptional.empty();
-		if (getTransferSpeed(true) > getTransferSpeed(false))
-			return LazyOptional.empty();
-		return super.provideHandler();
-	}
-
-	@Override
-	public FluidStack provideFluid() {
-		if (!provideHandler().isPresent())
-			return FluidStack.EMPTY;
-
-		Couple<Pair<BlockFace, WeakReference<PumpTileEntity>>> pumps = getPumps();
-		for (boolean current : Iterate.trueAndFalse) {
-			if (isPulling(current))
-				continue;
-
-			Pair<BlockFace, WeakReference<PumpTileEntity>> pair = pumps.get(current);
-			BlockFace blockFace = pair.getFirst();
-			PumpTileEntity pumpTileEntity = pair.getSecond()
-				.get();
-			if (pumpTileEntity == null)
-				continue;
-			if (pumpTileEntity.networks == null)
-				continue;
-			FluidNetwork fluidNetwork = pumpTileEntity.networks.get(pumpTileEntity.isFront(blockFace.getFace()));
-			for (FluidNetworkFlow fluidNetworkFlow : fluidNetwork.flows) {
-				for (FluidNetworkEndpoint fne : fluidNetworkFlow.outputEndpoints) {
-					if (!(fne instanceof InterPumpEndpoint))
-						continue;
-					InterPumpEndpoint ipe = (InterPumpEndpoint) fne;
-					if (!ipe.location.isEquivalent(location))
-						continue;
-
-					FluidStack heldFluid = fluidNetworkFlow.fluidStack;
-					if (heldFluid.isEmpty())
-						return heldFluid;
-					FluidStack copy = heldFluid.copy();
-					copy.setAmount(1);
-					return heldFluid;
-				}
-			}
-		}
-		return FluidStack.EMPTY;
-	}
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpFluidHandler.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpFluidHandler.java
deleted file mode 100644
index 34e0bd8e1..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/InterPumpFluidHandler.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import net.minecraftforge.fluids.FluidStack;
-import net.minecraftforge.fluids.capability.templates.FluidTank;
-
-public class InterPumpFluidHandler extends FluidTank {
-
-	InterPumpEndpoint endpoint;
-
-	public InterPumpFluidHandler(InterPumpEndpoint endpoint) {
-		super(Integer.MAX_VALUE);
-		this.endpoint = endpoint;
-	}
-
-	@Override
-	public int fill(FluidStack resource, FluidAction action) {
-		if (resource.isEmpty())
-			return 0;
-		int maxInput = Math.min(resource.getAmount(), Math.max(getTransferCapacity() - getFluidAmount(), 0));
-		FluidStack toInsert = resource.copy();
-		toInsert.setAmount(maxInput);
-		FluidPropagator.showBlockFace(endpoint.location).colored(0x77d196).lineWidth(1/4f);
-		return super.fill(toInsert, action);
-	}
-
-	@Override
-	public FluidStack drain(int maxDrain, FluidAction action) {
-		return super.drain(maxDrain, action);
-	}
-	
-	public FluidStack provide() {
-		FluidStack heldFluid = getFluid();
-		if (heldFluid.isEmpty())
-			return heldFluid;
-		FluidStack copy = heldFluid.copy();
-		copy.setAmount(1);
-		return copy;
-	}
-	
-	private int getTransferCapacity() {
-		return Math.min(endpoint.getTransferSpeed(true), endpoint.getTransferSpeed(false));
-	}
-
-}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java
index 383b8f8ec..8f3f79196 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/OpenEndedPipe.java
@@ -9,12 +9,10 @@ import com.simibubi.create.content.contraptions.fluids.potion.PotionFluidHandler
 import com.simibubi.create.foundation.fluid.FluidHelper;
 import com.simibubi.create.foundation.utility.BlockFace;
 import com.simibubi.create.foundation.utility.BlockHelper;
-import com.simibubi.create.foundation.utility.Iterate;
 
 import net.minecraft.block.BlockState;
 import net.minecraft.block.FlowingFluidBlock;
 import net.minecraft.entity.LivingEntity;
-import net.minecraft.fluid.Fluid;
 import net.minecraft.fluid.FluidState;
 import net.minecraft.fluid.Fluids;
 import net.minecraft.item.ItemStack;
@@ -25,7 +23,6 @@ import net.minecraft.potion.EffectInstance;
 import net.minecraft.potion.PotionUtils;
 import net.minecraft.state.properties.BlockStateProperties;
 import net.minecraft.tags.FluidTags;
-import net.minecraft.tileentity.TileEntity;
 import net.minecraft.util.Direction;
 import net.minecraft.util.SoundCategory;
 import net.minecraft.util.SoundEvents;
@@ -35,10 +32,9 @@ import net.minecraft.world.World;
 import net.minecraftforge.common.util.LazyOptional;
 import net.minecraftforge.fluids.FluidStack;
 import net.minecraftforge.fluids.capability.IFluidHandler;
-import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
 import net.minecraftforge.fluids.capability.templates.FluidTank;
 
-public class OpenEndedPipe {
+public class OpenEndedPipe extends FlowSource {
 
 	World world;
 	BlockPos pos;
@@ -47,12 +43,12 @@ public class OpenEndedPipe {
 	private OpenEndFluidHandler fluidHandler;
 	private BlockPos outputPos;
 	private boolean wasPulling;
-	private boolean stale;
 
 	private FluidStack cachedFluid;
 	private List<EffectInstance> cachedEffects;
 
 	public OpenEndedPipe(BlockFace face) {
+		super(face);
 		fluidHandler = new OpenEndFluidHandler();
 		outputPos = face.getConnectedPos();
 		pos = face.getPos();
@@ -61,15 +57,17 @@ public class OpenEndedPipe {
 			aoe = aoe.expand(0, -1, 0);
 	}
 
-	public void tick(World world, boolean pulling) {
+	@Override
+	public void manageSource(World world) {
 		this.world = world;
+	}
+
+	private FluidStack removeFluidFromSpace(boolean simulate) {
+		FluidStack empty = FluidStack.EMPTY;
+		if (world == null)
+			return empty;
 		if (!world.isAreaLoaded(outputPos, 0))
-			return;
-		if (pulling != wasPulling) {
-			if (pulling)
-				fluidHandler.clear();
-			wasPulling = pulling;
-		}
+			return empty;
 
 		BlockState state = world.getBlockState(outputPos);
 		FluidState fluidState = state.getFluidState();
@@ -77,71 +75,88 @@ public class OpenEndedPipe {
 
 		if (!waterlog && !state.getMaterial()
 			.isReplaceable())
-			return;
+			return empty;
+		if (fluidState.isEmpty() || !fluidState.isSource())
+			return empty;
 
-		if (pulling) {
-			if (fluidState.isEmpty() || !fluidState.isSource())
-				return;
-			if (!fluidHandler.tryCollectFluid(fluidState.getFluid()))
-				return;
-			if (waterlog) {
-				world.setBlockState(outputPos, state.with(BlockStateProperties.WATERLOGGED, false), 3);
-				world.getPendingFluidTicks()
-					.scheduleTick(outputPos, Fluids.WATER, 1);
-				return;
-			}
-			world.setBlockState(outputPos, fluidState.getBlockState()
-				.with(FlowingFluidBlock.LEVEL, 14), 3);
-			return;
+		FluidStack stack = new FluidStack(fluidState.getFluid(), 1000);
+
+		if (simulate)
+			return stack;
+
+		if (waterlog) {
+			world.setBlockState(outputPos, state.with(BlockStateProperties.WATERLOGGED, false), 3);
+			world.getPendingFluidTicks()
+				.scheduleTick(outputPos, Fluids.WATER, 1);
+			return stack;
 		}
+		world.setBlockState(outputPos, fluidState.getBlockState()
+			.with(FlowingFluidBlock.LEVEL, 14), 3);
+		return stack;
+	}
 
-		FluidStack fluid = fluidHandler.getFluid();
+	private boolean provideFluidToSpace(FluidStack fluid, boolean simulate) {
+		if (world == null)
+			return false;
+		if (!world.isAreaLoaded(outputPos, 0))
+			return false;
+
+		BlockState state = world.getBlockState(outputPos);
+		FluidState fluidState = state.getFluidState();
+		boolean waterlog = state.contains(BlockStateProperties.WATERLOGGED);
+
+		if (!waterlog && !state.getMaterial()
+			.isReplaceable())
+			return false;
 		if (fluid.isEmpty())
-			return;
+			return false;
 		if (!FluidHelper.hasBlockState(fluid.getFluid())) {
-			fluidHandler.drain(fluid.getAmount() > 1 ? fluid.getAmount() - 1 : 1, FluidAction.EXECUTE);
-			if (fluidHandler.isEmpty())
-				updatePumpIfNecessary();
-			if (!fluid.getFluid()
-				.isEquivalentTo(AllFluids.POTION.get()))
-				return;
-			applyPotionEffects(world, fluid);
-			return;
+			if (!simulate)
+				applyEffects(world, fluid);
+			return true;
 		}
 
-		Fluid providedFluid = fluidHandler.tryProvidingFluid();
-		if (providedFluid == null)
-			return;
-		if (!fluidState.isEmpty() && fluidState.getFluid() != providedFluid) {
-			FluidReactions.handlePipeSpillCollision(world, outputPos, providedFluid, fluidState);
-			return;
+		if (!fluidState.isEmpty() && fluidState.getFluid() != fluid.getFluid()) {
+			FluidReactions.handlePipeSpillCollision(world, outputPos, fluid.getFluid(), fluidState);
+			return false;
 		}
+
 		if (fluidState.isSource())
-			return;
+			return false;
+		if (waterlog && fluid.getFluid() != Fluids.WATER)
+			return false;
+		if (simulate)
+			return true;
 
-		if (world.getDimension().isUltrawarm() && providedFluid.getFluid()
+		if (world.getDimension().isUltrawarm() && fluid.getFluid()
 			.isIn(FluidTags.WATER)) {
 			int i = outputPos.getX();
 			int j = outputPos.getY();
 			int k = outputPos.getZ();
 			world.playSound(null, i, j, k, SoundEvents.BLOCK_FIRE_EXTINGUISH, SoundCategory.BLOCKS, 0.5F,
 				2.6F + (world.rand.nextFloat() - world.rand.nextFloat()) * 0.8F);
-			return;
+			return true;
 		}
 
 		if (waterlog) {
-			if (providedFluid.getFluid() != Fluids.WATER)
-				return;
 			world.setBlockState(outputPos, state.with(BlockStateProperties.WATERLOGGED, true), 3);
 			world.getPendingFluidTicks()
 				.scheduleTick(outputPos, Fluids.WATER, 1);
-			return;
+			return true;
 		}
-		world.setBlockState(outputPos, providedFluid.getDefaultState()
+		world.setBlockState(outputPos, fluid.getFluid()
+			.getDefaultState()
 			.getBlockState(), 3);
+		return true;
 	}
 
-	private void applyPotionEffects(World world, FluidStack fluid) {
+	private void applyEffects(World world, FluidStack fluid) {
+		if (!fluid.getFluid()
+			.isEquivalentTo(AllFluids.POTION.get())) {
+			// other fx
+			return;
+		}
+
 		if (cachedFluid == null || cachedEffects == null || !fluid.isFluidEqual(cachedFluid)) {
 			FluidStack copy = fluid.copy();
 			copy.setAmount(250);
@@ -167,47 +182,30 @@ public class OpenEndedPipe {
 
 	}
 
-	public LazyOptional<IFluidHandler> getCapability() {
+	@Override
+	public LazyOptional<IFluidHandler> provideHandler() {
 		return LazyOptional.of(() -> fluidHandler);
 	}
 
-	public CompoundNBT writeToNBT(CompoundNBT compound) {
+	public CompoundNBT serializeNBT() {
+		CompoundNBT compound = new CompoundNBT();
 		fluidHandler.writeToNBT(compound);
 		compound.putBoolean("Pulling", wasPulling);
+		compound.put("Location", location.serializeNBT());
 		return compound;
 	}
 
-	public void readNBT(CompoundNBT compound) {
-		fluidHandler.readFromNBT(compound);
-		wasPulling = compound.getBoolean("Pulling");
-	}
-
-	public void markStale() {
-		stale = true;
-	}
-
-	public void unmarkStale() {
-		stale = false;
-	}
-
-	public boolean isStale() {
-		return stale;
-	}
-
-	private void updatePumpIfNecessary() {
-		if (world == null)
-			return;
-		if (!PumpBlock.isPump(world.getBlockState(pos)))
-			return;
-		TileEntity tileEntity = world.getTileEntity(pos);
-		if (tileEntity instanceof PumpTileEntity)
-			((PumpTileEntity) tileEntity).sendData();
+	public static OpenEndedPipe fromNBT(CompoundNBT compound) {
+		OpenEndedPipe oep = new OpenEndedPipe(BlockFace.fromNBT(compound.getCompound("Location")));
+		oep.fluidHandler.readFromNBT(compound);
+		oep.wasPulling = compound.getBoolean("Pulling");
+		return oep;
 	}
 
 	private class OpenEndFluidHandler extends FluidTank {
 
 		public OpenEndFluidHandler() {
-			super(1500);
+			super(1000);
 		}
 
 		@Override
@@ -219,76 +217,76 @@ public class OpenEndedPipe {
 				return 0;
 			if (resource.isEmpty())
 				return 0;
-
-			FluidStack prevFluid = getFluid();
-			BlockState state = world.getBlockState(outputPos);
-			FluidState fluidState = state.getFluidState();
-			if (!fluidState.isEmpty() && fluidState.getFluid() != resource.getFluid()) {
-				FluidReactions.handlePipeSpillCollision(world, outputPos, resource.getFluid(), fluidState);
-				return 0;
-			}
-			if (fluidState.isSource())
-				return 0;
-			if (!(BlockHelper.hasBlockStateProperty(state, BlockStateProperties.WATERLOGGED) && resource.getFluid() == Fluids.WATER)
-				&& !state.getMaterial()
-					.isReplaceable())
+			if (!provideFluidToSpace(resource, true))
 				return 0;
 
-			// Never allow being filled above 1000
-			FluidStack insertable = resource.copy();
-			insertable.setAmount(Math.min(insertable.getAmount(), Math.max(1000 - getFluidAmount(), 0)));
-			int fill = super.fill(insertable, action);
-
-			if (!getFluid().isFluidEqual(prevFluid))
-				updatePumpIfNecessary();
+			if (!getFluid().isEmpty() && !getFluid().isFluidEqual(resource))
+				setFluid(FluidStack.EMPTY);
+			if (wasPulling)
+				wasPulling = false;
 
+			int fill = super.fill(resource, action);
+			if (action.execute() && (getFluidAmount() == 1000 || !FluidHelper.hasBlockState(getFluid().getFluid()))
+				&& provideFluidToSpace(getFluid(), false))
+				setFluid(FluidStack.EMPTY);
 			return fill;
 		}
 
 		@Override
 		public FluidStack drain(FluidStack resource, FluidAction action) {
-			boolean wasEmpty = isEmpty();
-			FluidStack drain = super.drain(resource, action);
-			if (action.execute() && !wasEmpty && isEmpty())
-				updatePumpIfNecessary();
-			return drain;
+			return drainInner(resource.getAmount(), resource, action);
 		}
 
 		@Override
 		public FluidStack drain(int maxDrain, FluidAction action) {
-			boolean wasEmpty = isEmpty();
-			FluidStack drain = super.drain(maxDrain, action);
-			if (action.execute() && !wasEmpty && isEmpty())
-				updatePumpIfNecessary();
-			return drain;
+			return drainInner(maxDrain, null, action);
 		}
 
-		public boolean tryCollectFluid(Fluid fluid) {
-			for (boolean simulate : Iterate.trueAndFalse)
-				if (super.fill(new FluidStack(fluid, 1000),
-					simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE) != 1000)
-					return false;
-			updatePumpIfNecessary();
-			return true;
-		}
+		private FluidStack drainInner(int amount, @Nullable FluidStack filter, FluidAction action) {
+			FluidStack empty = FluidStack.EMPTY;
+			boolean filterPresent = filter != null;
 
-		@Nullable
-		public Fluid tryProvidingFluid() {
-			Fluid fluid = getFluid().getFluid();
-			for (boolean simulate : Iterate.trueAndFalse)
-				if (drain(1000, simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE).getAmount() != 1000)
-					return null;
-			updatePumpIfNecessary();
-			return fluid;
-		}
+			if (world == null)
+				return empty;
+			if (!world.isAreaLoaded(outputPos, 0))
+				return empty;
+			if (amount == 0)
+				return empty;
+			if (amount > 1000) {
+				amount = 1000;
+				if (filterPresent)
+					filter = FluidHelper.copyStackWithAmount(filter, amount);
+			}
 
-		public void clear() {
-			boolean wasEmpty = isEmpty();
-			setFluid(FluidStack.EMPTY);
-			if (!wasEmpty)
-				updatePumpIfNecessary();
+			if (!wasPulling)
+				wasPulling = true;
+
+			FluidStack drainedFromInternal = filterPresent ? super.drain(filter, action) : super.drain(amount, action);
+			if (!drainedFromInternal.isEmpty())
+				return drainedFromInternal;
+
+			FluidStack drainedFromWorld = removeFluidFromSpace(action.simulate());
+			if (drainedFromWorld.isEmpty())
+				return FluidStack.EMPTY;
+			if (filterPresent && !drainedFromWorld.isFluidEqual(filter))
+				return FluidStack.EMPTY;
+
+			int remainder = drainedFromWorld.getAmount() - amount;
+			drainedFromWorld.setAmount(amount);
+
+			if (!action.simulate() && remainder > 0) {
+				if (!getFluid().isEmpty() && !getFluid().isFluidEqual(drainedFromWorld))
+					setFluid(FluidStack.EMPTY);
+				super.fill(FluidHelper.copyStackWithAmount(drainedFromWorld, remainder), FluidAction.EXECUTE);
+			}
+			return drainedFromWorld;
 		}
 
 	}
 
+	@Override
+	public boolean isEndpoint() {
+		return true;
+	}
+
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeAttachmentModel.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeAttachmentModel.java
index 7c5e12f69..cda095609 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeAttachmentModel.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeAttachmentModel.java
@@ -6,8 +6,9 @@ import java.util.List;
 import java.util.Random;
 
 import com.simibubi.create.AllBlockPartials;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour.AttachmentTypes;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour.AttachmentTypes;
 import com.simibubi.create.content.contraptions.fluids.pipes.FluidPipeBlock;
+import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.foundation.block.connected.BakedModelWrapperWithData;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.utility.Iterate;
@@ -35,16 +36,16 @@ public class PipeAttachmentModel extends BakedModelWrapperWithData {
 	@Override
 	protected Builder gatherModelData(Builder builder, IBlockDisplayReader world, BlockPos pos, BlockState state) {
 		PipeModelData data = new PipeModelData();
-		FluidPipeAttachmentBehaviour attachmentBehaviour =
-			TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
+		FluidTransportBehaviour transport = TileEntityBehaviour.get(world, pos, FluidTransportBehaviour.TYPE);
+		BracketedTileEntityBehaviour bracket = TileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
 
-		if (attachmentBehaviour != null) {
+		if (transport != null)
 			for (Direction d : Iterate.directions)
-				data.putRim(d, attachmentBehaviour.getAttachment(world, pos, state, d));
-			data.putBracket(attachmentBehaviour.getBracket());
-		}
-		data.setEncased(FluidPipeBlock.shouldDrawCasing(world, pos, state));
+				data.putRim(d, transport.getRenderedRimAttachment(world, pos, state, d));
+		if (bracket != null)
+			data.putBracket(bracket.getBracket());
 
+		data.setEncased(FluidPipeBlock.shouldDrawCasing(world, pos, state));
 		return builder.withInitial(PIPE_PROPERTY, data);
 	}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeConnection.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeConnection.java
new file mode 100644
index 000000000..1dd1ed66c
--- /dev/null
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/PipeConnection.java
@@ -0,0 +1,476 @@
+package com.simibubi.create.content.contraptions.fluids;
+
+import java.util.Optional;
+import java.util.Random;
+import java.util.function.Predicate;
+
+import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
+import com.simibubi.create.foundation.utility.BlockFace;
+import com.simibubi.create.foundation.utility.Couple;
+import com.simibubi.create.foundation.utility.Iterate;
+import com.simibubi.create.foundation.utility.LerpedFloat;
+import com.simibubi.create.foundation.utility.VecHelper;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.nbt.FloatNBT;
+import net.minecraft.nbt.ListNBT;
+import net.minecraft.particles.IParticleData;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import net.minecraftforge.common.util.Constants.NBT;
+import net.minecraftforge.fluids.FluidStack;
+import net.minecraftforge.fml.DistExecutor;
+
+public class PipeConnection {
+
+	Direction side;
+
+	// Layer I
+	Couple<Float> pressure; // [inbound, outward]
+	Optional<FlowSource> source;
+	Optional<FlowSource> previousSource;
+
+	// Layer II
+	Optional<Flow> flow;
+	boolean particleSplashNextTick;
+
+	// Layer III
+	Optional<FluidNetwork> network; // not serialized
+
+	public PipeConnection(Direction side) {
+		this.side = side;
+		pressure = Couple.create(() -> 0f);
+		flow = Optional.empty();
+		previousSource = Optional.empty();
+		source = Optional.empty();
+		network = Optional.empty();
+		particleSplashNextTick = false;
+	}
+
+	public FluidStack getProvidedFluid() {
+		FluidStack empty = FluidStack.EMPTY;
+		if (!hasFlow())
+			return empty;
+		Flow flow = this.flow.get();
+		if (!flow.inbound)
+			return empty;
+		if (!flow.complete)
+			return empty;
+		return flow.fluid;
+	}
+
+	public boolean flipFlowsIfPressureReversed() {
+		if (!hasFlow())
+			return false;
+		boolean singlePressure = comparePressure() != 0 && (getInboundPressure() == 0 || getOutwardPressure() == 0);
+		Flow flow = this.flow.get();
+		if (!singlePressure || comparePressure() < 0 == flow.inbound)
+			return false;
+		flow.inbound = !flow.inbound;
+		if (!flow.complete)
+			this.flow = Optional.empty();
+		return true;
+	}
+
+	public void manageSource(World world, BlockPos pos) {
+		if (!source.isPresent() && !determineSource(world, pos))
+			return;
+		FlowSource flowSource = source.get();
+		flowSource.manageSource(world);
+	}
+		
+	public boolean manageFlows(World world, BlockPos pos, FluidStack internalFluid,
+		Predicate<FluidStack> extractionPredicate) {
+
+		// Only keep network if still valid
+		Optional<FluidNetwork> retainedNetwork = network;
+		network = Optional.empty();
+
+		// chunk border
+		if (!source.isPresent() && !determineSource(world, pos))
+			return false;
+		FlowSource flowSource = source.get();
+
+		if (!hasFlow()) {
+			if (!hasPressure())
+				return false;
+
+			// Try starting a new flow
+			boolean prioritizeInbound = comparePressure() < 0;
+			for (boolean trueFalse : Iterate.trueAndFalse) {
+				boolean inbound = prioritizeInbound == trueFalse;
+				if (pressure.get(inbound) == 0)
+					continue;
+				if (tryStartingNewFlow(inbound, inbound ? flowSource.provideFluid(extractionPredicate) : internalFluid))
+					return true;
+			}
+			return false;
+		}
+
+		// Manage existing flow
+		Flow flow = this.flow.get();
+		FluidStack provided = flow.inbound ? flowSource.provideFluid(extractionPredicate) : internalFluid;
+		if (!hasPressure() || provided.isEmpty() || !provided.isFluidEqual(flow.fluid)) {
+			this.flow = Optional.empty();
+			return true;
+		}
+
+		// Overwrite existing flow
+		if (flow.inbound != comparePressure() < 0) {
+			boolean inbound = !flow.inbound;
+			if (inbound && !provided.isEmpty() || !inbound && !internalFluid.isEmpty()) {
+				FluidPropagator.resetAffectedFluidNetworks(world, pos, side);
+				tryStartingNewFlow(inbound, inbound ? flowSource.provideFluid(extractionPredicate) : internalFluid);
+				return true;
+			}
+		}
+
+		flowSource.whileFlowPresent(world, flow.inbound);
+
+		if (!flowSource.isEndpoint())
+			return false;
+		if (!flow.inbound)
+			return false;
+
+		// Layer III
+		network = retainedNetwork;
+		if (!hasNetwork())
+			network = Optional.of(new FluidNetwork(world, new BlockFace(pos, side), flowSource::provideHandler));
+		network.get()
+			.tick();
+
+		return false;
+	}
+
+	private boolean tryStartingNewFlow(boolean inbound, FluidStack providedFluid) {
+		if (providedFluid.isEmpty())
+			return false;
+		Flow flow = new Flow(inbound, providedFluid);
+		this.flow = Optional.of(flow);
+		return true;
+	}
+
+	private boolean determineSource(World world, BlockPos pos) {
+		if (!world.isAreaLoaded(pos, 1))
+			return false;
+		BlockFace location = new BlockFace(pos, side);
+
+		if (FluidPropagator.isOpenEnd(world, pos, side)) {
+			if (previousSource.orElse(null) instanceof OpenEndedPipe)
+				source = previousSource;
+			else
+				source = Optional.of(new OpenEndedPipe(location));
+			return true;
+		}
+
+		if (FluidPropagator.hasFluidCapability(world, location.getConnectedPos(), side.getOpposite())) {
+			source = Optional.of(new FlowSource.FluidHandler(location));
+			return true;
+		}
+
+		FluidTransportBehaviour behaviour =
+			TileEntityBehaviour.get(world, pos.offset(side), FluidTransportBehaviour.TYPE);
+		source = Optional.of(behaviour == null ? new FlowSource.Blocked(location) : new FlowSource.OtherPipe(location));
+		return true;
+	}
+
+	public void tickFlowProgress(World world, BlockPos pos) {
+		if (!hasFlow())
+			return;
+		Flow flow = this.flow.get();
+		if (flow.fluid.isEmpty())
+			return;
+
+		if (world.isRemote) {
+			if (!source.isPresent())
+				determineSource(world, pos);
+			
+			spawnParticles(world, pos, flow.fluid);
+			if (particleSplashNextTick)
+				spawnSplashOnRim(world, pos, flow.fluid);
+			particleSplashNextTick = false;
+		}
+
+		float flowSpeed = 1 / 32f + MathHelper.clamp(pressure.get(flow.inbound) / 512f, 0, 1) * 31 / 32f;
+		flow.progress.setValue(Math.min(flow.progress.getValue() + flowSpeed, 1));
+		if (flow.progress.getValue() >= 1)
+			flow.complete = true;
+	}
+
+	public void serializeNBT(CompoundNBT tag, boolean clientPacket) {
+		CompoundNBT connectionData = new CompoundNBT();
+		tag.put(side.getName(), connectionData);
+
+		if (hasPressure()) {
+			ListNBT pressureData = new ListNBT();
+			pressureData.add(FloatNBT.of(getInboundPressure()));
+			pressureData.add(FloatNBT.of(getOutwardPressure()));
+			connectionData.put("Pressure", pressureData);
+		}
+
+		if (hasOpenEnd())
+			connectionData.put("OpenEnd", ((OpenEndedPipe) source.get()).serializeNBT());
+
+		if (hasFlow()) {
+			CompoundNBT flowData = new CompoundNBT();
+			Flow flow = this.flow.get();
+			flow.fluid.writeToNBT(flowData);
+			flowData.putBoolean("In", flow.inbound);
+			if (!flow.complete)
+				flowData.put("Progress", flow.progress.writeNBT());
+			connectionData.put("Flow", flowData);
+		}
+
+	}
+
+	private boolean hasOpenEnd() {
+		return source.orElse(null) instanceof OpenEndedPipe;
+	}
+
+	public void deserializeNBT(CompoundNBT tag, boolean clientPacket) {
+		CompoundNBT connectionData = tag.getCompound(side.getName());
+
+		if (connectionData.contains("Pressure")) {
+			ListNBT pressureData = connectionData.getList("Pressure", NBT.TAG_FLOAT);
+			pressure = Couple.create(pressureData.getFloat(0), pressureData.getFloat(1));
+		} else
+			pressure.replace(f -> 0f);
+
+		source = Optional.empty();
+		if (connectionData.contains("OpenEnd"))
+			source = Optional.of(OpenEndedPipe.fromNBT(connectionData.getCompound("OpenEnd")));
+
+		if (connectionData.contains("Flow")) {
+			CompoundNBT flowData = connectionData.getCompound("Flow");
+			FluidStack fluid = FluidStack.loadFluidStackFromNBT(flowData);
+			boolean inbound = flowData.getBoolean("In");
+			if (!flow.isPresent()) {
+				flow = Optional.of(new Flow(inbound, fluid));
+				if (clientPacket)
+					particleSplashNextTick = true;
+			}
+			Flow flow = this.flow.get();
+
+			flow.fluid = fluid;
+			flow.inbound = inbound;
+			flow.complete = !flowData.contains("Progress");
+
+			if (!flow.complete)
+				flow.progress.readNBT(flowData.getCompound("Progress"), clientPacket);
+			else {
+				if (flow.progress.getValue() == 0)
+					flow.progress.startWithValue(1);
+				flow.progress.setValue(1);
+			}
+
+		} else
+			flow = Optional.empty();
+
+	}
+
+	/**
+	 * @return zero if outward == inbound <br>
+	 *         positive if outward > inbound <br>
+	 *         negative if outward < inbound
+	 */
+	public float comparePressure() {
+		return getOutwardPressure() - getInboundPressure();
+	}
+
+	public void wipePressure() {
+		this.pressure.replace(f -> 0f);
+		if (this.source.isPresent())
+			this.previousSource = this.source;
+		this.source = Optional.empty();
+		resetNetwork();
+	}
+
+	public FluidStack provideOutboundFlow() {
+		if (!hasFlow())
+			return FluidStack.EMPTY;
+		Flow flow = this.flow.get();
+		if (!flow.complete || flow.inbound)
+			return FluidStack.EMPTY;
+		return flow.fluid;
+	}
+
+	public void addPressure(boolean inbound, float pressure) {
+		this.pressure = this.pressure.mapWithContext((f, in) -> in == inbound ? f + pressure : f);
+	}
+
+	public boolean hasPressure() {
+		return getInboundPressure() != 0 || getOutwardPressure() != 0;
+	}
+
+	private float getOutwardPressure() {
+		return pressure.getSecond();
+	}
+
+	private float getInboundPressure() {
+		return pressure.getFirst();
+	}
+
+	public boolean hasFlow() {
+		return flow.isPresent();
+	}
+
+	public boolean hasNetwork() {
+		return network.isPresent();
+	}
+
+	public void resetNetwork() {
+		network.ifPresent(FluidNetwork::reset);
+	}
+
+	public class Flow {
+
+		public boolean complete;
+		public boolean inbound;
+		public LerpedFloat progress;
+		public FluidStack fluid;
+
+		public Flow(boolean inbound, FluidStack fluid) {
+			this.inbound = inbound;
+			this.fluid = fluid;
+			this.progress = LerpedFloat.linear()
+				.startWithValue(0);
+			this.complete = false;
+		}
+
+	}
+
+	public static final int MAX_PARTICLE_RENDER_DISTANCE = 20;
+	public static final int SPLASH_PARTICLE_AMOUNT = 1;
+	public static final float IDLE_PARTICLE_SPAWN_CHANCE = 1 / 1000f;
+	public static final float RIM_RADIUS = 1 / 4f + 1 / 64f;
+	public static final Random r = new Random();
+
+	public void spawnSplashOnRim(World world, BlockPos pos, FluidStack fluid) {
+		DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> spawnSplashOnRimInner(world, pos, fluid));
+	}
+
+	public void spawnParticles(World world, BlockPos pos, FluidStack fluid) {
+		DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> spawnParticlesInner(world, pos, fluid));
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	private void spawnParticlesInner(World world, BlockPos pos, FluidStack fluid) {
+		if (!isRenderEntityWithinDistance(pos))
+			return;
+		if (hasOpenEnd())
+			spawnPouringLiquid(world, pos, fluid, 1);
+		else if (r.nextFloat() < IDLE_PARTICLE_SPAWN_CHANCE)
+			spawnRimParticles(world, pos, fluid, 1);
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	private void spawnSplashOnRimInner(World world, BlockPos pos, FluidStack fluid) {
+		if (!isRenderEntityWithinDistance(pos))
+			return;
+		spawnRimParticles(world, pos, fluid, SPLASH_PARTICLE_AMOUNT);
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	private void spawnRimParticles(World world, BlockPos pos, FluidStack fluid, int amount) {
+		if (hasOpenEnd()) {
+			spawnPouringLiquid(world, pos, fluid, amount);
+			return;
+		}
+
+		IParticleData particle = FluidFX.getDrippingParticle(fluid);
+		FluidFX.spawnRimParticles(world, pos, side, amount, particle, RIM_RADIUS);
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	private void spawnPouringLiquid(World world, BlockPos pos, FluidStack fluid, int amount) {
+		IParticleData particle = FluidFX.getFluidParticle(fluid);
+		Vec3d directionVec = new Vec3d(side.getDirectionVec());
+		if (!hasFlow())
+			return;
+		Flow flow = this.flow.get();
+		FluidFX.spawnPouringLiquid(world, pos, amount, particle, RIM_RADIUS, directionVec, flow.inbound);
+	}
+
+	@OnlyIn(Dist.CLIENT)
+	public static boolean isRenderEntityWithinDistance(BlockPos pos) {
+		Entity renderViewEntity = Minecraft.getInstance()
+			.getRenderViewEntity();
+		if (renderViewEntity == null)
+			return false;
+		Vec3d center = VecHelper.getCenterOf(pos);
+		if (renderViewEntity.getPositionVec()
+			.distanceTo(center) > MAX_PARTICLE_RENDER_DISTANCE)
+			return false;
+		return true;
+	}
+
+//	void visualizePressure(BlockPos pos) {
+//		if (!hasPressure())
+//			return;
+//
+//		pressure.forEachWithContext((pressure, inbound) -> {
+//			if (inbound)
+//				return;
+//
+//			Vec3d directionVec = new Vec3d(side.getDirectionVec());
+//			Vec3d scaleVec = directionVec.scale(-.25f * side.getAxisDirection()
+//				.getOffset());
+//			directionVec = directionVec.scale(inbound ? .35f : .45f);
+//			CreateClient.outliner.chaseAABB("pressure" + pos.toShortString() + side.getName() + String.valueOf(inbound),
+//				FluidPropagator.smallCenter.offset(directionVec.add(new Vec3d(pos)))
+//					.grow(scaleVec.x, scaleVec.y, scaleVec.z)
+//					.expand(0, pressure / 64f, 0)
+//					.grow(1 / 64f));
+//		});
+//	}
+//
+//	void visualizeFlow(BlockPos pos) {
+//		if (!hasFlow())
+//			return;
+//
+//		Vec3d directionVec = new Vec3d(side.getDirectionVec());
+//		float size = 1 / 4f;
+//		float length = .5f;
+//		Flow flow = this.flow.get();
+//		boolean inbound = flow.inbound;
+//		FluidStack fluid = flow.fluid;
+//
+//		if (flow.progress == null)
+//			return;
+//		float value = flow.progress.getValue();
+//		Vec3d start = directionVec.scale(inbound ? .5 : .5f - length);
+//		Vec3d offset = directionVec.scale(length * (inbound ? -1 : 1))
+//			.scale(value);
+//
+//		Vec3d scale = new Vec3d(1, 1, 1).subtract(directionVec.scale(side.getAxisDirection()
+//			.getOffset()))
+//			.scale(size);
+//		AxisAlignedBB bb = new AxisAlignedBB(start, start.add(offset)).offset(VecHelper.getCenterOf(pos))
+//			.grow(scale.x, scale.y, scale.z);
+//
+//		int color = 0x7fdbda;
+//		if (!fluid.isEmpty()) {
+//			Fluid fluid2 = fluid.getFluid();
+//			if (fluid2 == Fluids.WATER)
+//				color = 0x1D4D9B;
+//			else if (fluid2 == Fluids.LAVA)
+//				color = 0xFF773D;
+//			else
+//				color = fluid2.getAttributes()
+//					.getColor(fluid);
+//		}
+//
+//		CreateClient.outliner.chaseAABB(this, bb)
+//			.withFaceTexture(AllSpecialTextures.SELECTION)
+//			.colored(color)
+//			.lineWidth(0);
+//	}
+
+}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpBlock.java
index 8cb253f03..2bae022e7 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpBlock.java
@@ -1,15 +1,12 @@
 package com.simibubi.create.content.contraptions.fluids;
 
-import java.util.Map;
+import java.util.Random;
 
 import org.apache.commons.lang3.mutable.MutableBoolean;
 
 import com.simibubi.create.AllShapes;
 import com.simibubi.create.AllTileEntities;
 import com.simibubi.create.content.contraptions.base.DirectionalKineticBlock;
-import com.simibubi.create.content.contraptions.fluids.pipes.FluidPipeBlock;
-import com.simibubi.create.foundation.utility.BlockFace;
-import com.simibubi.create.foundation.utility.Iterate;
 
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
@@ -30,8 +27,9 @@ import net.minecraft.util.math.shapes.VoxelShape;
 import net.minecraft.world.IBlockReader;
 import net.minecraft.world.IWorld;
 import net.minecraft.world.IWorldReader;
+import net.minecraft.world.TickPriority;
 import net.minecraft.world.World;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.world.server.ServerWorld;
 
 public class PumpBlock extends DirectionalKineticBlock implements IWaterLoggable {
 
@@ -67,26 +65,8 @@ public class PumpBlock extends DirectionalKineticBlock implements IWaterLoggable
 		if (!(tileEntity instanceof PumpTileEntity))
 			return state;
 		PumpTileEntity pump = (PumpTileEntity) tileEntity;
-		if (pump.networks == null)
-			return state;
-
-		FluidNetwork apn1 = pump.networks.get(true);
-		FluidNetwork apn2 = pump.networks.get(false);
-
-		// Collect pipes that can be skipped
-		apn1.clearFlows(world, true);
-		apn2.clearFlows(world, true);
-
-		// Swap skipsets as the networks change sides
-		Map<BlockFace, FluidStack> skippedConnections = apn1.previousFlow;
-		apn1.previousFlow = apn2.previousFlow;
-		apn2.previousFlow = skippedConnections;
-
-		// Init networks next tick
-		pump.networksToUpdate.forEach(MutableBoolean::setTrue);
-		pump.networks.swap();
+		pump.sidesToUpdate.forEach(MutableBoolean::setTrue);
 		pump.reversed = !pump.reversed;
-
 		return state;
 	}
 
@@ -111,22 +91,29 @@ public class PumpBlock extends DirectionalKineticBlock implements IWaterLoggable
 	public void neighborChanged(BlockState state, World world, BlockPos pos, Block otherBlock, BlockPos neighborPos,
 		boolean isMoving) {
 		DebugPacketSender.func_218806_a(world, pos);
-		if (world.isRemote)
+		Direction d = FluidPropagator.validateNeighbourChange(state, world, pos, otherBlock, neighborPos, isMoving);
+		if (d == null)
 			return;
-		if (otherBlock instanceof FluidPipeBlock)
+		if (!isOpenAt(state, d))
 			return;
-		TileEntity tileEntity = world.getTileEntity(pos);
-		if (!(tileEntity instanceof PumpTileEntity))
-			return;
-		PumpTileEntity pump = (PumpTileEntity) tileEntity;
-		Direction facing = state.get(FACING);
-		for (boolean front : Iterate.trueAndFalse) {
-			Direction side = front ? facing : facing.getOpposite();
-			if (!pos.offset(side)
-				.equals(neighborPos))
-				continue;
-			pump.updatePipesOnSide(side);
-		}
+		world.getPendingBlockTicks()
+			.scheduleTick(pos, this, 1, TickPriority.HIGH);
+//		if (world.isRemote)
+//			return;
+//		if (otherBlock instanceof FluidPipeBlock)
+//			return;
+//		TileEntity tileEntity = world.getTileEntity(pos);
+//		if (!(tileEntity instanceof PumpTileEntity))
+//			return;
+//		PumpTileEntity pump = (PumpTileEntity) tileEntity;
+//		Direction facing = state.get(FACING);
+//		for (boolean front : Iterate.trueAndFalse) {
+//			Direction side = front ? facing : facing.getOpposite();
+//			if (!pos.offset(side)
+//				.equals(neighborPos))
+//				continue;
+//			pump.updatePipesOnSide(side);
+//		}
 	}
 
 	@Override
@@ -163,4 +150,32 @@ public class PumpBlock extends DirectionalKineticBlock implements IWaterLoggable
 		return state.getBlock() instanceof PumpBlock;
 	}
 
+	@Override
+	public void onBlockAdded(BlockState state, World world, BlockPos pos, BlockState oldState, boolean isMoving) {
+		if (world.isRemote)
+			return;
+		if (state != oldState)
+			world.getPendingBlockTicks()
+				.scheduleTick(pos, this, 1, TickPriority.HIGH);
+	}
+
+	public static boolean isOpenAt(BlockState state, Direction d) {
+		return d.getAxis() == state.get(FACING)
+			.getAxis();
+	}
+
+	@Override
+	public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random r) {
+		FluidPropagator.propagateChangedPipe(world, pos, state);
+	}
+
+	@Override
+	public void onReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving) {
+		boolean blockTypeChanged = state.getBlock() != newState.getBlock();
+		if (blockTypeChanged && !world.isRemote)
+			FluidPropagator.propagateChangedPipe(world, pos, state);
+		if (state.hasTileEntity() && (blockTypeChanged || !newState.hasTileEntity()))
+			world.removeTileEntity(pos);
+	}
+	
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpEndpoint.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpEndpoint.java
deleted file mode 100644
index eb0d0f18c..000000000
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpEndpoint.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.simibubi.create.content.contraptions.fluids;
-
-import com.simibubi.create.foundation.utility.BlockFace;
-
-import net.minecraft.world.IWorld;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.fluids.FluidStack;
-
-public class PumpEndpoint extends FluidNetworkEndpoint {
-
-	PumpTileEntity pumpTE;
-
-	public PumpEndpoint(BlockFace location, PumpTileEntity pumpTE) {
-		super(pumpTE.getWorld(), location, LazyOptional.empty());
-		this.pumpTE = pumpTE;
-	}
-
-	@Override
-	protected void onHandlerInvalidated(IWorld world) {}
-
-	@Override
-	public FluidStack provideFluid() {
-		return pumpTE.providedFluid;
-	}
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpTileEntity.java
index 5fa6d3422..f70822875 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/PumpTileEntity.java
@@ -1,11 +1,13 @@
 package com.simibubi.create.content.contraptions.fluids;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashMap;
-import java.util.Iterator;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 
 import javax.annotation.Nullable;
 
@@ -19,48 +21,37 @@ import com.simibubi.create.foundation.utility.Couple;
 import com.simibubi.create.foundation.utility.Iterate;
 import com.simibubi.create.foundation.utility.LerpedFloat;
 import com.simibubi.create.foundation.utility.LerpedFloat.Chaser;
-import com.simibubi.create.foundation.utility.NBTHelper;
+import com.simibubi.create.foundation.utility.Pair;
 
 import net.minecraft.block.BlockState;
 import net.minecraft.nbt.CompoundNBT;
-import net.minecraft.nbt.ListNBT;
-import net.minecraft.particles.IParticleData;
+import net.minecraft.tileentity.TileEntity;
 import net.minecraft.tileentity.TileEntityType;
 import net.minecraft.util.Direction;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.vector.Vector3d;
 import net.minecraft.world.IBlockDisplayReader;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.common.util.Constants.NBT;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.world.IWorld;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
 import net.minecraftforge.fluids.capability.IFluidHandler;
-import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
-import net.minecraftforge.fml.DistExecutor;
 
 public class PumpTileEntity extends KineticTileEntity {
 
 	LerpedFloat arrowDirection;
-	Couple<FluidNetwork> networks;
-	Couple<Map<BlockFace, OpenEndedPipe>> openEnds;
-	Couple<MutableBoolean> networksToUpdate;
-
+	Couple<MutableBoolean> sidesToUpdate;
 	boolean reversed;
-	FluidStack providedFluid;
 
 	public PumpTileEntity(TileEntityType<?> typeIn) {
 		super(typeIn);
 		arrowDirection = LerpedFloat.linear()
 			.startWithValue(1);
-		networksToUpdate = Couple.create(MutableBoolean::new);
-		openEnds = Couple.create(HashMap::new);
-		setProvidedFluid(FluidStack.EMPTY);
+		sidesToUpdate = Couple.create(MutableBoolean::new);
 	}
 
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
 		super.addBehaviours(behaviours);
-		behaviours.add(new PumpAttachmentBehaviour(this));
+		behaviours.add(new PumpFluidTransferBehaviour(this));
 	}
 
 	@Override
@@ -77,196 +68,235 @@ public class PumpTileEntity extends KineticTileEntity {
 		if (world.isRemote) {
 			if (speed == 0)
 				return;
-			spawnParticles();
 			arrowDirection.chase(speed >= 0 ? 1 : -1, .5f, Chaser.EXP);
 			arrowDirection.tickChaser();
 			return;
 		}
 
-		BlockState blockState = getBlockState();
-		if (!(blockState.getBlock() instanceof PumpBlock))
-			return;
-		Direction face = blockState.get(PumpBlock.FACING);
-		MutableBoolean networkUpdated = new MutableBoolean(false);
-
-		if (networks == null) {
-			networks = Couple.create(new FluidNetwork(), new FluidNetwork());
-			networks.forEachWithContext((fn, front) -> {
-				BlockFace blockFace = new BlockFace(pos, front ? face : face.getOpposite());
-				fn.assemble(world, this, blockFace);
-				FluidPropagator.showBlockFace(blockFace)
-					.lineWidth(1 / 8f);
-			});
-			networkUpdated.setTrue();
-		}
-
-		networksToUpdate.forEachWithContext((update, front) -> {
+		sidesToUpdate.forEachWithContext((update, isFront) -> {
 			if (update.isFalse())
 				return;
-			FluidNetwork activePipeNetwork = networks.get(front);
-			if (activePipeNetwork == null)
-				return;
-			BlockFace blockFace = new BlockFace(pos, front ? face : face.getOpposite());
-			activePipeNetwork.reAssemble(world, this, blockFace);
-			FluidPropagator.showBlockFace(blockFace)
-				.lineWidth(1 / 8f);
 			update.setFalse();
-			networkUpdated.setTrue();
+			distributePressureTo(isFront ? getFront() : getFront().getOpposite());
 		});
 
-		if (networkUpdated.isTrue())
-			return;
-
-		networks.forEach(fn -> fn.tick(world, this));
-
 		if (speed == 0)
 			return;
 		if (speed < 0 != reversed) {
-			networks.forEachWithContext((fn, current) -> fn.clearFlows(world, true));
 			reversed = speed < 0;
 			return;
 		}
-
-		boolean pullingSide = isPullingOnSide(true);
-		float flowSpeed = Math.abs(speed) / 256f;
-
-		networks.forEachWithContext((fn, front) -> {
-			boolean pulling = isPullingOnSide(front);
-			fn.tickFlows(world, this, pulling, flowSpeed);
-			openEnds.get(front)
-				.values()
-				.forEach(oep -> oep.tick(world, pulling));
-		});
-
-		if (!networks.get(pullingSide)
-			.hasEndpoints()) {
-			setProvidedFluid(FluidStack.EMPTY);
-			return;
-		}
-
-		if (networks.getFirst()
-			.hasEndpoints()
-			&& networks.getSecond()
-				.hasEndpoints()) {
-			performTransfer();
-		}
-
 	}
 
 	@Override
-	public void remove() {
-		super.remove();
-		if (networks != null)
-			networks.forEachWithContext((fn, current) -> fn.clearFlows(world, false));
+	public void onSpeedChanged(float previousSpeed) {
+		super.onSpeedChanged(previousSpeed);
+
+		if (previousSpeed == getSpeed())
+			return;
+		if (speed != 0)
+			reversed = speed < 0;
+		if (world.isRemote)
+			return;
+
+		BlockPos frontPos = pos.offset(getFront());
+		BlockPos backPos = pos.offset(getFront().getOpposite());
+		FluidPropagator.propagateChangedPipe(world, frontPos, world.getBlockState(frontPos));
+		FluidPropagator.propagateChangedPipe(world, backPos, world.getBlockState(backPos));
 	}
 
-	private void performTransfer() {
-		boolean input = isPullingOnSide(true);
-		Collection<FluidNetworkEndpoint> inputs = networks.get(input)
-			.getEndpoints(true);
-		Collection<FluidNetworkEndpoint> outputs = networks.get(!input)
-			.getEndpoints(false);
+	protected void distributePressureTo(Direction side) {
+		if (getSpeed() == 0)
+			return;
 
-		int flowSpeed = getFluidTransferSpeed();
-		FluidStack transfer = FluidStack.EMPTY;
-		for (boolean simulate : Iterate.trueAndFalse) {
-			FluidAction action = simulate ? FluidAction.SIMULATE : FluidAction.EXECUTE;
+		BlockFace start = new BlockFace(pos, side);
+		boolean pull = isPullingOnSide(isFront(side));
+		Set<BlockFace> targets = new HashSet<>();
+		Map<BlockPos, Pair<Integer, Map<Direction, Boolean>>> pipeGraph = new HashMap<>();
 
-			List<FluidNetworkEndpoint> availableInputs = new ArrayList<>(inputs);
-			while (!availableInputs.isEmpty() && transfer.getAmount() < flowSpeed) {
-				int diff = flowSpeed - transfer.getAmount();
-				int dividedTransfer = diff / availableInputs.size();
-				int remainder = diff % availableInputs.size();
+		if (!pull)
+			FluidPropagator.resetAffectedFluidNetworks(world, pos, side.getOpposite());
 
-				for (Iterator<FluidNetworkEndpoint> iterator = availableInputs.iterator(); iterator.hasNext();) {
-					int toTransfer = dividedTransfer;
-					if (remainder > 0) {
-						toTransfer++;
-						remainder--;
-					}
+		if (!hasReachedValidEndpoint(world, start, pull)) {
 
-					FluidNetworkEndpoint ne = iterator.next();
-					IFluidHandler handler = ne.provideHandler()
-						.orElse(null);
-					if (handler == null) {
-						iterator.remove();
-						continue;
-					}
-					FluidStack drained = handler.drain(toTransfer, action);
-					if (drained.isEmpty()) {
-						iterator.remove();
-						continue;
-					}
-					if (transfer.isFluidEqual(drained) || transfer.isEmpty()) {
-						if (drained.getAmount() < toTransfer)
-							iterator.remove();
-						FluidStack copy = drained.copy();
-						copy.setAmount(drained.getAmount() + transfer.getAmount());
-						transfer = copy;
-						continue;
-					}
-					iterator.remove();
+			pipeGraph.computeIfAbsent(pos, $ -> Pair.of(0, new IdentityHashMap<>()))
+				.getSecond()
+				.put(side, pull);
+			pipeGraph.computeIfAbsent(start.getConnectedPos(), $ -> Pair.of(1, new IdentityHashMap<>()))
+				.getSecond()
+				.put(side.getOpposite(), !pull);
+
+			List<Pair<Integer, BlockPos>> frontier = new ArrayList<>();
+			Set<BlockPos> visited = new HashSet<>();
+			int maxDistance = FluidPropagator.getPumpRange();
+			frontier.add(Pair.of(1, start.getConnectedPos()));
+
+			while (!frontier.isEmpty()) {
+				Pair<Integer, BlockPos> entry = frontier.remove(0);
+				int distance = entry.getFirst();
+				BlockPos currentPos = entry.getSecond();
+
+				if (!world.isAreaLoaded(currentPos, 0))
+					continue;
+				if (visited.contains(currentPos))
+					continue;
+				visited.add(currentPos);
+				BlockState currentState = world.getBlockState(currentPos);
+				FluidTransportBehaviour pipe = FluidPropagator.getPipe(world, currentPos);
+				if (pipe == null)
 					continue;
-				}
 
-			}
+				for (Direction face : FluidPropagator.getPipeConnections(currentState, pipe)) {
+					BlockFace blockFace = new BlockFace(currentPos, face);
+					BlockPos connectedPos = blockFace.getConnectedPos();
 
-			List<FluidNetworkEndpoint> availableOutputs = new ArrayList<>(outputs);
-			while (!availableOutputs.isEmpty() && transfer.getAmount() > 0) {
-				int dividedTransfer = transfer.getAmount() / availableOutputs.size();
-				int remainder = transfer.getAmount() % availableOutputs.size();
-
-				for (Iterator<FluidNetworkEndpoint> iterator = availableOutputs.iterator(); iterator.hasNext();) {
-					FluidNetworkEndpoint ne = iterator.next();
-					int toTransfer = dividedTransfer;
-					if (remainder > 0) {
-						toTransfer++;
-						remainder--;
-					}
-
-					if (transfer.isEmpty())
-						break;
-					IFluidHandler handler = ne.provideHandler()
-						.orElse(null);
-					if (handler == null) {
-						iterator.remove();
+					if (!world.isAreaLoaded(connectedPos, 0))
+						continue;
+					if (blockFace.isEquivalent(start))
+						continue;
+					if (hasReachedValidEndpoint(world, blockFace, pull)) {
+						pipeGraph.computeIfAbsent(currentPos, $ -> Pair.of(distance, new IdentityHashMap<>()))
+							.getSecond()
+							.put(face, pull);
+						targets.add(blockFace);
 						continue;
 					}
 
-					FluidStack divided = transfer.copy();
-					divided.setAmount(toTransfer);
-					int fill = handler.fill(divided, action);
-					transfer.setAmount(transfer.getAmount() - fill);
-					if (fill < toTransfer)
-						iterator.remove();
+					FluidTransportBehaviour pipeBehaviour = FluidPropagator.getPipe(world, connectedPos);
+					if (pipeBehaviour == null)
+						continue;
+					if (pipeBehaviour instanceof PumpFluidTransferBehaviour)
+						continue;
+					if (visited.contains(connectedPos))
+						continue;
+					if (distance + 1 >= maxDistance) {
+						pipeGraph.computeIfAbsent(currentPos, $ -> Pair.of(distance, new IdentityHashMap<>()))
+							.getSecond()
+							.put(face, pull);
+						targets.add(blockFace);
+						continue;
+					}
+
+					pipeGraph.computeIfAbsent(currentPos, $ -> Pair.of(distance, new IdentityHashMap<>()))
+						.getSecond()
+						.put(face, pull);
+					pipeGraph.computeIfAbsent(connectedPos, $ -> Pair.of(distance + 1, new IdentityHashMap<>()))
+						.getSecond()
+						.put(face.getOpposite(), !pull);
+					frontier.add(Pair.of(distance + 1, connectedPos));
 				}
-
 			}
-
-			flowSpeed -= transfer.getAmount();
-			transfer = FluidStack.EMPTY;
 		}
+
+		// DFS
+		Map<Integer, Set<BlockFace>> validFaces = new HashMap<>();
+		searchForEndpointRecursively(pipeGraph, targets, validFaces,
+			new BlockFace(start.getPos(), start.getOppositeFace()), pull);
+
+		float pressure = Math.abs(getSpeed());
+		for (Set<BlockFace> set : validFaces.values()) {
+			int parallelBranches = set.size();
+			for (BlockFace face : set) {
+				BlockPos pipePos = face.getPos();
+				Direction pipeSide = face.getFace();
+
+				if (pipePos.equals(pos))
+					continue;
+
+				boolean inbound = pipeGraph.get(pipePos)
+					.getSecond()
+					.get(pipeSide);
+				FluidTransportBehaviour pipeBehaviour = FluidPropagator.getPipe(world, pipePos);
+				if (pipeBehaviour == null)
+					continue;
+
+				pipeBehaviour.addPressure(pipeSide, inbound, pressure / parallelBranches);
+			}
+		}
+
 	}
 
-	public int getFluidTransferSpeed() {
-		float rotationSpeed = Math.abs(getSpeed());
-		int flowSpeed = (int) (rotationSpeed / 2f);
-		if (rotationSpeed != 0 && flowSpeed == 0)
-			flowSpeed = 1;
-		return flowSpeed;
+	protected boolean searchForEndpointRecursively(Map<BlockPos, Pair<Integer, Map<Direction, Boolean>>> pipeGraph,
+		Set<BlockFace> targets, Map<Integer, Set<BlockFace>> validFaces, BlockFace currentFace, boolean pull) {
+		BlockPos currentPos = currentFace.getPos();
+		if (!pipeGraph.containsKey(currentPos))
+			return false;
+		Pair<Integer, Map<Direction, Boolean>> pair = pipeGraph.get(currentPos);
+		int distance = pair.getFirst();
+
+		boolean atLeastOneBranchSuccessful = false;
+		for (Direction nextFacing : Iterate.directions) {
+			if (nextFacing == currentFace.getFace())
+				continue;
+			Map<Direction, Boolean> map = pair.getSecond();
+			if (!map.containsKey(nextFacing))
+				continue;
+
+			BlockFace localTarget = new BlockFace(currentPos, nextFacing);
+			if (targets.contains(localTarget)) {
+				validFaces.computeIfAbsent(distance, $ -> new HashSet<>())
+					.add(localTarget);
+				atLeastOneBranchSuccessful = true;
+				continue;
+			}
+
+			if (map.get(nextFacing) != pull)
+				continue;
+			if (!searchForEndpointRecursively(pipeGraph, targets, validFaces,
+				new BlockFace(currentPos.offset(nextFacing), nextFacing.getOpposite()), pull))
+				continue;
+
+			validFaces.computeIfAbsent(distance, $ -> new HashSet<>())
+				.add(localTarget);
+			atLeastOneBranchSuccessful = true;
+		}
+
+		if (atLeastOneBranchSuccessful)
+			validFaces.computeIfAbsent(distance, $ -> new HashSet<>())
+				.add(currentFace);
+
+		return atLeastOneBranchSuccessful;
+	}
+
+	private boolean hasReachedValidEndpoint(IWorld world, BlockFace blockFace, boolean pull) {
+		BlockPos connectedPos = blockFace.getConnectedPos();
+		BlockState connectedState = world.getBlockState(connectedPos);
+		TileEntity tileEntity = world.getTileEntity(connectedPos);
+		Direction face = blockFace.getFace();
+
+		// facing a pump
+		if (PumpBlock.isPump(connectedState) && connectedState.get(PumpBlock.FACING)
+			.getAxis() == face.getAxis() && tileEntity instanceof PumpTileEntity) {
+			PumpTileEntity pumpTE = (PumpTileEntity) tileEntity;
+			return pumpTE.isPullingOnSide(pumpTE.isFront(blockFace.getOppositeFace())) != pull;
+		}
+
+		// other pipe, no endpoint
+		FluidTransportBehaviour pipe = FluidPropagator.getPipe(world, connectedPos);
+		if (pipe != null && pipe.canHaveFlowToward(connectedState, blockFace.getOppositeFace()))
+			return false;
+
+		// fluid handler endpoint
+		if (tileEntity != null) {
+			LazyOptional<IFluidHandler> capability =
+				tileEntity.getCapability(CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY, face.getOpposite());
+			if (capability.isPresent())
+				return true;
+		}
+
+		// open endpoint
+		return FluidPropagator.isOpenEnd(world, blockFace.getPos(), face);
 	}
 
 	@Override
 	public void write(CompoundNBT compound, boolean clientPacket) {
 		compound.putBoolean("Reversed", reversed);
-		serializeOpenEnds(compound);
 		super.write(compound, clientPacket);
 	}
 
 	@Override
 	protected void fromTag(BlockState state, CompoundNBT compound, boolean clientPacket) {
 		reversed = compound.getBoolean("Reversed");
-		deserializeOpenEnds(compound);
 		super.fromTag(state, compound, clientPacket);
 	}
 
@@ -274,11 +304,10 @@ public class PumpTileEntity extends KineticTileEntity {
 		if (!isSideAccessible(side))
 			return;
 		updatePipeNetwork(isFront(side));
+		getBehaviour(FluidTransportBehaviour.TYPE).wipePressure();
 	}
 
 	protected boolean isFront(Direction side) {
-		if (networks == null)
-			return false;
 		BlockState blockState = getBlockState();
 		if (!(blockState.getBlock() instanceof PumpBlock))
 			return false;
@@ -296,13 +325,8 @@ public class PumpTileEntity extends KineticTileEntity {
 	}
 
 	protected void updatePipeNetwork(boolean front) {
-		if (networks != null)
-			networks.get(front)
-				.clearFlows(world, true);
-		networksToUpdate.get(front)
+		sidesToUpdate.get(front)
 			.setTrue();
-		if (getSpeed() == 0 || (isPullingOnSide(front)) && networks != null)
-			setProvidedFluid(FluidStack.EMPTY);
 	}
 
 	public boolean isSideAccessible(Direction side) {
@@ -317,113 +341,32 @@ public class PumpTileEntity extends KineticTileEntity {
 		return front == reversed;
 	}
 
-	public void spawnParticles() {
-		DistExecutor.runWhenOn(Dist.CLIENT, () -> this::spawnParticlesInner);
-	}
+	class PumpFluidTransferBehaviour extends FluidTransportBehaviour {
 
-	@OnlyIn(Dist.CLIENT)
-	private void spawnParticlesInner() {
-		if (!FluidPipeBehaviour.isRenderEntityWithinDistance(pos))
-			return;
-		for (boolean front : Iterate.trueAndFalse) {
-			Direction side = getFront();
-			if (side == null)
-				return;
-			if (!front)
-				side = side.getOpposite();
-			if (!FluidPropagator.isOpenEnd(world, pos, side))
-				continue;
-			BlockFace key = new BlockFace(pos, side);
-			Map<BlockFace, OpenEndedPipe> map = openEnds.get(front);
-			if (map.containsKey(key)) {
-				FluidStack fluidStack = map.get(key)
-					.getCapability()
-					.map(fh -> fh.getFluidInTank(0))
-					.orElse(FluidStack.EMPTY);
-				if (!fluidStack.isEmpty())
-					spawnPouringLiquid(fluidStack, side, 1);
-			}
-		}
-	}
-
-	@OnlyIn(Dist.CLIENT)
-	private void spawnPouringLiquid(FluidStack fluid, Direction side, int amount) {
-		IParticleData particle = FluidFX.getFluidParticle(fluid);
-		float rimRadius = 1 / 4f + 1 / 64f;
-		boolean inbound = isPullingOnSide(getFront() == side);
-		Vector3d directionVec = Vector3d.of(side.getDirectionVec());
-		FluidFX.spawnPouringLiquid(world, pos, amount, particle, rimRadius, directionVec, inbound);
-	}
-
-	public Map<BlockFace, OpenEndedPipe> getOpenEnds(Direction side) {
-		return openEnds.get(isFront(side));
-	}
-
-	private void serializeOpenEnds(CompoundNBT compound) {
-		compound.put("OpenEnds", openEnds.serializeEach(m -> {
-			CompoundNBT compoundNBT = new CompoundNBT();
-			ListNBT entries = new ListNBT();
-			m.entrySet()
-				.forEach(e -> {
-					CompoundNBT innerCompound = new CompoundNBT();
-					innerCompound.put("Pos", e.getKey()
-						.serializeNBT());
-					e.getValue()
-						.writeToNBT(innerCompound);
-					entries.add(innerCompound);
-				});
-			compoundNBT.put("Entries", entries);
-			return compoundNBT;
-		}));
-	}
-
-	private void deserializeOpenEnds(CompoundNBT compound) {
-		openEnds = Couple.deserializeEach(compound.getList("OpenEnds", NBT.TAG_COMPOUND), c -> {
-			Map<BlockFace, OpenEndedPipe> map = new HashMap<>();
-			NBTHelper.iterateCompoundList(c.getList("Entries", NBT.TAG_COMPOUND), innerCompound -> {
-				BlockFace key = BlockFace.fromNBT(innerCompound.getCompound("Pos"));
-				OpenEndedPipe value = new OpenEndedPipe(key);
-				value.readNBT(innerCompound);
-				map.put(key, value);
-			});
-			return map;
-		});
-
-		compound.put("OpenEnds", openEnds.serializeEach(m -> {
-			CompoundNBT compoundNBT = new CompoundNBT();
-			ListNBT entries = new ListNBT();
-			m.entrySet()
-				.forEach(e -> {
-					CompoundNBT innerCompound = new CompoundNBT();
-					innerCompound.put("Pos", e.getKey()
-						.serializeNBT());
-					e.getValue()
-						.writeToNBT(innerCompound);
-					entries.add(innerCompound);
-				});
-			compoundNBT.put("Entries", entries);
-			return compoundNBT;
-		}));
-	}
-
-	public void setProvidedFluid(FluidStack providedFluid) {
-		this.providedFluid = providedFluid;
-	}
-
-	class PumpAttachmentBehaviour extends FluidPipeAttachmentBehaviour {
-
-		public PumpAttachmentBehaviour(SmartTileEntity te) {
+		public PumpFluidTransferBehaviour(SmartTileEntity te) {
 			super(te);
 		}
 
 		@Override
-		public boolean isPipeConnectedTowards(BlockState state, Direction direction) {
+		public void tick() {
+			super.tick();
+			for (Entry<Direction, PipeConnection> entry : interfaces.entrySet()) {
+				boolean pull = isPullingOnSide(isFront(entry.getKey()));
+				Couple<Float> pressure = entry.getValue().pressure;
+				pressure.set(pull, Math.abs(getSpeed()));
+				pressure.set(!pull, 0f);
+			}
+		}
+
+		@Override
+		public boolean canHaveFlowToward(BlockState state, Direction direction) {
 			return isSideAccessible(direction);
 		}
 
 		@Override
-		public AttachmentTypes getAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state, Direction direction) {
-			AttachmentTypes attachment = super.getAttachment(world, pos, state, direction);
+		public AttachmentTypes getRenderedRimAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state,
+			Direction direction) {
+			AttachmentTypes attachment = super.getRenderedRimAttachment(world, pos, state, direction);
 			if (attachment == AttachmentTypes.RIM)
 				return AttachmentTypes.NONE;
 			return attachment;
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/AxisPipeBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/AxisPipeBlock.java
index a3570ee9a..d93f79610 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/AxisPipeBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/AxisPipeBlock.java
@@ -6,8 +6,8 @@ import java.util.Random;
 
 import com.simibubi.create.AllBlocks;
 import com.simibubi.create.AllShapes;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour;
 import com.simibubi.create.content.contraptions.fluids.FluidPropagator;
+import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.content.contraptions.wrench.IWrenchableWithBracket;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.utility.Iterate;
@@ -22,9 +22,9 @@ import net.minecraft.network.DebugPacketSender;
 import net.minecraft.state.BooleanProperty;
 import net.minecraft.util.ActionResultType;
 import net.minecraft.util.Direction;
-import net.minecraft.util.Hand;
 import net.minecraft.util.Direction.Axis;
 import net.minecraft.util.Direction.AxisDirection;
+import net.minecraft.util.Hand;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.BlockRayTraceResult;
 import net.minecraft.util.math.RayTraceResult;
@@ -126,7 +126,7 @@ public class AxisPipeBlock extends RotatedPillarBlock implements IWrenchableWith
 
 	@Override
 	public Optional<ItemStack> removeBracket(IBlockReader world, BlockPos pos) {
-		FluidPipeAttachmentBehaviour behaviour = TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
+		BracketedTileEntityBehaviour behaviour = TileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
 		if (behaviour == null)
 			return Optional.empty();
 		BlockState bracket = behaviour.getBracket();
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/BracketBlockItem.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/BracketBlockItem.java
index 98e563b1f..2dfa57d2c 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/BracketBlockItem.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/BracketBlockItem.java
@@ -2,7 +2,6 @@ package com.simibubi.create.content.contraptions.fluids.pipes;
 
 import java.util.Optional;
 
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour;
 import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 
@@ -32,38 +31,36 @@ public class BracketBlockItem extends BlockItem {
 		BracketBlock bracketBlock = getBracketBlock();
 		PlayerEntity player = context.getPlayer();
 
-		BracketedTileEntityBehaviour behaviour = TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
+		BracketedTileEntityBehaviour behaviour = TileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
+
 		if (behaviour == null)
-			behaviour = TileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
-
-		if (behaviour != null && behaviour.canHaveBracket()) {
-			if (world.isRemote)
-				return ActionResultType.SUCCESS;
-
-			Optional<BlockState> suitableBracket = bracketBlock.getSuitableBracket(state, context.getFace());
-			if (!suitableBracket.isPresent() && player != null)
-				suitableBracket =
-					bracketBlock.getSuitableBracket(state, Direction.getFacingDirections(player)[0].getOpposite());
-			if (!suitableBracket.isPresent())
-				return ActionResultType.SUCCESS;
-
-			BlockState bracket = behaviour.getBracket();
-			behaviour.applyBracket(suitableBracket.get());
-			if (player == null || !player.isCreative()) {
-				context.getItem()
-					.shrink(1);
-				if (bracket != Blocks.AIR.getDefaultState()) {
-					ItemStack returnedStack = new ItemStack(bracket.getBlock());
-					if (player == null)
-						Block.spawnAsEntity(world, pos, returnedStack);
-					else
-						player.inventory.placeItemBackInInventory(world, returnedStack);
-				}
-			}
+			return ActionResultType.FAIL;
+		if (!behaviour.canHaveBracket())
+			return ActionResultType.FAIL;
+		if (world.isRemote)
 			return ActionResultType.SUCCESS;
-		}
 
-		return ActionResultType.FAIL;
+		Optional<BlockState> suitableBracket = bracketBlock.getSuitableBracket(state, context.getFace());
+		if (!suitableBracket.isPresent() && player != null)
+			suitableBracket =
+				bracketBlock.getSuitableBracket(state, Direction.getFacingDirections(player)[0].getOpposite());
+		if (!suitableBracket.isPresent())
+			return ActionResultType.SUCCESS;
+
+		BlockState bracket = behaviour.getBracket();
+		behaviour.applyBracket(suitableBracket.get());
+		if (player == null || !player.isCreative()) {
+			context.getItem()
+				.shrink(1);
+			if (bracket != Blocks.AIR.getDefaultState()) {
+				ItemStack returnedStack = new ItemStack(bracket.getBlock());
+				if (player == null)
+					Block.spawnAsEntity(world, pos, returnedStack);
+				else
+					player.inventory.placeItemBackInInventory(world, returnedStack);
+			}
+		}
+		return ActionResultType.SUCCESS;
 	}
 
 	private BracketBlock getBracketBlock() {
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java
index ae036a097..df8894f80 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeBlock.java
@@ -7,8 +7,9 @@ import javax.annotation.Nullable;
 
 import com.simibubi.create.AllBlocks;
 import com.simibubi.create.AllTileEntities;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour;
 import com.simibubi.create.content.contraptions.fluids.FluidPropagator;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour;
+import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.content.contraptions.wrench.IWrenchableWithBracket;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.utility.Iterate;
@@ -136,18 +137,17 @@ public class FluidPipeBlock extends SixWayBlock implements IWaterLoggable, IWren
 		return state.getBlock() instanceof FluidPipeBlock;
 	}
 
-	public static boolean canConnectTo(IBlockDisplayReader world, BlockPos pos, BlockState neighbour,
-		Direction blockFace) {
-		if (FluidPropagator.hasFluidCapability(neighbour, world, pos, blockFace))
+	public static boolean canConnectTo(IBlockDisplayReader world, BlockPos neighbourPos, BlockState neighbour, Direction direction) {
+		if (FluidPropagator.hasFluidCapability(world, neighbourPos, direction.getOpposite()))
 			return true;
-		FluidPipeAttachmentBehaviour attachmentBehaviour =
-			TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
+		FluidTransportBehaviour transport = TileEntityBehaviour.get(world, neighbourPos, FluidTransportBehaviour.TYPE);
+		BracketedTileEntityBehaviour bracket = TileEntityBehaviour.get(world, neighbourPos, BracketedTileEntityBehaviour.TYPE);
 		if (isPipe(neighbour))
-			return attachmentBehaviour == null || attachmentBehaviour.getBracket() == Blocks.AIR.getDefaultState()
-				|| FluidPropagator.getStraightPipeAxis(neighbour) == blockFace.getAxis();
-		if (attachmentBehaviour == null)
+			return bracket == null || !bracket.isBacketPresent()
+				|| FluidPropagator.getStraightPipeAxis(neighbour) == direction.getAxis();
+		if (transport == null)
 			return false;
-		return attachmentBehaviour.isPipeConnectedTowards(neighbour, blockFace.getOpposite());
+		return transport.canHaveFlowToward(neighbour, direction.getOpposite());
 	}
 
 	public static boolean shouldDrawRim(IBlockDisplayReader world, BlockPos pos, BlockState state,
@@ -222,8 +222,8 @@ public class FluidPipeBlock extends SixWayBlock implements IWaterLoggable, IWren
 	public BlockState updateBlockState(BlockState state, Direction preferredDirection, @Nullable Direction ignore,
 		IBlockDisplayReader world, BlockPos pos) {
 
-		FluidPipeAttachmentBehaviour behaviour = TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
-		if (behaviour != null && behaviour.getBracket() != Blocks.AIR.getDefaultState())
+		BracketedTileEntityBehaviour bracket = TileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
+		if (bracket != null && bracket.isBacketPresent())
 			return state;
 
 		// Update sides that are not ignored
@@ -260,7 +260,8 @@ public class FluidPipeBlock extends SixWayBlock implements IWaterLoggable, IWren
 
 	@Override
 	public Optional<ItemStack> removeBracket(IBlockReader world, BlockPos pos) {
-		FluidPipeAttachmentBehaviour behaviour = TileEntityBehaviour.get(world, pos, FluidPipeAttachmentBehaviour.TYPE);
+		BracketedTileEntityBehaviour behaviour =
+			BracketedTileEntityBehaviour.get(world, pos, BracketedTileEntityBehaviour.TYPE);
 		if (behaviour == null)
 			return Optional.empty();
 		BlockState bracket = behaviour.getBracket();
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeTileEntity.java
index 566fdfa4d..bc8ee0be2 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidPipeTileEntity.java
@@ -3,8 +3,8 @@ package com.simibubi.create.content.contraptions.fluids.pipes;
 import java.util.List;
 
 import com.simibubi.create.AllBlocks;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeBehaviour;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour;
+import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 
@@ -22,43 +22,40 @@ public class FluidPipeTileEntity extends SmartTileEntity {
 
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
-		behaviours.add(new StandardPipeBehaviour(this));
-		behaviours.add(new StandardPipeAttachmentBehaviour(this));
+		behaviours.add(new StandardPipeFluidTransportBehaviour(this));
+		behaviours.add(new BracketedTileEntityBehaviour(this, this::canHaveBracket));
 	}
 
-	class StandardPipeBehaviour extends FluidPipeBehaviour {
+	private boolean canHaveBracket(BlockState state) {
+		return !(state.getBlock() instanceof EncasedPipeBlock);
+	}
 
-		public StandardPipeBehaviour(SmartTileEntity te) {
+	class StandardPipeFluidTransportBehaviour extends FluidTransportBehaviour {
+
+		public StandardPipeFluidTransportBehaviour(SmartTileEntity te) {
 			super(te);
 		}
 
 		@Override
-		public boolean isConnectedTo(BlockState state, Direction direction) {
+		public boolean canHaveFlowToward(BlockState state, Direction direction) {
 			return (FluidPipeBlock.isPipe(state) || state.getBlock() instanceof EncasedPipeBlock)
 				&& state.get(FluidPipeBlock.FACING_TO_PROPERTY_MAP.get(direction));
 		}
 
-	}
-
-	class StandardPipeAttachmentBehaviour extends FluidPipeAttachmentBehaviour {
-
-		public StandardPipeAttachmentBehaviour(SmartTileEntity te) {
-			super(te);
-		}
-
 		@Override
-		public AttachmentTypes getAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state, Direction direction) {
-			AttachmentTypes attachment = super.getAttachment(world, pos, state, direction);
+		public AttachmentTypes getRenderedRimAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state,
+			Direction direction) {
+			AttachmentTypes attachment = super.getRenderedRimAttachment(world, pos, state, direction);
 
 			if (attachment == AttachmentTypes.RIM && AllBlocks.ENCASED_FLUID_PIPE.has(state))
 				return AttachmentTypes.RIM;
 
 			BlockPos offsetPos = pos.offset(direction);
 			if (!FluidPipeBlock.isPipe(world.getBlockState(offsetPos))) {
-				FluidPipeAttachmentBehaviour attachmentBehaviour =
-					TileEntityBehaviour.get(world, offsetPos, FluidPipeAttachmentBehaviour.TYPE);
-				if (attachmentBehaviour != null && attachmentBehaviour
-					.isPipeConnectedTowards(world.getBlockState(offsetPos), direction.getOpposite()))
+				FluidTransportBehaviour pipeBehaviour =
+					TileEntityBehaviour.get(world, offsetPos, FluidTransportBehaviour.TYPE);
+				if (pipeBehaviour != null
+					&& pipeBehaviour.canHaveFlowToward(world.getBlockState(offsetPos), direction.getOpposite()))
 					return AttachmentTypes.NONE;
 			}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveBlock.java
index 4b30fa022..aa93364de 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveBlock.java
@@ -1,12 +1,16 @@
 package com.simibubi.create.content.contraptions.fluids.pipes;
 
+import java.util.Random;
+
 import com.simibubi.create.AllShapes;
 import com.simibubi.create.AllTileEntities;
 import com.simibubi.create.content.contraptions.base.DirectionalAxisKineticBlock;
+import com.simibubi.create.content.contraptions.fluids.FluidPropagator;
 import com.simibubi.create.foundation.utility.Iterate;
 
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
+import net.minecraft.network.DebugPacketSender;
 import net.minecraft.state.BooleanProperty;
 import net.minecraft.state.StateContainer.Builder;
 import net.minecraft.tileentity.TileEntity;
@@ -17,6 +21,9 @@ import net.minecraft.util.math.shapes.ISelectionContext;
 import net.minecraft.util.math.shapes.VoxelShape;
 import net.minecraft.world.IBlockReader;
 import net.minecraft.world.IWorldReader;
+import net.minecraft.world.TickPriority;
+import net.minecraft.world.World;
+import net.minecraft.world.server.ServerWorld;
 
 public class FluidValveBlock extends DirectionalAxisKineticBlock implements IAxisPipe {
 
@@ -75,4 +82,49 @@ public class FluidValveBlock extends DirectionalAxisKineticBlock implements IAxi
 		return getPipeAxis(state);
 	}
 
+	@Override
+	public void onReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving) {
+		boolean blockTypeChanged = state.getBlock() != newState.getBlock();
+		if (blockTypeChanged && !world.isRemote)
+			FluidPropagator.propagateChangedPipe(world, pos, state);
+		if (state.hasTileEntity() && (blockTypeChanged || !newState.hasTileEntity()))
+			world.removeTileEntity(pos);
+	}
+
+	@Override
+	public boolean isValidPosition(BlockState p_196260_1_, IWorldReader p_196260_2_, BlockPos p_196260_3_) {
+		return true;
+	}
+
+	@Override
+	public void onBlockAdded(BlockState state, World world, BlockPos pos, BlockState oldState, boolean isMoving) {
+		if (world.isRemote)
+			return;
+		if (state != oldState)
+			world.getPendingBlockTicks()
+				.scheduleTick(pos, this, 1, TickPriority.HIGH);
+	}
+
+	@Override
+	public void neighborChanged(BlockState state, World world, BlockPos pos, Block otherBlock, BlockPos neighborPos,
+		boolean isMoving) {
+		DebugPacketSender.func_218806_a(world, pos);
+		Direction d = FluidPropagator.validateNeighbourChange(state, world, pos, otherBlock, neighborPos, isMoving);
+		if (d == null)
+			return;
+		if (!isOpenAt(state, d))
+			return;
+		world.getPendingBlockTicks()
+			.scheduleTick(pos, this, 1, TickPriority.HIGH);
+	}
+
+	public static boolean isOpenAt(BlockState state, Direction d) {
+		return d.getAxis() == getPipeAxis(state);
+	}
+
+	@Override
+	public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random r) {
+		FluidPropagator.propagateChangedPipe(world, pos, state);
+	}
+
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveTileEntity.java
index bc7b7dfa8..fc0009804 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/FluidValveTileEntity.java
@@ -3,11 +3,9 @@ package com.simibubi.create.content.contraptions.fluids.pipes;
 import java.util.List;
 
 import com.simibubi.create.content.contraptions.base.KineticTileEntity;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeBehaviour;
-import com.simibubi.create.content.contraptions.fluids.pipes.StraightPipeTileEntity.StraightPipeAttachmentBehaviour;
+import com.simibubi.create.content.contraptions.fluids.pipes.StraightPipeTileEntity.StraightPipeFluidTransportBehaviour;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
-import com.simibubi.create.foundation.utility.BlockHelper;
 import com.simibubi.create.foundation.utility.LerpedFloat;
 import com.simibubi.create.foundation.utility.LerpedFloat.Chaser;
 
@@ -79,24 +77,23 @@ public class FluidValveTileEntity extends KineticTileEntity {
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
 		behaviours.add(new ValvePipeBehaviour(this));
-		behaviours.add(new StraightPipeAttachmentBehaviour(this));
 	}
 
-	class ValvePipeBehaviour extends FluidPipeBehaviour {
+	class ValvePipeBehaviour extends StraightPipeFluidTransportBehaviour {
 
 		public ValvePipeBehaviour(SmartTileEntity te) {
 			super(te);
 		}
 
 		@Override
-		public boolean isConnectedTo(BlockState state, Direction direction) {
+		public boolean canHaveFlowToward(BlockState state, Direction direction) {
 			return FluidValveBlock.getPipeAxis(state) == direction.getAxis();
 		}
 
 		@Override
-		public boolean canTransferToward(FluidStack fluid, BlockState state, Direction direction, boolean inbound) {
-			if (BlockHelper.hasBlockStateProperty(state, FluidValveBlock.ENABLED) && state.get(FluidValveBlock.ENABLED))
-				return super.canTransferToward(fluid, state, direction, inbound);
+		public boolean canPullFluidFrom(FluidStack fluid, BlockState state, Direction direction) {
+			if (state.contains(FluidValveBlock.ENABLED) && state.get(FluidValveBlock.ENABLED))
+				return super.canPullFluidFrom(fluid, state, direction);
 			return false;
 		}
 
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeBlock.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeBlock.java
index de3964bc9..70ce6c977 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeBlock.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeBlock.java
@@ -117,6 +117,11 @@ public class SmartFluidPipeBlock extends HorizontalFaceBlock implements IAxisPip
 	public static boolean isOpenAt(BlockState state, Direction d) {
 		return d.getAxis() == getPipeAxis(state);
 	}
+	
+	@Override
+	public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random r) {
+		FluidPropagator.propagateChangedPipe(world, pos, state);
+	}
 
 	protected static Axis getPipeAxis(BlockState state) {
 		return state.get(FACE) == AttachFace.WALL ? Axis.Y
@@ -134,11 +139,6 @@ public class SmartFluidPipeBlock extends HorizontalFaceBlock implements IAxisPip
 		return AllTileEntities.SMART_FLUID_PIPE.create();
 	}
 
-	@Override
-	public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random r) {
-		FluidPropagator.propagateChangedPipe(world, pos, state);
-	}
-
 	@Override
 	public VoxelShape getShape(BlockState state, IBlockReader p_220053_2_, BlockPos p_220053_3_,
 		ISelectionContext p_220053_4_) {
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeTileEntity.java
index dba49d65e..8b9544464 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/SmartFluidPipeTileEntity.java
@@ -3,9 +3,8 @@ package com.simibubi.create.content.contraptions.fluids.pipes;
 import java.util.List;
 
 import com.mojang.blaze3d.matrix.MatrixStack;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeBehaviour;
 import com.simibubi.create.content.contraptions.fluids.FluidPropagator;
-import com.simibubi.create.content.contraptions.fluids.pipes.StraightPipeTileEntity.StraightPipeAttachmentBehaviour;
+import com.simibubi.create.content.contraptions.fluids.pipes.StraightPipeTileEntity.StraightPipeFluidTransportBehaviour;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.ValueBoxTransform;
@@ -34,7 +33,6 @@ public class SmartFluidPipeTileEntity extends SmartTileEntity {
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
 		behaviours.add(new SmartPipeBehaviour(this));
-		behaviours.add(new StraightPipeAttachmentBehaviour(this));
 		behaviours.add(filter = new FilteringBehaviour(this, new SmartPipeFilterSlot()).forFluids()
 			.withCallback(this::onFilterChanged));
 	}
@@ -45,21 +43,21 @@ public class SmartFluidPipeTileEntity extends SmartTileEntity {
 		FluidPropagator.propagateChangedPipe(world, pos, getBlockState());
 	}
 
-	class SmartPipeBehaviour extends FluidPipeBehaviour {
+	class SmartPipeBehaviour extends StraightPipeFluidTransportBehaviour {
 
 		public SmartPipeBehaviour(SmartTileEntity te) {
 			super(te);
 		}
 
 		@Override
-		public boolean canTransferToward(FluidStack fluid, BlockState state, Direction direction, boolean inbound) {
+		public boolean canPullFluidFrom(FluidStack fluid, BlockState state, Direction direction) {
 			if (fluid.isEmpty() || filter != null && filter.test(fluid))
-				return super.canTransferToward(fluid, state, direction, inbound);
+				return super.canPullFluidFrom(fluid, state, direction);
 			return false;
 		}
 
 		@Override
-		public boolean isConnectedTo(BlockState state, Direction direction) {
+		public boolean canHaveFlowToward(BlockState state, Direction direction) {
 			return state.getBlock() instanceof SmartFluidPipeBlock
 				&& SmartFluidPipeBlock.getPipeAxis(state) == direction.getAxis();
 		}
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/StraightPipeTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/StraightPipeTileEntity.java
index 78e9ec2a2..3ebea8848 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/StraightPipeTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/StraightPipeTileEntity.java
@@ -2,8 +2,8 @@ package com.simibubi.create.content.contraptions.fluids.pipes;
 
 import java.util.List;
 
-import com.simibubi.create.content.contraptions.fluids.FluidPipeAttachmentBehaviour;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeBehaviour;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour;
+import com.simibubi.create.content.contraptions.relays.elementary.BracketedTileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 
@@ -23,32 +23,25 @@ public class StraightPipeTileEntity extends SmartTileEntity {
 
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
-		behaviours.add(new StraightPipeBehaviour(this));
-		behaviours.add(new StraightPipeAttachmentBehaviour(this));
+		behaviours.add(new StraightPipeFluidTransportBehaviour(this));
+		behaviours.add(new BracketedTileEntityBehaviour(this));
 	}
 
-	class StraightPipeBehaviour extends FluidPipeBehaviour {
+	static class StraightPipeFluidTransportBehaviour extends FluidTransportBehaviour {
 
-		public StraightPipeBehaviour(SmartTileEntity te) {
+		public StraightPipeFluidTransportBehaviour(SmartTileEntity te) {
 			super(te);
 		}
-
+		
 		@Override
-		public boolean isConnectedTo(BlockState state, Direction direction) {
+		public boolean canHaveFlowToward(BlockState state, Direction direction) {
 			return state.get(AxisPipeBlock.AXIS) == direction.getAxis();
 		}
 
-	}
-
-	static class StraightPipeAttachmentBehaviour extends FluidPipeAttachmentBehaviour {
-
-		public StraightPipeAttachmentBehaviour(SmartTileEntity te) {
-			super(te);
-		}
-
 		@Override
-		public AttachmentTypes getAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state, Direction direction) {
-			AttachmentTypes attachment = super.getAttachment(world, pos, state, direction);
+		public AttachmentTypes getRenderedRimAttachment(IBlockDisplayReader world, BlockPos pos, BlockState state,
+			Direction direction) {
+			AttachmentTypes attachment = super.getRenderedRimAttachment(world, pos, state, direction);
 			BlockState otherState = world.getBlockState(pos.offset(direction));
 
 			Axis axis = IAxisPipe.getAxisOf(state);
@@ -57,7 +50,7 @@ public class StraightPipeTileEntity extends SmartTileEntity {
 			if (axis == otherAxis && axis != null)
 				if (state.getBlock() == otherState.getBlock() || direction.getAxisDirection() == AxisDirection.POSITIVE)
 					return AttachmentTypes.NONE;
-			
+
 			if (otherState.getBlock() instanceof FluidValveBlock
 				&& FluidValveBlock.getPipeAxis(otherState) == direction.getAxis())
 				return AttachmentTypes.NONE;
diff --git a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/TransparentStraightPipeRenderer.java b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/TransparentStraightPipeRenderer.java
index c5d446bb6..243cc52d7 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/TransparentStraightPipeRenderer.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/fluids/pipes/TransparentStraightPipeRenderer.java
@@ -1,13 +1,13 @@
 package com.simibubi.create.content.contraptions.fluids.pipes;
 
 import com.mojang.blaze3d.matrix.MatrixStack;
-import com.simibubi.create.content.contraptions.fluids.FluidPipeBehaviour;
+import com.simibubi.create.content.contraptions.fluids.FluidTransportBehaviour;
+import com.simibubi.create.content.contraptions.fluids.PipeConnection.Flow;
 import com.simibubi.create.foundation.fluid.FluidRenderer;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.renderer.SafeTileEntityRenderer;
 import com.simibubi.create.foundation.utility.Iterate;
 import com.simibubi.create.foundation.utility.LerpedFloat;
-import com.simibubi.create.foundation.utility.Pair;
 
 import net.minecraft.client.renderer.IRenderTypeBuffer;
 import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
@@ -23,32 +23,40 @@ public class TransparentStraightPipeRenderer extends SafeTileEntityRenderer<Stra
 	@Override
 	protected void renderSafe(StraightPipeTileEntity te, float partialTicks, MatrixStack ms, IRenderTypeBuffer buffer,
 		int light, int overlay) {
-		FluidPipeBehaviour pipe = te.getBehaviour(FluidPipeBehaviour.TYPE);
+		FluidTransportBehaviour pipe = te.getBehaviour(FluidTransportBehaviour.TYPE);
 		if (pipe == null)
 			return;
-		FluidStack fluidStack = pipe.getFluid();
-		if (fluidStack.isEmpty())
-			return;
 
 		for (Direction side : Iterate.directions) {
-			if (!pipe.isConnectedTo(te.getBlockState(), side))
+
+			Flow flow = pipe.getFlow(side);
+			if (flow == null)
 				continue;
-			Pair<Boolean, LerpedFloat> strogestFlow = pipe.getStrogestFlow(side);
-			if (strogestFlow == null)
+			FluidStack fluidStack = flow.fluid;
+			if (fluidStack.isEmpty())
 				continue;
-			LerpedFloat second = strogestFlow.getSecond();
-			if (second == null)
+			LerpedFloat progress = flow.progress;
+			if (progress == null)
 				continue;
 
-			float value = second.getValue(partialTicks);
-			Boolean inbound = strogestFlow.getFirst();
-			if (value == 1 && !inbound) {
-				FluidPipeBehaviour adjacent = TileEntityBehaviour.get(te.getWorld(), te.getPos()
-					.offset(side), FluidPipeBehaviour.TYPE);
-
-				if (adjacent != null && adjacent.getFluid()
-					.isEmpty())
-					value -= 1e-6f;
+			float value = progress.getValue(partialTicks);
+			boolean inbound = flow.inbound;
+			if (value == 1) {
+				if (inbound) {
+					Flow opposite = pipe.getFlow(side.getOpposite());
+					if (opposite == null)
+						value -= 1e-6f;
+				} else {
+					FluidTransportBehaviour adjacent = TileEntityBehaviour.get(te.getWorld(), te.getPos()
+						.offset(side), FluidTransportBehaviour.TYPE);
+					if (adjacent == null)
+						value -= 1e-6f;
+					else {
+						Flow other = adjacent.getFlow(side.getOpposite());
+						if (other == null || !other.inbound && !other.complete)
+							value -= 1e-6f;
+					}
+				}
 			}
 
 			FluidRenderer.renderFluidStream(fluidStack, side, 3 / 16f, value, inbound, buffer, ms, light);
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 82a8db334..cd96a9655 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
@@ -182,6 +182,12 @@ public class BasinBlock extends Block implements ITE<BasinTileEntity>, IWrenchab
 	}
 
 	public static boolean canOutputTo(IBlockReader world, BlockPos basinPos, Direction direction) {
+		BlockPos neighbour = basinPos.offset(direction);
+		if (!world.getBlockState(neighbour)
+			.getCollisionShape(world, neighbour)
+			.isEmpty())
+			return false;
+
 		BlockPos offset = basinPos.down()
 			.offset(direction);
 		DirectBeltInputBehaviour directBeltInputBehaviour =
diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/BracketedTileEntityBehaviour.java b/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/BracketedTileEntityBehaviour.java
index 4ee03298d..6710dbb0b 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/BracketedTileEntityBehaviour.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/BracketedTileEntityBehaviour.java
@@ -1,26 +1,37 @@
 package com.simibubi.create.content.contraptions.relays.elementary;
 
 import java.util.Optional;
+import java.util.function.Predicate;
 
+import com.google.common.base.Predicates;
 import com.simibubi.create.foundation.tileEntity.SmartTileEntity;
 import com.simibubi.create.foundation.tileEntity.TileEntityBehaviour;
 import com.simibubi.create.foundation.tileEntity.behaviour.BehaviourType;
 import com.simibubi.create.foundation.utility.NBTHelper;
 
+import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
 import net.minecraft.block.Blocks;
 import net.minecraft.nbt.CompoundNBT;
 import net.minecraft.nbt.NBTUtil;
+import net.minecraft.world.World;
 
 public class BracketedTileEntityBehaviour extends TileEntityBehaviour {
-	
+
 	public static BehaviourType<BracketedTileEntityBehaviour> TYPE = new BehaviourType<>();
 
 	private Optional<BlockState> bracket;
 	private boolean reRender;
-	
+
+	private Predicate<BlockState> pred;
+
 	public BracketedTileEntityBehaviour(SmartTileEntity te) {
+		this(te, Predicates.alwaysTrue());
+	}
+
+	public BracketedTileEntityBehaviour(SmartTileEntity te, Predicate<BlockState> pred) {
 		super(te);
+		this.pred = pred;
 		bracket = Optional.empty();
 	}
 
@@ -28,23 +39,30 @@ public class BracketedTileEntityBehaviour extends TileEntityBehaviour {
 	public BehaviourType<?> getType() {
 		return TYPE;
 	}
-	
+
 	public void applyBracket(BlockState state) {
 		this.bracket = Optional.of(state);
 		reRender = true;
 		tileEntity.notifyUpdate();
 	}
-	
+
 	public void removeBracket() {
+		World world = getWorld();
+		if (!world.isRemote)
+			world.playEvent(2001, getPos(), Block.getStateId(getBracket()));
 		this.bracket = Optional.empty();
 		reRender = true;
 		tileEntity.notifyUpdate();
 	}
 
+	public boolean isBacketPresent() {
+		return getBracket() != Blocks.AIR.getDefaultState();
+	}
+
 	public BlockState getBracket() {
 		return bracket.orElse(Blocks.AIR.getDefaultState());
 	}
-	
+
 	@Override
 	public void write(CompoundNBT nbt, boolean clientPacket) {
 		bracket.ifPresent(p -> nbt.put("Bracket", NBTUtil.writeBlockState(p)));
@@ -64,12 +82,9 @@ public class BracketedTileEntityBehaviour extends TileEntityBehaviour {
 			getWorld().notifyBlockUpdate(getPos(), tileEntity.getBlockState(), tileEntity.getBlockState(), 16);
 		super.read(nbt, clientPacket);
 	}
-	
+
 	public boolean canHaveBracket() {
-		BlockState blockState = tileEntity.getBlockState();
-		if (blockState.getBlock() instanceof AbstractShaftBlock)
-			return true;
-		return false;
+		return pred.test(tileEntity.getBlockState());
 	}
 
 }
diff --git a/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/SimpleKineticTileEntity.java b/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/SimpleKineticTileEntity.java
index fbe11f3bb..a7cda5e6c 100644
--- a/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/SimpleKineticTileEntity.java
+++ b/src/main/java/com/simibubi/create/content/contraptions/relays/elementary/SimpleKineticTileEntity.java
@@ -9,20 +9,20 @@ import net.minecraft.tileentity.TileEntityType;
 import net.minecraft.util.math.AxisAlignedBB;
 
 public class SimpleKineticTileEntity extends KineticTileEntity {
-	
+
 	public SimpleKineticTileEntity(TileEntityType<? extends SimpleKineticTileEntity> type) {
 		super(type);
 	}
-	
+
 	@Override
 	public void addBehaviours(List<TileEntityBehaviour> behaviours) {
-		behaviours.add(new BracketedTileEntityBehaviour(this));
+		behaviours.add(new BracketedTileEntityBehaviour(this, state -> state.getBlock() instanceof AbstractShaftBlock));
 		super.addBehaviours(behaviours);
 	}
-	
+
 	@Override
 	public AxisAlignedBB getRenderBoundingBox() {
 		return new AxisAlignedBB(pos).grow(1);
 	}
-	
+
 }
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 27f9e9f78..b5bdb822c 100644
--- a/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java
+++ b/src/main/java/com/simibubi/create/foundation/fluid/FluidHelper.java
@@ -50,6 +50,14 @@ public class FluidHelper {
 		BlockState blockState = fluid.getDefaultState().getBlockState();
 		return blockState != null && blockState != Blocks.AIR.getDefaultState();
 	}
+	
+	public static FluidStack copyStackWithAmount(FluidStack fs, int amount) {
+		if (fs.isEmpty())
+			return FluidStack.EMPTY;
+		FluidStack copy = fs.copy();
+		copy.setAmount(amount);
+		return copy;
+	}
 
 	public static Fluid convertToFlowing(Fluid fluid) {
 		if (fluid == Fluids.WATER)
diff --git a/src/main/java/com/simibubi/create/foundation/utility/outliner/Outline.java b/src/main/java/com/simibubi/create/foundation/utility/outliner/Outline.java
index d71b18283..94321a234 100644
--- a/src/main/java/com/simibubi/create/foundation/utility/outliner/Outline.java
+++ b/src/main/java/com/simibubi/create/foundation/utility/outliner/Outline.java
@@ -48,6 +48,10 @@ public abstract class Outline {
 	}
 
 	public void renderAACuboidLine(MatrixStack ms, SuperRenderTypeBuffer buffer, Vector3d start, Vector3d end) {
+		float lineWidth = params.getLineWidth();
+		if (lineWidth == 0)
+			return;
+		
 		IVertexBuilder builder = buffer.getBuffer(RenderTypes.getOutlineSolid());
 
 		Vector3d diff = end.subtract(start);
@@ -58,7 +62,6 @@ public abstract class Outline {
 			diff = diff.scale(-1);
 		}
 
-		float lineWidth = params.getLineWidth();
 		Vector3d extension = diff.normalize()
 			.scale(lineWidth / 2);
 		Vector3d plane = VecHelper.axisAlingedPlaneOf(diff);
diff --git a/src/main/resources/assets/create/lang/default/messages.json b/src/main/resources/assets/create/lang/default/messages.json
index 14c300883..badd48a55 100644
--- a/src/main/resources/assets/create/lang/default/messages.json
+++ b/src/main/resources/assets/create/lang/default/messages.json
@@ -455,6 +455,8 @@
 	"create.hint.upward_funnel": "can only transfer items inserted by _Arms_, fan-powered _Chutes_, or items _thrown_ at them. Try building some _Chutes_ if you are looking to move your items _vertically_.",
 	"create.hint.empty_bearing.title": "Update Bearing",
 	"create.hint.empty_bearing": "_Right-click_ the bearing with an _empty_ _hand_ to _attach_ the structure you just built in front of it.",
+	"create.hint.full_deployer.title": "Deployer Item Overflow",
+	"create.hint.full_deployer": "It appears this _Deployer_ contains _excess_ _items_ that need to be _extracted._ Use a _hopper,_ _funnel_ or other means to free it from its overflow.",
 
 	"create.gui.config.overlay1": "Hi :)",
 	"create.gui.config.overlay2": "This is a sample overlay",