diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/CargoCrateBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/CargoCrateBlockEntity.kt
index 6d7b5c27c..fd17ee839 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/CargoCrateBlockEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/decorative/CargoCrateBlockEntity.kt
@@ -32,6 +32,8 @@ import ru.dbotthepony.mc.otm.block.decorative.CargoCrateBlock
 import ru.dbotthepony.mc.otm.block.entity.MatteryDeviceBlockEntity
 import ru.dbotthepony.mc.otm.container.MatteryContainer
 import ru.dbotthepony.mc.otm.container.HandlerFilter
+import ru.dbotthepony.mc.otm.container.slotted.FilteredContainerSlot
+import ru.dbotthepony.mc.otm.container.slotted.SlottedContainer
 import ru.dbotthepony.mc.otm.core.TranslatableComponent
 import ru.dbotthepony.mc.otm.core.nbt.map
 import ru.dbotthepony.mc.otm.core.nbt.set
@@ -43,20 +45,24 @@ class CargoCrateBlockEntity(
 	p_155229_: BlockPos,
 	p_155230_: BlockState
 ) : MatteryDeviceBlockEntity(MBlockEntities.CARGO_CRATE, p_155229_, p_155230_) {
-	val container = MatteryContainer(this::setChanged, CAPACITY).also(::addDroppableContainer)
+	private inner class Slot(container: SlottedContainer, slot: Int) : FilteredContainerSlot(container, slot) {
+		override fun canAutomationPlaceItem(itemStack: ItemStack): Boolean {
+			return super<FilteredContainerSlot>.canAutomationPlaceItem(itemStack) && lootTable == null
+		}
+
+		override fun canAutomationTakeItem(desired: Int): Boolean {
+			return super.canAutomationTakeItem(desired) && lootTable == null
+		}
+	}
+
+	val container = SlottedContainer.Builder()
+		.add(CAPACITY, ::Slot)
+		.onChanged(::setChanged)
+		.build()
+		.also(::addDroppableContainer)
 
 	private var interactingPlayers = 0
 
-	val handler = container.handler(object : HandlerFilter {
-		override fun canInsert(slot: Int, stack: ItemStack): Boolean {
-			return lootTable == null
-		}
-
-		override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
-			return lootTable == null
-		}
-	})
-
 	override fun beforeDroppingItems(oldBlockState: BlockState, level: Level, blockPos: BlockPos, newBlockState: BlockState, movedByPiston: Boolean) {
 		unpackLootTable()
 	}
@@ -88,7 +94,7 @@ class CargoCrateBlockEntity(
 	}
 
 	init {
-		exposeGlobally(Capabilities.ItemHandler.BLOCK, handler)
+		exposeGlobally(Capabilities.ItemHandler.BLOCK, container)
 		savetablesLevel.stateful(::container, INVENTORY_KEY)
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/MatteryScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/MatteryScreen.kt
index 2eeccb3e4..b0fed5874 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/MatteryScreen.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/MatteryScreen.kt
@@ -348,8 +348,6 @@ abstract class MatteryScreen<T : MatteryMenu>(menu: T, inventory: Inventory, tit
 					override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean {
 						return false
 					}
-
-					override var slotFilter: Item? by slot.filter!!
 				}
 			}
 
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/CargoCrateScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/CargoCrateScreen.kt
index 26bb90d13..974878362 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/CargoCrateScreen.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/decorative/CargoCrateScreen.kt
@@ -20,7 +20,7 @@ class CargoCrateScreen(menu: CargoCrateMenu, inventory: Inventory, title: Compon
 		val grid = GridPanel(this, frame, 8f, 18f, 9f * 18f, 6f * 18f, 9, 6)
 
 		for (slot in menu.storageSlots)
-			UserFilteredSlotPanel.of(this, grid, slot)
+			UserFilteredSlotPanel(this, grid, slot)
 
 		val controls = DeviceControls(this, frame)
 
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt
index caa01f6aa..09588c48d 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/InventorySlotPanel.kt
@@ -18,10 +18,6 @@ open class InventorySlotPanel<out S : MatteryScreen<*>, out T : MatteryMenu.Inve
 	x: Float = 0f,
 	y: Float = 0f,
 ) : UserFilteredSlotPanel<S, T>(screen, parent, slot, x, y, SIZE, SIZE) {
-	override var slotFilter: Item?
-		get() = slot.filter?.get()
-		set(value) { slot.filter?.accept(value) }
-
 	override fun renderBackgroundBeforeFilter(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
 		if (slot.chargeFlag?.get() == true) {
 			Widgets18.CHARGE_SLOT_BACKGROUND.render(graphics, 0f, 0f, width, height)
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/SlotPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/SlotPanel.kt
index e7139cff1..6453b8d7b 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/SlotPanel.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/SlotPanel.kt
@@ -5,9 +5,13 @@ package ru.dbotthepony.mc.otm.client.screen.panels.slot
 import com.mojang.blaze3d.systems.RenderSystem
 import net.minecraft.ChatFormatting
 import net.minecraft.client.renderer.GameRenderer
+import net.minecraft.network.chat.Component
 import net.minecraft.world.inventory.AbstractContainerMenu
 import net.minecraft.world.inventory.Slot
 import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions
+import ru.dbotthepony.kommons.math.RGBAColor
 import ru.dbotthepony.mc.otm.client.render.MGUIGraphics
 import ru.dbotthepony.mc.otm.client.minecraft
 import ru.dbotthepony.mc.otm.client.render.Widgets18
@@ -15,6 +19,10 @@ import ru.dbotthepony.mc.otm.client.screen.MatteryScreen
 import ru.dbotthepony.mc.otm.client.screen.panels.EditablePanel
 import ru.dbotthepony.mc.otm.compat.itemborders.isItemBordersLoaded
 import ru.dbotthepony.mc.otm.compat.itemborders.renderSlotBorder
+import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot
+import ru.dbotthepony.mc.otm.container.util.containerSlotOrNull
+import ru.dbotthepony.mc.otm.core.TextComponent
+import ru.dbotthepony.mc.otm.core.TranslatableComponent
 import javax.annotation.Nonnull
 import kotlin.math.roundToInt
 
@@ -52,6 +60,31 @@ open class SlotPanel<out S : MatteryScreen<*>, out T : Slot>(
 		}
 	}
 
+	protected open fun renderBackgroundBeforeFilter(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {}
+
+	override fun renderSlotBackground(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
+		super.renderSlotBackground(graphics, mouseX, mouseY, partialTick)
+
+		val containerSlot = slot.container.containerSlotOrNull(slot.slotIndex)
+
+		if (containerSlot is IFilteredContainerSlot) {
+			renderBackgroundBeforeFilter(graphics, mouseX, mouseY, partialTick)
+
+			if (containerSlot.filter !== null) {
+				if (containerSlot.filter !== Items.AIR) {
+					val itemStack = ItemStack(containerSlot.filter!!, 1)
+
+					screen.renderItemStack(graphics, itemStack, null)
+					clearDepth(graphics)
+
+					graphics.renderRect(0f, 0f, width, height, color = SLOT_FILTER_COLOR)
+				} else {
+					graphics.renderRect(0f, 0f, width, height, color = SLOT_BLOCK_COLOR)
+				}
+			}
+		}
+	}
+
 	override fun innerRender(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
 		slot.x = absoluteX.roundToInt() - screen.guiLeft
 		slot.y = absoluteY.roundToInt() - screen.guiTop
@@ -121,9 +154,46 @@ open class SlotPanel<out S : MatteryScreen<*>, out T : Slot>(
 	}
 
 	override fun innerRenderTooltips(@Nonnull graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
+		val slot = slot.container.containerSlotOrNull(slot.containerSlot) as? IFilteredContainerSlot
+
+		if (isHovered && slot?.filter != null && slot.filter !== Items.AIR && itemStack.isEmpty) {
+			val itemstack = ItemStack(slot.filter!!, 1)
+
+			graphics.renderComponentTooltip(
+				IClientItemExtensions.of(itemstack).getFont(itemstack, IClientItemExtensions.FontContext.TOOLTIP) ?: font,
+				getItemStackTooltip(itemstack).toMutableList().also {
+					it.add(0, TranslatableComponent("otm.gui.slot_filter.filtered").withStyle(ChatFormatting.GRAY))
+					it.add(1, TranslatableComponent("otm.gui.slot_filter.hint").withStyle(ChatFormatting.GRAY))
+					it.add(2, TextComponent(""))
+				},
+				mouseX.toInt(),
+				mouseY.toInt(),
+				itemstack
+			)
+
+			return true
+		} else if (isHovered && slot?.filter === Items.AIR && itemStack.isEmpty) {
+			graphics.renderComponentTooltip(
+				font,
+				ArrayList<Component>().also {
+					it.add(TranslatableComponent("otm.gui.slot_filter.forbidden").withStyle(ChatFormatting.GRAY))
+					it.add(TranslatableComponent("otm.gui.slot_filter.hint").withStyle(ChatFormatting.GRAY))
+				},
+				mouseX.toInt(),
+				mouseY.toInt()
+			)
+
+			return true
+		}
+
 		// no op, screen does it for us (completely)
 		return false
 	}
+
+	companion object {
+		val SLOT_FILTER_COLOR = RGBAColor(85, 113, 216, 150)
+		val SLOT_BLOCK_COLOR = RGBAColor(219, 113, 113, 150)
+	}
 }
 
 fun <S : MatteryScreen<*>, T : Slot> BatterySlotPanel(
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/UserFilteredSlotPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/UserFilteredSlotPanel.kt
index 86c93d76a..8fdb61822 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/UserFilteredSlotPanel.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/slot/UserFilteredSlotPanel.kt
@@ -1,28 +1,16 @@
 package ru.dbotthepony.mc.otm.client.screen.panels.slot
 
 import com.mojang.blaze3d.platform.InputConstants
-import net.minecraft.ChatFormatting
-import net.minecraft.network.chat.Component
-import net.minecraft.world.inventory.Slot
-import net.minecraft.world.item.Item
-import net.minecraft.world.item.ItemStack
-import net.minecraft.world.item.Items
-import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions
-import ru.dbotthepony.mc.otm.client.render.MGUIGraphics
 import ru.dbotthepony.mc.otm.client.isCtrlDown
 import ru.dbotthepony.mc.otm.client.minecraft
 import ru.dbotthepony.mc.otm.client.playGuiClickSound
 import ru.dbotthepony.mc.otm.client.screen.MatteryScreen
 import ru.dbotthepony.mc.otm.client.screen.panels.EditablePanel
-import ru.dbotthepony.mc.otm.core.TextComponent
-import ru.dbotthepony.mc.otm.core.TranslatableComponent
-import ru.dbotthepony.kommons.math.RGBAColor
-import ru.dbotthepony.kommons.util.Delegate
-import ru.dbotthepony.kommons.util.getValue
-import ru.dbotthepony.kommons.util.setValue
+import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot
+import ru.dbotthepony.mc.otm.container.util.containerSlot
 import ru.dbotthepony.mc.otm.menu.UserFilteredSlot
 
-abstract class UserFilteredSlotPanel<out S : MatteryScreen<*>, out T : Slot>(
+open class UserFilteredSlotPanel<out S : MatteryScreen<*>, out T : UserFilteredSlot>(
 	screen: S,
 	parent: EditablePanel<*>?,
 	slot: T,
@@ -31,77 +19,24 @@ abstract class UserFilteredSlotPanel<out S : MatteryScreen<*>, out T : Slot>(
 	width: Float = SIZE,
 	height: Float = SIZE,
 ) : SlotPanel<S, T>(screen, parent, slot, x, y, width, height) {
-	abstract var slotFilter: Item?
-
-	protected open fun renderBackgroundBeforeFilter(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {}
-
-	override fun renderSlotBackground(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
-		super.renderSlotBackground(graphics, mouseX, mouseY, partialTick)
-
-		renderBackgroundBeforeFilter(graphics, mouseX, mouseY, partialTick)
-
-		if (slotFilter != null) {
-			if (slotFilter !== Items.AIR) {
-				val itemStack = ItemStack(slotFilter!!, 1)
-
-				screen.renderItemStack(graphics, itemStack, null)
-				clearDepth(graphics)
-
-				graphics.renderRect(0f, 0f, width, height, color = SLOT_FILTER_COLOR)
-			} else {
-				graphics.renderRect(0f, 0f, width, height, color = SLOT_BLOCK_COLOR)
-			}
-		}
-	}
-
-	override fun innerRenderTooltips(graphics: MGUIGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
-		if (isHovered && slotFilter != null && slotFilter !== Items.AIR && itemStack.isEmpty) {
-			val itemstack = ItemStack(slotFilter!!, 1)
-
-			graphics.renderComponentTooltip(
-				IClientItemExtensions.of(itemstack).getFont(itemstack, IClientItemExtensions.FontContext.TOOLTIP) ?: font,
-				getItemStackTooltip(itemstack).toMutableList().also {
-					it.add(0, TranslatableComponent("otm.gui.slot_filter.filtered").withStyle(ChatFormatting.GRAY))
-					it.add(1, TranslatableComponent("otm.gui.slot_filter.hint").withStyle(ChatFormatting.GRAY))
-					it.add(2, TextComponent(""))
-				},
-				mouseX.toInt(),
-				mouseY.toInt(),
-				itemstack
-			)
-
-			return true
-		} else if (isHovered && slotFilter === Items.AIR && itemStack.isEmpty) {
-			graphics.renderComponentTooltip(
-				font,
-				ArrayList<Component>().also {
-					it.add(TranslatableComponent("otm.gui.slot_filter.forbidden").withStyle(ChatFormatting.GRAY))
-					it.add(TranslatableComponent("otm.gui.slot_filter.hint").withStyle(ChatFormatting.GRAY))
-				},
-				mouseX.toInt(),
-				mouseY.toInt()
-			)
-
-			return true
-		}
-
-		return super.innerRenderTooltips(graphics, mouseX, mouseY, partialTick)
-	}
-
 	override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
+		if (slot.filterInput == null)
+			return super.mouseClickedInner(x, y, button)
+
+		val containerSlot = slot.containerSlot() as IFilteredContainerSlot
+
 		if (button == InputConstants.MOUSE_BUTTON_LEFT && minecraft.window.isCtrlDown) {
-			if (slotFilter === null) {
+			if (containerSlot.filter === null) {
 				if (screen.menu.carried.isEmpty) {
-					slotFilter = slot.item.item
+					slot.filterInput!!.accept(slot.item.item)
 				} else {
-					slotFilter = screen.menu.carried.item
+					slot.filterInput!!.accept(screen.menu.carried.item)
 				}
 			} else {
-				slotFilter = null
+				slot.filterInput!!.accept(null)
 			}
 
 			playGuiClickSound()
-
 			return true
 		} else {
 			return super.mouseClickedInner(x, y, button)
@@ -115,40 +50,4 @@ abstract class UserFilteredSlotPanel<out S : MatteryScreen<*>, out T : Slot>(
 
 		return super.mouseReleasedInner(x, y, button)
 	}
-
-	companion object {
-		val SLOT_FILTER_COLOR = RGBAColor(85, 113, 216, 150)
-		val SLOT_BLOCK_COLOR = RGBAColor(219, 113, 113, 150)
-
-		fun <S : MatteryScreen<*>, T : Slot> of(
-			screen: S,
-			parent: EditablePanel<*>?,
-			slot: T,
-			x: Float = 0f,
-			y: Float = 0f,
-			width: Float = SIZE,
-			height: Float = SIZE,
-			filter: Delegate<Item?>
-		): UserFilteredSlotPanel<S, T> {
-			return object : UserFilteredSlotPanel<S, T>(screen, parent, slot, x, y, width, height) {
-				override var slotFilter: Item? by filter
-			}
-		}
-
-		fun <S : MatteryScreen<*>, T : UserFilteredSlot> of(
-			screen: S,
-			parent: EditablePanel<*>?,
-			slot: T,
-			x: Float = 0f,
-			y: Float = 0f,
-			width: Float = SIZE,
-			height: Float = SIZE,
-		): UserFilteredSlotPanel<S, T> {
-			return object : UserFilteredSlotPanel<S, T>(screen, parent, slot, x, y, width, height) {
-				override var slotFilter: Item?
-					get() = slot.filter?.get()
-					set(value) { slot.filter?.accept(value) }
-			}
-		}
-	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/tech/ItemHatchScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/tech/ItemHatchScreen.kt
index 7408515ea..913f5bdbf 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/tech/ItemHatchScreen.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/tech/ItemHatchScreen.kt
@@ -20,7 +20,7 @@ class ItemHatchScreen(menu: ItemHatchMenu, inventory: Inventory, title: Componen
 		val grid = GridPanel(this, frame, 8f, 18f, 9f * 18f, 6f * 18f, 9, 6)
 
 		for (slot in menu.storageSlots)
-			UserFilteredSlotPanel.of(this, grid, slot)
+			UserFilteredSlotPanel(this, grid, slot)
 
 		if (menu.isInput) {
 			val controls = DeviceControls(this, frame)
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/IAutomatedContainerSlot.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/IAutomatedContainerSlot.kt
index 16ca182f2..2c7df2085 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/IAutomatedContainerSlot.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/IAutomatedContainerSlot.kt
@@ -4,7 +4,7 @@ import net.minecraft.world.item.ItemStack
 import net.neoforged.neoforge.items.IItemHandler
 
 /**
- * Slot of [ISlottedContainer], with additional methods to implement interaction behavior for both for players and mechanisms
+ * Slot of [IAutomatedContainer], with additional methods to implement interaction behavior for both for players and mechanisms
  */
 interface IAutomatedContainerSlot : IContainerSlot {
 	fun canAutomationPlaceItem(itemStack: ItemStack): Boolean {
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/IFilteredContainerSlot.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/IFilteredContainerSlot.kt
index 0eaa8ee8f..1a6c6f250 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/IFilteredContainerSlot.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/IFilteredContainerSlot.kt
@@ -1,6 +1,5 @@
 package ru.dbotthepony.mc.otm.container
 
-import net.minecraft.world.Container
 import net.minecraft.world.item.Item
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.item.Items
@@ -15,7 +14,6 @@ interface IFilteredContainerSlot : IContainerSlot {
 	val hasFilter: Boolean
 		get() = filter != null
 
-
 	fun testSlotFilter(itemStack: ItemStack): Boolean {
 		return testSlotFilter(itemStack.item)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
index 7ab61cebd..18109d9be 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/slotted/ContainerSlot.kt
@@ -83,7 +83,7 @@ open class ContainerSlot(
 			return ItemStack.EMPTY
 		}
 
-		if (item.count >= count) {
+		if (item.count <= count) {
 			this.item = ItemStack.EMPTY
 			return item
 		} else {
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/container/util/Iterators.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/container/util/Iterators.kt
index 64f4a9b73..9160415c9 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/container/util/Iterators.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/container/util/Iterators.kt
@@ -1,6 +1,7 @@
 package ru.dbotthepony.mc.otm.container.util
 
 import net.minecraft.world.Container
+import net.minecraft.world.inventory.Slot
 import net.minecraft.world.item.Item
 import net.minecraft.world.item.ItemStack
 import ru.dbotthepony.mc.otm.container.IContainerSlot
@@ -19,6 +20,25 @@ fun Container.containerSlot(slot: Int): IContainerSlot {
 	}
 }
 
+/**
+ * Returns [IContainerSlot] only if this container is [IEnhancedContainer]
+ */
+fun Container.containerSlotOrNull(slot: Int): IContainerSlot? {
+	if (this is IEnhancedContainer) {
+		return containerSlot(slot)
+	} else {
+		return null
+	}
+}
+
+fun Slot.containerSlot(): IContainerSlot {
+	return container.containerSlot(slotIndex)
+}
+
+fun Slot.containerSlotOrNull(): IContainerSlot? {
+	return container.containerSlotOrNull(slotIndex)
+}
+
 operator fun Container.iterator(): Iterator<ItemStack> {
 	if (this is IEnhancedContainer) {
 		return iterator()
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt
index 5c7ed9174..9bff7873a 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/MatteryMenu.kt
@@ -38,9 +38,11 @@ import ru.dbotthepony.mc.otm.capability.matteryPlayer
 import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorSlots
 import ru.dbotthepony.mc.otm.compat.curios.curiosSlots
 import ru.dbotthepony.mc.otm.compat.curios.isCurioSlot
+import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot
 import ru.dbotthepony.mc.otm.container.IMatteryContainer
 import ru.dbotthepony.mc.otm.container.computeSortedIndices
 import ru.dbotthepony.mc.otm.container.sortWithIndices
+import ru.dbotthepony.mc.otm.container.util.containerSlotOrNull
 import ru.dbotthepony.mc.otm.core.ResourceLocation
 import ru.dbotthepony.mc.otm.core.collect.ConditionalSet
 import ru.dbotthepony.mc.otm.core.math.Decimal
@@ -207,7 +209,7 @@ abstract class MatteryMenu(
 	var sortInventoryInput: SortInput? = null
 		private set
 
-	val playerSortSettings = IItemStackSortingSettings.inputs(this, player.matteryPlayer?.sortingSettings)
+	val playerSortSettings = IItemStackSortingSettings.inputs(this, player.matteryPlayer.sortingSettings)
 
 	var offhandSlot: InventorySlot? = null
 		protected set
@@ -221,7 +223,7 @@ abstract class MatteryMenu(
 	protected var inventorySlotIndexStart = 0
 	protected var inventorySlotIndexEnd = 0
 
-	open inner class InventorySlot(container: Container, index: Int, addFilter: Boolean = false) : UserFilteredSlot(container, index, 0, 0) {
+	open inner class InventorySlot(container: Container, index: Int) : UserFilteredSlot(container, index, 0, 0) {
 		override fun mayPlace(itemStack: ItemStack): Boolean {
 			return !isInventorySlotLocked(index) && super.mayPlace(itemStack)
 		}
@@ -236,15 +238,6 @@ abstract class MatteryMenu(
 		init {
 			val mattery = player.matteryPlayer
 
-			if (addFilter) {
-				val mContainer = container as IMatteryContainer
-
-				filter = Delegate.Of(
-					getter = { mContainer.getSlotFilter(slotIndex) },
-					setter = nullableItemInput(true) { mContainer.setSlotFilter(slotIndex, it) }::accept
-				)
-			}
-
 			if (mattery.hasExopack) {
 				chargeFlag = Delegate.Of(
 					getter = { slotIndex in mattery.slotsChargeFlag },
@@ -302,7 +295,7 @@ abstract class MatteryMenu(
 		for (i in 0 until if (mattery.hasExopack) mattery.combinedInventory.containerSize else mattery.wrappedItemInventory.containerSize) {
 			if (i in Inventory.INVENTORY_SIZE until player.inventory.containerSize) continue
 
-			val slot = InventorySlot(mattery.combinedInventory, i, true)
+			val slot = InventorySlot(mattery.combinedInventory, i)
 
 			_playerInventorySlots.add(slot)
 
@@ -392,21 +385,8 @@ abstract class MatteryMenu(
 		if (!seenSlots.add(pSlot))
 			return pSlot
 
-		if (pSlot is UserFilteredSlot && !pSlot.hasSetFilter) {
-			val container = pSlot.container
-
-			val input: PlayerInput<Item?>
-			val field: Delegate<Item?>
-
-			if (container is IMatteryContainer) {
-				input = PlayerInput(StreamCodecs.ITEM_TYPE_NULLABLE, handler = { container.setSlotFilter(pSlot.slotIndex, it) })
-				field = mSynchronizer.add(delegate = { container.getSlotFilter(pSlot.slotIndex) }, StreamCodecs.ITEM_TYPE_NULLABLE)
-			} else {
-				input = PlayerInput(StreamCodecs.ITEM_TYPE_NULLABLE, handler = { throw UnsupportedOperationException() })
-				field = mSynchronizer.add(delegate = { null }, StreamCodecs.ITEM_TYPE_NULLABLE)
-			}
-
-			pSlot.filter = Delegate.Of(getter = field::get, setter = input::accept)
+		if (pSlot is MatterySlot) {
+			pSlot.setupNetworkControls(this)
 		}
 
 		return super.addSlot(pSlot)
@@ -487,7 +467,7 @@ abstract class MatteryMenu(
 		val copy = slot.item.copy()
 		var any = false
 
-		if (target.any { it.any { it is UserFilteredSlot && it.filter != null } }) {
+		if (target.any { it.any { it.containerSlotOrNull() is IFilteredContainerSlot } }) {
 			for (collection in target) {
 				if (moveItemStackTo(slot, collection, onlyFiltered = true)) {
 					any = true
@@ -582,9 +562,9 @@ abstract class MatteryMenu(
 		// first pass - stack with existing slots
 		if (copy.isStackable) {
 			for (slot in slots) {
-				if (onlyFiltered && (slot !is UserFilteredSlot || !slot.test(item))) {
+				if (onlyFiltered && slot.containerSlotOrNull().let { it !is IFilteredContainerSlot || it.filter == null || !it.testSlotFilter(item) }) {
 					continue
-				} else if (!onlyFiltered && slot is UserFilteredSlot && !slot.test(item)) {
+				} else if (!onlyFiltered && slot.containerSlotOrNull().let { it is IFilteredContainerSlot && it.filter != null }) {
 					continue
 				}
 
@@ -609,9 +589,9 @@ abstract class MatteryMenu(
 
 		// second pass - drop stack into first free slot
 		for (slot in slots) {
-			if (onlyFiltered && (slot !is UserFilteredSlot || slot.filter == null || slot.filter!!.get() != item.item)) {
+			if (onlyFiltered && slot.containerSlotOrNull().let { it !is IFilteredContainerSlot || it.filter == null || !it.testSlotFilter(item) }) {
 				continue
-			} else if (!onlyFiltered && slot is UserFilteredSlot && slot.filter != null && slot.filter!!.get() != null && slot.filter!!.get() != item.item) {
+			} else if (!onlyFiltered && slot.containerSlotOrNull().let { it is IFilteredContainerSlot && it.filter != null }) {
 				continue
 			}
 
@@ -651,7 +631,7 @@ abstract class MatteryMenu(
 			val slot = slots[i]
 			slots.add(slot)
 
-			if (slot is InventorySlot && slot.filter != null) {
+			if (slot.containerSlotOrNull() is IFilteredContainerSlot) {
 				filters = true
 			}
 		}
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/Slots.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/Slots.kt
index bf6ee95a6..e436740f1 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/Slots.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/Slots.kt
@@ -17,20 +17,22 @@ import ru.dbotthepony.mc.otm.capability.MatteryCapability
 import ru.dbotthepony.mc.otm.capability.UpgradeType
 import ru.dbotthepony.mc.otm.capability.energy
 import ru.dbotthepony.mc.otm.client.minecraft
+import ru.dbotthepony.mc.otm.container.IFilteredContainerSlot
 import ru.dbotthepony.mc.otm.container.IMatteryContainer
 import ru.dbotthepony.mc.otm.container.ItemFilter
 import ru.dbotthepony.mc.otm.container.UpgradeContainer
+import ru.dbotthepony.mc.otm.container.util.containerSlotOrNull
 import ru.dbotthepony.mc.otm.core.collect.ConditionalEnumSet
 import ru.dbotthepony.mc.otm.core.immutableList
 import ru.dbotthepony.mc.otm.core.math.Decimal
 import ru.dbotthepony.mc.otm.menu.input.BooleanInputWithFeedback
 import ru.dbotthepony.mc.otm.menu.input.InstantBooleanInput
+import ru.dbotthepony.mc.otm.network.StreamCodecs
 import ru.dbotthepony.mc.otm.runOnClient
 import java.util.*
 import java.util.function.BooleanSupplier
 import java.util.function.DoubleSupplier
 import java.util.function.IntSupplier
-import java.util.function.Predicate
 import java.util.function.Supplier
 import kotlin.reflect.KMutableProperty0
 
@@ -51,6 +53,14 @@ inline fun <S : Slot> makeSlots(containers: List<Container>?, size: Int, initial
 open class MatterySlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : Slot(container, index, x, y) {
 	var ignoreSpectators = true
 
+	open fun setupNetworkControls(menu: MatteryMenu) {
+		val slot = containerSlotOrNull()
+
+		if (slot is IFilteredContainerSlot) {
+			menu.mSynchronizer.add(Delegate.Of(slot::filter), StreamCodecs.ITEM_TYPE_NULLABLE)
+		}
+	}
+
 	override fun setChanged() {
 		if (container is IMatteryContainer) {
 			(container as IMatteryContainer).setChanged(containerSlot)
@@ -68,7 +78,7 @@ open class MatterySlot(container: Container, index: Int, x: Int = 0, y: Int = 0)
 	}
 
 	open fun canTakeItemForPickAll(): Boolean {
-		return true
+		return (container.containerSlotOrNull(slotIndex) as? IFilteredContainerSlot)?.filter == null
 	}
 
 	override fun getMaxStackSize(): Int {
@@ -90,38 +100,29 @@ open class MatterySlot(container: Container, index: Int, x: Int = 0, y: Int = 0)
 			return super.getMaxStackSize(itemStack)
 		}
 	}
-}
 
-open class UserFilteredSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y), Predicate<ItemStack> {
-	var hasSetFilter = false
-		private set
-
-	var filter: Delegate<Item?>? = null
-		set(value) {
-			hasSetFilter = true
-			field = value
-		}
-
-	override fun canTakeItemForPickAll(): Boolean {
-		return filter?.get() == null
-	}
-
-	override fun test(t: ItemStack): Boolean {
-		return filter?.get() == null || filter?.get() == t.item
-	}
-
-	fun isSameFilter(other: Slot): Boolean {
-		if (other !is UserFilteredSlot)
-			return filter?.get() == null
-
-		return (
-			(other.filter == null && filter == null) ||
-			(other.filter != null && filter != null && other.filter!!.get() == filter!!.get())
-		)
+	private fun isSameFilter(other: Slot): Boolean {
+		val sSelf = containerSlotOrNull() as? IFilteredContainerSlot
+		val sOther = other.containerSlotOrNull() as? IFilteredContainerSlot
+		return sSelf?.filter == sOther?.filter
 	}
 
 	override fun isSameInventory(other: Slot): Boolean {
-		return isSameFilter(other) && super.isSameInventory(other)
+		return super.isSameInventory(other) && isSameFilter(other)
+	}
+}
+
+open class UserFilteredSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
+	var filterInput: MatteryMenu.PlayerInput<Item?>? = null
+		private set
+
+	override fun setupNetworkControls(menu: MatteryMenu) {
+		super.setupNetworkControls(menu)
+		val slot = containerSlotOrNull()
+
+		if (slot is IFilteredContainerSlot) {
+			filterInput = menu.PlayerInput(StreamCodecs.ITEM_TYPE_NULLABLE, handler = { slot.filter = it })
+		}
 	}
 }
 
diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/CargoCrateMenu.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/CargoCrateMenu.kt
index 2dc02ccf9..4d997dcf9 100644
--- a/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/CargoCrateMenu.kt
+++ b/src/main/kotlin/ru/dbotthepony/mc/otm/menu/decorative/CargoCrateMenu.kt
@@ -5,6 +5,7 @@ import net.minecraft.world.SimpleContainer
 import net.minecraft.world.entity.player.Inventory
 import net.minecraft.world.entity.player.Player
 import ru.dbotthepony.mc.otm.block.entity.decorative.CargoCrateBlockEntity
+import ru.dbotthepony.mc.otm.container.slotted.SlottedContainer
 import ru.dbotthepony.mc.otm.menu.MatteryMenu
 import ru.dbotthepony.mc.otm.menu.UserFilteredSlot
 import ru.dbotthepony.mc.otm.menu.makeSlots
@@ -15,7 +16,7 @@ class CargoCrateMenu(
 	inventory: Inventory,
 	tile: CargoCrateBlockEntity? = null
 ) : MatteryMenu(MMenus.CARGO_CRATE, containerId, inventory, tile) {
-	val actualContainer: Container = tile?.container ?: SimpleContainer(CargoCrateBlockEntity.CAPACITY)
+	val actualContainer = tile?.container ?: SlottedContainer.filtered(CargoCrateBlockEntity.CAPACITY)
 	val storageSlots = makeSlots(actualContainer, ::UserFilteredSlot)
 	private val trackedPlayerOpen = !inventory.player.isSpectator