diff --git a/build.gradle.kts b/build.gradle.kts index c4f760579..a0c3c0627 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -196,7 +196,7 @@ dependencies { modCompileOnly("maven.modrinth:litematica:$litematicaVersion") // DevLogin -// modRuntimeOnly("com.ptsmods:devlogin:3.5.1:fabric") + modRuntimeOnly("com.ptsmods:devlogin:3.5.1:fabric") // Test implementations testImplementation(kotlin("test")) diff --git a/gradle.properties b/gradle.properties index 08d727a15..abbaeef67 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ baritoneVersion=1.14.0 discordIPCVersion=6f6b6cce17 classGraphVersion=4.8.184 ktorVersion=3.3.3 -jacksonVersion=3.1.1 +jacksonVersion=3.1.4 mockkVersion=1.14.7 spairVersion=1.90.0 diff --git a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 768ece4c2..d20a73c7a 100644 --- a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -21,7 +21,7 @@ import com.lambda.event.events.*; import com.lambda.gui.DearImGui; import com.lambda.gui.components.ClickGuiLayout; -import com.lambda.interaction.handlers.TimerHandler; +import com.lambda.interaction.handler.handlers.TimerHandler; import com.lambda.module.modules.movement.BetterFirework; import com.lambda.module.modules.player.Interact; import com.lambda.module.modules.player.InventoryMove; @@ -141,7 +141,8 @@ private void onScreenOpen(@Nullable Screen screen, CallbackInfo ci) { } } - @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;removed()V", shift = At.Shift.AFTER)) + @SuppressWarnings("DiscouragedShift") + @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;removed()V", shift = At.Shift.BEFORE)) private void onScreenRemove(@Nullable Screen screen, CallbackInfo ci) { if (currentScreen == null) return; if (currentScreen instanceof ScreenHandlerProvider handledScreen) { diff --git a/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java b/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java index dbdb9a9d2..5630fb0b2 100644 --- a/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java +++ b/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java @@ -20,9 +20,10 @@ import com.lambda.event.EventFlow; import com.lambda.event.events.InventoryEvent; import com.lambda.event.events.PlayerEvent; -import com.lambda.interaction.handlers.BaritoneHandler; -import com.lambda.interaction.managers.inventory.InventoryManager; -import com.lambda.interaction.managers.rotating.RotationManager; +import com.lambda.interaction.handler.handlers.BaritoneHandler; +import com.lambda.interaction.manager.managers.inventory.InventoryManager; +import com.lambda.interaction.manager.managers.rotating.RotationManager; +import com.lambda.module.modules.client.Client; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; @@ -111,6 +112,17 @@ public void clickSlotHead(int syncId, int slotId, int button, SlotActionType act if (EventFlow.post(click).isCanceled()) ci.cancel(); } + @Inject(method = "clickSlot", at = @At("TAIL")) + public void clickSlotTail(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player, CallbackInfo ci) { + if (syncId != player.currentScreenHandler.syncId) return; + EventFlow.post(new PlayerEvent.SlotClick.Post(syncId, slotId, button, actionType, player.currentScreenHandler)); + } + + @ModifyExpressionValue(method = "clickSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/screen/ScreenHandler;getRevision()I")) + private int modifyClickSlotRevision(int originalRevision) { + return Client.INSTANCE.getAvoidInventoryDesync() ? -1 : originalRevision; + } + /** * Posts {@link InventoryEvent.HotbarSlot.Update} and returns the event value as the selected slot *
{@code
@@ -144,7 +156,7 @@ private void cancelBlockBreakingPre(CallbackInfo ci) {
     @WrapMethod(method = "createPlayer(Lnet/minecraft/client/world/ClientWorld;Lnet/minecraft/stat/StatHandler;Lnet/minecraft/client/recipebook/ClientRecipeBook;)Lnet/minecraft/client/network/ClientPlayerEntity;")
     private ClientPlayerEntity wrapCreatePlayer(ClientWorld world, StatHandler statHandler, ClientRecipeBook recipeBook, Operation original) {
         var player = original.call(world, statHandler, recipeBook);
-        InventoryManager.INSTANCE.setScreenHandler(player.playerScreenHandler);
+        InventoryManager.onSetScreenHandler(player.playerScreenHandler);
         return player;
     }
 }
diff --git a/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java b/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java
index c8bd52b47..136c9d2d4 100644
--- a/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java
@@ -19,8 +19,8 @@
 
 import com.lambda.event.EventFlow;
 import com.lambda.event.events.*;
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.movement.NoJumpCooldown;
 import com.lambda.module.modules.movement.elytrafly.ElytraFly;
 import com.lambda.module.modules.player.PortalGui;
@@ -56,7 +56,7 @@
 import java.util.Objects;
 
 import static com.lambda.Lambda.getMc;
-import static com.lambda.interaction.managers.rotating.Rotation.dist;
+import static com.lambda.interaction.manager.managers.rotating.Rotation.dist;
 
 @Mixin(value = ClientPlayerEntity.class, priority = Integer.MAX_VALUE)
 public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity {
diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java
index 2c31d2ff8..99fb142f6 100644
--- a/src/main/java/com/lambda/mixin/entity/EntityMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/EntityMixin.java
@@ -20,8 +20,8 @@
 import com.lambda.event.EventFlow;
 import com.lambda.event.events.EntityEvent;
 import com.lambda.event.events.PlayerEvent;
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.movement.elytrafly.ElytraFly;
 import com.lambda.module.modules.render.NoRender;
 import com.lambda.util.math.Vec2d;
diff --git a/src/main/java/com/lambda/mixin/entity/HandledScreensMixin.java b/src/main/java/com/lambda/mixin/entity/HandledScreensMixin.java
index 079ea9411..106e0bbb0 100644
--- a/src/main/java/com/lambda/mixin/entity/HandledScreensMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/HandledScreensMixin.java
@@ -17,7 +17,7 @@
 
 package com.lambda.mixin.entity;
 
-import com.lambda.interaction.managers.inventory.InventoryManager;
+import com.lambda.interaction.manager.managers.inventory.InventoryManager;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.ingame.HandledScreens;
 import net.minecraft.screen.ScreenHandler;
diff --git a/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java b/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java
index 1f3caf618..9af7e6302 100644
--- a/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java
@@ -20,9 +20,9 @@
 import com.lambda.Lambda;
 import com.lambda.event.EventFlow;
 import com.lambda.event.events.MovementEvent;
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.handlers.GlideHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.handler.handlers.GlideHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.movement.Velocity;
 import com.lambda.module.modules.movement.elytrafly.ElytraFly;
 import com.lambda.module.modules.render.ViewModel;
@@ -49,7 +49,6 @@
 
 @Mixin(LivingEntity.class)
 public abstract class LivingEntityMixin extends EntityMixin {
-
     @Unique private final LivingEntity lambda$instance = (LivingEntity) (Object) this;
 
     @Definition(id = "getJumpVelocity", method = "Lnet/minecraft/entity/LivingEntity;getJumpVelocity()F")
diff --git a/src/main/java/com/lambda/mixin/entity/PlayerEntityMixin.java b/src/main/java/com/lambda/mixin/entity/PlayerEntityMixin.java
index 8a0fc6d0d..c7722b2bc 100644
--- a/src/main/java/com/lambda/mixin/entity/PlayerEntityMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/PlayerEntityMixin.java
@@ -20,9 +20,9 @@
 import com.lambda.Lambda;
 import com.lambda.event.EventFlow;
 import com.lambda.event.events.MovementEvent;
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.handlers.GlideHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.handler.handlers.GlideHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.player.Reach;
 import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
 import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
diff --git a/src/main/java/com/lambda/mixin/entity/PlayerInventoryMixin.java b/src/main/java/com/lambda/mixin/entity/PlayerInventoryMixin.java
index 26c4c0a91..e538912bd 100644
--- a/src/main/java/com/lambda/mixin/entity/PlayerInventoryMixin.java
+++ b/src/main/java/com/lambda/mixin/entity/PlayerInventoryMixin.java
@@ -18,7 +18,7 @@
 package com.lambda.mixin.entity;
 
 import com.lambda.Lambda;
-import com.lambda.interaction.managers.hotbar.HotbarManager;
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import net.minecraft.entity.player.PlayerInventory;
 import org.objectweb.asm.Opcodes;
diff --git a/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
index 9f83518dd..a85af677e 100644
--- a/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
@@ -23,8 +23,8 @@
 import com.lambda.event.events.InventoryEvent;
 import com.lambda.event.events.PlayerEvent;
 import com.lambda.event.events.WorldEvent;
-import com.lambda.interaction.managers.inventory.InventoryManager;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.manager.managers.inventory.InventoryManager;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.movement.Velocity;
 import com.lambda.module.modules.render.NoRender;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
@@ -134,6 +134,11 @@ private void wrapOnInventory(InventoryS2CPacket packet, Operation original
         InventoryManager.onInventoryUpdate(packet, original);
     }
 
+    @WrapMethod(method = "onSetCursorItem")
+    private void wrapOnSetCursorItem(SetCursorItemS2CPacket packet, Operation original) {
+        InventoryManager.onCursorUpdate(packet, original);
+    }
+
     @WrapMethod(method = "sendChatMessage(Ljava/lang/String;)V")
     void onSendMessage(String content, Operation original) {
         var event = new ChatEvent.Send(content);
diff --git a/src/main/java/com/lambda/mixin/render/CameraMixin.java b/src/main/java/com/lambda/mixin/render/CameraMixin.java
index 38f629489..e6ca7231f 100644
--- a/src/main/java/com/lambda/mixin/render/CameraMixin.java
+++ b/src/main/java/com/lambda/mixin/render/CameraMixin.java
@@ -17,7 +17,7 @@
 
 package com.lambda.mixin.render;
 
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.render.CameraTweaks;
 import com.lambda.module.modules.render.FreeLook;
 import com.lambda.module.modules.render.Freecam;
diff --git a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
index dc18dc35a..86ffe2d5e 100644
--- a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
@@ -18,8 +18,8 @@
 package com.lambda.mixin.render;
 
 import com.lambda.Lambda;
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.lambda.module.modules.render.Nametags;
 import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
 import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
diff --git a/src/main/java/com/lambda/mixin/render/PlayerListHudMixin.java b/src/main/java/com/lambda/mixin/render/PlayerListHudMixin.java
index eefaba6f1..f6e090956 100644
--- a/src/main/java/com/lambda/mixin/render/PlayerListHudMixin.java
+++ b/src/main/java/com/lambda/mixin/render/PlayerListHudMixin.java
@@ -17,7 +17,7 @@
 
 package com.lambda.mixin.render;
 
-import com.lambda.interaction.handlers.FriendHandler;
+import com.lambda.interaction.handler.handlers.FriendHandler;
 import com.lambda.module.modules.render.ExtraTab;
 import com.lambda.util.text.TextBuilder;
 import com.lambda.util.text.TextDslKt;
diff --git a/src/main/java/com/lambda/mixin/world/DirectionMixin.java b/src/main/java/com/lambda/mixin/world/DirectionMixin.java
index 765248bb3..da8698f2e 100644
--- a/src/main/java/com/lambda/mixin/world/DirectionMixin.java
+++ b/src/main/java/com/lambda/mixin/world/DirectionMixin.java
@@ -17,8 +17,8 @@
 
 package com.lambda.mixin.world;
 
-import com.lambda.interaction.handlers.BaritoneHandler;
-import com.lambda.interaction.managers.rotating.RotationManager;
+import com.lambda.interaction.handler.handlers.BaritoneHandler;
+import com.lambda.interaction.manager.managers.rotating.RotationManager;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import net.minecraft.entity.Entity;
 import net.minecraft.util.math.Direction;
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/BlockArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/BlockArguments.kt
index bafbb787a..9c0034f3f 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/BlockArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/BlockArguments.kt
@@ -37,9 +37,8 @@ import java.util.function.Predicate
  * @see BlockPredicateArgumentType.getBlockPredicate
  */
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Predicate {
-    return BlockPredicateArgumentType.getBlockPredicate(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): Predicate =
+    BlockPredicateArgumentType.getBlockPredicate(context.assumeSourceNotUsed(), name)
 
 /**
  * Reads the [BlockStateArgument] value of the argument in
@@ -48,9 +47,8 @@ fun DefaultArgumentReader.value(): Predicate.value(): BlockStateArgument {
-    return BlockStateArgumentType.getBlockState(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): BlockStateArgument =
+    BlockStateArgumentType.getBlockState(context.assumeSourceNotUsed(), name)
 
 /**
  * Creates a block predicate argument with [name] as the parameter name.
@@ -62,9 +60,8 @@ fun DefaultArgumentReader.value(): BlockStateArgument {
 fun  blockPredicate(
     name: String,
     registryAccess: CommandRegistryAccess,
-): DefaultArgumentConstructor {
-    return argument(name, BlockPredicateArgumentType.blockPredicate(registryAccess))
-}
+): DefaultArgumentConstructor =
+    argument(name, BlockPredicateArgumentType.blockPredicate(registryAccess))
 
 /**
  * Creates a block state argument with [name] as the parameter name.
@@ -76,6 +73,5 @@ fun  blockPredicate(
 fun  blockState(
     name: String,
     registryAccess: CommandRegistryAccess,
-): DefaultArgumentConstructor {
-    return argument(name, BlockStateArgumentType.blockState(registryAccess))
-}
+): DefaultArgumentConstructor =
+    argument(name, BlockStateArgumentType.blockState(registryAccess))
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/GeometricArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/GeometricArguments.kt
index 6c2117456..065bb8e0c 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/GeometricArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/GeometricArguments.kt
@@ -167,9 +167,8 @@ fun  rotation(name: String): DefaultArgumentConstructor swizzle(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, SwizzleArgumentType.swizzle())
-}
+): DefaultArgumentConstructor =
+    argument(name, SwizzleArgumentType.swizzle())
 
 /**
  * Creates a block pos argument with [name] as the parameter name.
@@ -177,9 +176,8 @@ fun  swizzle(
 @BrigadierDsl
 fun  blockPos(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, BlockPosArgumentType.blockPos())
-}
+): DefaultArgumentConstructor =
+    argument(name, BlockPosArgumentType.blockPos())
 
 /**
  * Creates a column pos argument with [name] as the parameter name.
@@ -187,9 +185,8 @@ fun  blockPos(
 @BrigadierDsl
 fun  columnPos(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, ColumnPosArgumentType.columnPos())
-}
+): DefaultArgumentConstructor =
+    argument(name, ColumnPosArgumentType.columnPos())
 
 /**
  * Creates a vec2 argument with [name] as the parameter name.
@@ -200,9 +197,8 @@ fun  columnPos(
 fun  vec2(
     name: String,
     centerIntegers: Boolean = false,
-): DefaultArgumentConstructor {
-    return argument(name, Vec2ArgumentType.vec2(centerIntegers))
-}
+): DefaultArgumentConstructor =
+    argument(name, Vec2ArgumentType.vec2(centerIntegers))
 
 /**
  * Creates a vec3 argument with [name] as the parameter name.
@@ -213,6 +209,5 @@ fun  vec2(
 fun  vec3(
     name: String,
     centerIntegers: Boolean = false,
-): DefaultArgumentConstructor {
-    return argument(name, Vec3ArgumentType.vec3(centerIntegers))
-}
+): DefaultArgumentConstructor =
+    argument(name, Vec3ArgumentType.vec3(centerIntegers))
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/IdentifierArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/IdentifierArguments.kt
index c88a39cb3..d2a3b0218 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/IdentifierArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/IdentifierArguments.kt
@@ -27,8 +27,6 @@ import com.lambda.brigadier.assumeSourceNotUsed
 import net.minecraft.advancement.AdvancementEntry
 import net.minecraft.command.argument.IdentifierArgumentType
 import net.minecraft.command.argument.RegistryKeyArgumentType
-import net.minecraft.loot.condition.LootCondition
-import net.minecraft.loot.function.LootFunction
 import net.minecraft.recipe.RecipeEntry
 import net.minecraft.server.command.ServerCommandSource
 import net.minecraft.util.Identifier
@@ -41,9 +39,8 @@ import net.minecraft.util.Identifier
  */
 @JvmName("valueIdentifierArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Identifier {
-    return IdentifierArgumentType.getIdentifier(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): Identifier =
+    IdentifierArgumentType.getIdentifier(context.assumeSourceNotUsed(), name)
 
 /**
  * Reads the [Identifier] value from the
@@ -53,22 +50,16 @@ fun DefaultArgumentReader.value(): Identifier {
  * @see RegistryKeyArgumentType.getAdvancementEntry
  */
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        DefaultArgumentDescriptor<
-                IdentifierArgumentType
-                >
-        >.asAdvancement(): AdvancementEntry {
-    return RegistryKeyArgumentType.getAdvancementEntry(context, name)
-}
+fun ArgumentReader>.asAdvancement(): AdvancementEntry =
+    RegistryKeyArgumentType.getAdvancementEntry(context, name)
 
-/**
- * Reads the [Identifier] value from the
- * argument in the receiver [ArgumentReader]
- * as a [LootCondition].
- *
- * @see IdentifierArgumentType.getPredicateArgument
- */
+///**
+// * Reads the [Identifier] value from the
+// * argument in the receiver [ArgumentReader]
+// * as a [LootCondition].
+// *
+// * @see IdentifierArgumentType.getPredicateArgument
+// */
 //@BrigadierDsl
 //fun ArgumentReader<
 //        ServerCommandSource,
@@ -79,13 +70,13 @@ fun ArgumentReader<
 //    return IdentifierArgumentType.getPredicateArgument(context, name)
 //}
 
-/**
- * Reads the [Identifier] value from the
- * argument in the receiver [ArgumentReader]
- * as a [LootFunction].
- *
- * @see IdentifierArgumentType.getItemModifierArgument
- */
+///**
+// * Reads the [Identifier] value from the
+// * argument in the receiver [ArgumentReader]
+// * as a [LootFunction].
+// *
+// * @see IdentifierArgumentType.getItemModifierArgument
+// */
 //@BrigadierDsl
 //fun ArgumentReader<
 //        ServerCommandSource,
@@ -104,14 +95,8 @@ fun ArgumentReader<
  * @see IdentifierArgumentType.getRecipeArgument
  */
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        DefaultArgumentDescriptor<
-                IdentifierArgumentType
-                >
-        >.asRecipe(): RecipeEntry<*> {
-    return RegistryKeyArgumentType.getRecipeEntry(context, name)
-}
+fun ArgumentReader>.asRecipe(): RecipeEntry<*> =
+    RegistryKeyArgumentType.getRecipeEntry(context, name)
 
 /**
  * Creates an identifier argument with [name] as the parameter name.
@@ -119,6 +104,5 @@ fun ArgumentReader<
 @BrigadierDsl
 fun  identifier(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, IdentifierArgumentType.identifier())
-}
+): DefaultArgumentConstructor =
+    argument(name, IdentifierArgumentType.identifier())
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt
index 846cdb2e3..ecaa559ef 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt
@@ -46,9 +46,8 @@ import java.util.function.Predicate
  */
 @JvmName("valueItemPredicateArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Predicate {
-    return ItemPredicateArgumentType.getItemStackPredicate(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): Predicate =
+    ItemPredicateArgumentType.getItemStackPredicate(context.assumeSourceNotUsed(), name)
 
 /**
  * Reads the integer value from the
@@ -58,9 +57,8 @@ fun DefaultArgumentReader.value(): Predicate.value(): Int {
-    return ItemSlotArgumentType.getItemSlot(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): Int =
+    ItemSlotArgumentType.getItemSlot(context.assumeSourceNotUsed(), name)
 
 /**
  * Reads the [ItemStackArgument] value from the
@@ -70,9 +68,8 @@ fun DefaultArgumentReader.value(): Int {
  */
 @JvmName("valueItemStackArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): ItemStackArgument {
-    return ItemStackArgumentType.getItemStackArgument(context, name)
-}
+fun DefaultArgumentReader.value(): ItemStackArgument =
+    ItemStackArgumentType.getItemStackArgument(context, name)
 
 /**
  * Creates an item predicate argument with [name] as the parameter name.
@@ -83,9 +80,8 @@ fun DefaultArgumentReader.value(): ItemStackArgument {
 fun  itemPredicate(
     name: String,
     context: CommandRegistryAccess,
-): DefaultArgumentConstructor {
-    return argument(name, ItemPredicateArgumentType.itemPredicate(context))
-}
+): DefaultArgumentConstructor =
+    argument(name, ItemPredicateArgumentType.itemPredicate(context))
 
 /**
  * Creates an item slot argument with [name] as the parameter name.
@@ -93,9 +89,8 @@ fun  itemPredicate(
 @BrigadierDsl
 fun  itemSlot(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, ItemSlotArgumentType.itemSlot())
-}
+): DefaultArgumentConstructor =
+    argument(name, ItemSlotArgumentType.itemSlot())
 
 /**
  * Creates an item stack argument with [name] as the parameter name.
@@ -106,6 +101,5 @@ fun  itemSlot(
 fun  itemStack(
     name: String,
     context: CommandRegistryAccess,
-): DefaultArgumentConstructor {
-    return argument(name, ItemStackArgumentType.itemStack(context))
-}
+): DefaultArgumentConstructor =
+    argument(name, ItemStackArgumentType.itemStack(context))
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/MiscArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/MiscArguments.kt
index 3deb92a56..7a2c1f498 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/MiscArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/MiscArguments.kt
@@ -48,9 +48,8 @@ object LiteralDescriptor : ArgumentDescriptor>
  */
 @JvmName("valueTimeArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Int {
-    return IntegerArgumentType.getInteger(context, name)
-} // TimeArgumentType does not provide an accessor, defaulting to int
+fun DefaultArgumentReader.value(): Int =
+    IntegerArgumentType.getInteger(context, name) // TimeArgumentType does not provide an accessor, defaulting to int
 
 /**
  * Reads the [UUID] value from the
@@ -60,9 +59,8 @@ fun DefaultArgumentReader.value(): Int {
  */
 @JvmName("valueUuidArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): UUID {
-    return UuidArgumentType.getUuid(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): UUID =
+    UuidArgumentType.getUuid(context.assumeSourceNotUsed(), name)
 
 /**
  * Creates a time argument with [name] as the parameter name.
@@ -73,9 +71,8 @@ fun DefaultArgumentReader.value(): UUID {
 fun  time(
     name: String,
     minimumTicks: Int = 0,
-): DefaultArgumentConstructor {
-    return argument(name, TimeArgumentType.time(minimumTicks))
-}
+): DefaultArgumentConstructor =
+    argument(name, TimeArgumentType.time(minimumTicks))
 
 /**
  * Creates a UUID argument with [name] as the parameter name.
@@ -83,9 +80,8 @@ fun  time(
 @BrigadierDsl
 fun  uuid(
     name: String,
-): RequiredArgumentConstructor> {
-    return argument(name, UuidArgumentType.uuid())
-}
+): RequiredArgumentConstructor> =
+    argument(name, UuidArgumentType.uuid())
 
 /**
  * Creates a literal argument with [name] as the literal.
@@ -98,6 +94,5 @@ fun  uuid(
 @BrigadierDsl
 fun  literal(
     name: String,
-): ArgumentConstructor, LiteralDescriptor> {
-    return ArgumentConstructor(LiteralArgumentBuilder.literal(name), name, LiteralDescriptor)
-}
+): ArgumentConstructor, LiteralDescriptor> =
+    ArgumentConstructor(LiteralArgumentBuilder.literal(name), name, LiteralDescriptor)
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/NBTArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/NBTArguments.kt
index 08122cc82..41a7c13ac 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/NBTArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/NBTArguments.kt
@@ -45,9 +45,8 @@ import net.minecraft.nbt.NbtElement
  */
 @JvmName("valueNbtCompoundArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): NbtCompound {
-    return NbtCompoundArgumentType.getNbtCompound(context, name)
-}
+fun DefaultArgumentReader.value(): NbtCompound =
+    NbtCompoundArgumentType.getNbtCompound(context, name)
 
 /**
  * Reads the [NbtElement] value from the
@@ -57,9 +56,8 @@ fun DefaultArgumentReader.value(): NbtCompound {
  */
 @JvmName("valueNbtElementArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): NbtElement {
-    return NbtElementArgumentType.getNbtElement(context, name)
-}
+fun DefaultArgumentReader.value(): NbtElement =
+    NbtElementArgumentType.getNbtElement(context, name)
 
 /**
  * Reads the [NbtPath] value from the
@@ -69,9 +67,8 @@ fun DefaultArgumentReader.value(): NbtElement {
  */
 @JvmName("valueNbtPathArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): NbtPath {
-    return NbtPathArgumentType.getNbtPath(context.assumeSourceNotUsed(), name)
-}
+fun DefaultArgumentReader.value(): NbtPath =
+    NbtPathArgumentType.getNbtPath(context.assumeSourceNotUsed(), name)
 
 /**
  * Creates a nbt compound argument with [name] as the parameter name.
@@ -79,9 +76,8 @@ fun DefaultArgumentReader.value(): NbtPath {
 @BrigadierDsl
 fun  nbtCompound(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, NbtCompoundArgumentType.nbtCompound())
-}
+): DefaultArgumentConstructor =
+    argument(name, NbtCompoundArgumentType.nbtCompound())
 
 /**
  * Creates an NBT element argument with [name] as the parameter name.
@@ -89,9 +85,8 @@ fun  nbtCompound(
 @BrigadierDsl
 fun  nbtElement(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, NbtElementArgumentType.nbtElement())
-}
+): DefaultArgumentConstructor =
+    argument(name, NbtElementArgumentType.nbtElement())
 
 /**
  * Creates an NBT path argument with [name] as the parameter name.
@@ -99,6 +94,5 @@ fun  nbtElement(
 @BrigadierDsl
 fun  nbtPath(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, NbtPathArgumentType.nbtPath())
-}
+): DefaultArgumentConstructor =
+    argument(name, NbtPathArgumentType.nbtPath())
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/PlayerArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/PlayerArguments.kt
index 102021f26..36169393c 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/PlayerArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/PlayerArguments.kt
@@ -65,14 +65,8 @@ object ListPlayerArgumentDescriptor : ArgumentDescriptor
  */
 @JvmName("valueGameProfileArg")
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        DefaultArgumentDescriptor<
-                GameProfileArgumentType
-                >
-        >.value(): Collection {
-    return GameProfileArgumentType.getProfileArgument(context, name)
-}
+fun ArgumentReader>.value(): Collection =
+    GameProfileArgumentType.getProfileArgument(context, name)
 
 /**
  * Reads the [Team] value from the
@@ -82,14 +76,8 @@ fun ArgumentReader<
  */
 @JvmName("valueTeamArg")
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        DefaultArgumentDescriptor<
-                TeamArgumentType
-                >
-        >.value(): Team {
-    return TeamArgumentType.getTeam(context, name)
-}
+fun ArgumentReader>.value(): Team =
+    TeamArgumentType.getTeam(context, name)
 
 /**
  * Reads the [ServerPlayerEntity] value from the
@@ -99,12 +87,8 @@ fun ArgumentReader<
  */
 @JvmName("valuePlayerArg")
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        SinglePlayerArgumentDescriptor
-        >.value(): ServerPlayerEntity {
-    return EntityArgumentType.getPlayer(context, name)
-}
+fun ArgumentReader.value(): ServerPlayerEntity =
+    EntityArgumentType.getPlayer(context, name)
 
 /**
  * Reads the collection of players from the argument in
@@ -116,12 +100,8 @@ fun ArgumentReader<
  */
 @JvmName("requiredPlayerArg")
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        ListPlayerArgumentDescriptor
-        >.required(): Collection {
-    return EntityArgumentType.getPlayers(context, name)
-}
+fun ArgumentReader.required(): Collection =
+    EntityArgumentType.getPlayers(context, name)
 
 /**
  * Reads the collection of players from the argument in
@@ -133,12 +113,8 @@ fun ArgumentReader<
  */
 @JvmName("optionalPlayerArg")
 @BrigadierDsl
-fun ArgumentReader<
-        ServerCommandSource,
-        ListPlayerArgumentDescriptor
-        >.optional(): Collection {
-    return EntityArgumentType.getOptionalPlayers(context, name)
-}
+fun ArgumentReader.optional(): Collection =
+    EntityArgumentType.getOptionalPlayers(context, name)
 
 /**
  * Creates a game profile argument with [name] as the parameter name.
@@ -146,9 +122,8 @@ fun ArgumentReader<
 @BrigadierDsl
 fun  gameProfile(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, GameProfileArgumentType.gameProfile())
-}
+): DefaultArgumentConstructor =
+    argument(name, GameProfileArgumentType.gameProfile())
 
 /**
  * Creates a team argument with [name] as the parameter name.
@@ -156,9 +131,8 @@ fun  gameProfile(
 @BrigadierDsl
 fun  team(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, TeamArgumentType.team())
-}
+): DefaultArgumentConstructor =
+    argument(name, TeamArgumentType.team())
 
 /**
  * Creates a player selector argument with [name] as the parameter name.
@@ -166,12 +140,8 @@ fun  team(
 @BrigadierDsl
 fun  player(
     name: String,
-): RequiredArgumentConstructor<
-        S,
-        SinglePlayerArgumentDescriptor
-        > {
-    return argument(name, EntityArgumentType.player(), SinglePlayerArgumentDescriptor)
-}
+): RequiredArgumentConstructor =
+    argument(name, EntityArgumentType.player(), SinglePlayerArgumentDescriptor)
 
 /**
  * Creates a multiple player selector argument with [name] as the parameter name.
@@ -179,9 +149,5 @@ fun  player(
 @BrigadierDsl
 fun  players(
     name: String,
-): RequiredArgumentConstructor<
-        S,
-        ListPlayerArgumentDescriptor
-        > {
-    return argument(name, EntityArgumentType.players(), ListPlayerArgumentDescriptor)
-}
+): RequiredArgumentConstructor =
+    argument(name, EntityArgumentType.players(), ListPlayerArgumentDescriptor)
diff --git a/src/main/kotlin/com/lambda/brigadier/argument/PrimitiveArguments.kt b/src/main/kotlin/com/lambda/brigadier/argument/PrimitiveArguments.kt
index 6544a31f3..258676e5c 100644
--- a/src/main/kotlin/com/lambda/brigadier/argument/PrimitiveArguments.kt
+++ b/src/main/kotlin/com/lambda/brigadier/argument/PrimitiveArguments.kt
@@ -42,9 +42,8 @@ import com.mojang.brigadier.arguments.LongArgumentType
  */
 @JvmName("valueBoolArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Boolean {
-    return BoolArgumentType.getBool(context, name)
-}
+fun DefaultArgumentReader.value(): Boolean =
+    BoolArgumentType.getBool(context, name)
 
 /**
  * Reads the boolean value from the argument in
@@ -54,9 +53,8 @@ fun DefaultArgumentReader.value(): Boolean {
  */
 @JvmName("valueDoubleArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Double {
-    return DoubleArgumentType.getDouble(context, name)
-}
+fun DefaultArgumentReader.value(): Double =
+    DoubleArgumentType.getDouble(context, name)
 
 /**
  * Reads the float value from the argument in
@@ -66,9 +64,8 @@ fun DefaultArgumentReader.value(): Double {
  */
 @JvmName("valueFloatArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Float {
-    return FloatArgumentType.getFloat(context, name)
-}
+fun DefaultArgumentReader.value(): Float =
+    FloatArgumentType.getFloat(context, name)
 
 /**
  * Reads the integer value from the argument in
@@ -78,9 +75,8 @@ fun DefaultArgumentReader.value(): Float {
  */
 @JvmName("valueIntArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Int {
-    return IntegerArgumentType.getInteger(context, name)
-}
+fun DefaultArgumentReader.value(): Int =
+    IntegerArgumentType.getInteger(context, name)
 
 /**
  * Reads the long value from the argument in
@@ -90,9 +86,8 @@ fun DefaultArgumentReader.value(): Int {
  */
 @JvmName("valueLongArg")
 @BrigadierDsl
-fun DefaultArgumentReader.value(): Long {
-    return LongArgumentType.getLong(context, name)
-}
+fun DefaultArgumentReader.value(): Long =
+    LongArgumentType.getLong(context, name)
 
 /**
  * Creates a boolean argument with [name] as the parameter name.
@@ -100,9 +95,8 @@ fun DefaultArgumentReader.value(): Long {
 @BrigadierDsl
 fun  boolean(
     name: String,
-): DefaultArgumentConstructor {
-    return argument(name, BoolArgumentType.bool())
-}
+): DefaultArgumentConstructor =
+    argument(name, BoolArgumentType.bool())
 
 /**
  * Creates a double argument with [name] as the parameter name.
@@ -115,9 +109,8 @@ fun  double(
     name: String,
     min: Double = -Double.MAX_VALUE,
     max: Double = Double.MAX_VALUE,
-): DefaultArgumentConstructor {
-    return argument(name, DoubleArgumentType.doubleArg(min, max))
-}
+): DefaultArgumentConstructor =
+    argument(name, DoubleArgumentType.doubleArg(min, max))
 
 /**
  * Creates a float argument with [name] as the parameter name.
@@ -130,9 +123,8 @@ fun  float(
     name: String,
     min: Float = -Float.MAX_VALUE,
     max: Float = Float.MAX_VALUE,
-): DefaultArgumentConstructor {
-    return argument(name, FloatArgumentType.floatArg(min, max))
-}
+): DefaultArgumentConstructor =
+    argument(name, FloatArgumentType.floatArg(min, max))
 
 /**
  * Creates an integer argument with [name] as the parameter name.
@@ -145,9 +137,8 @@ fun  integer(
     name: String,
     min: Int = -Int.MAX_VALUE,
     max: Int = Int.MAX_VALUE,
-): DefaultArgumentConstructor {
-    return argument(name, IntegerArgumentType.integer(min, max))
-}
+): DefaultArgumentConstructor =
+    argument(name, IntegerArgumentType.integer(min, max))
 
 /**
  * Creates a long argument with [name] as the parameter name.
@@ -160,6 +151,5 @@ fun  long(
     name: String,
     min: Long = -Long.MAX_VALUE,
     max: Long = Long.MAX_VALUE,
-): DefaultArgumentConstructor {
-    return argument(name, LongArgumentType.longArg(min, max))
-}
+): DefaultArgumentConstructor =
+    argument(name, LongArgumentType.longArg(min, max))
diff --git a/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt b/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt
index 51e93f749..ccd2212ab 100644
--- a/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt
+++ b/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt
@@ -28,10 +28,10 @@ import com.lambda.command.LambdaCommand
 import com.lambda.config.automation.AutomationConfig
 import com.lambda.interaction.construction.StructureRegistry
 import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
-import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
-import com.lambda.task.RootTask.run
+import com.lambda.interaction.construction.blueprint.toBlueprint
+import com.lambda.task.start
 import com.lambda.task.tasks.BuildTask
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.tasks.build
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.extension.CommandBuilder
@@ -62,12 +62,12 @@ object BuildCommand : LambdaCommand(
                                 .loadStructureByRelativePath(Path.of(pathString))
                                 .let { template ->
                                     info("Building structure $pathString with dimensions ${template.size.toShortString()} created by ${template.author}")
-                                    lastBuildTask = with(AutomationConfig.Companion.DEFAULT) {
+                                    lastBuildTask = with(AutomationConfig.DEFAULT) {
                                         template.toStructure()
                                             .move(player.blockPos)
                                             .toBlueprint()
                                             .build()
-                                            .run()
+                                            .start()
                                     }
 
                                     return@executeWithResult success()
diff --git a/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt b/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt
index 87f4b8454..9b9441535 100644
--- a/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt
+++ b/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt
@@ -32,7 +32,7 @@ import com.lambda.brigadier.required
 import com.lambda.command.CommandRegistry.prefix
 import com.lambda.command.LambdaCommand
 import com.lambda.config.categories.FriendCategory
-import com.lambda.interaction.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler
 import com.lambda.interaction.handlers.FriendImporter
 import com.lambda.network.mojang.getProfile
 import com.lambda.threading.runIO
diff --git a/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt b/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt
index 33adbaee7..b3d9392ee 100644
--- a/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt
+++ b/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt
@@ -28,12 +28,13 @@ import com.lambda.brigadier.executeWithResult
 import com.lambda.brigadier.required
 import com.lambda.command.LambdaCommand
 import com.lambda.config.automation.AutomationConfig
-import com.lambda.interaction.handlers.ContainerHandler
-import com.lambda.interaction.handlers.ContainerHandler.findContainersWithMaterial
-import com.lambda.interaction.handlers.ContainerHandler.findContainersWithSpace
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
-import com.lambda.task.RootTask
+import com.lambda.interaction.container.selection.containerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findContainers
 import com.lambda.task.Task
+import com.lambda.task.start
+import com.lambda.task.tasks.transfer
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.extension.CommandBuilder
@@ -51,60 +52,64 @@ object TransferCommand : LambdaCommand(
             required(integer("amount", 1)) { amount ->
                 required(string("from")) { from ->
                     suggests { ctx, builder ->
-                        val selection = selectStack(amount(ctx).value()) {
-                            isItem(stack(ctx).value().item)
-                        }
+                        val selection = stack(ctx).value().item.select(amount(ctx).value())
                         AutomationConfig.DEFAULT.runSafeAutomated {
-                            val containers = selection.findContainersWithMaterial()
+                            val containers =
+                                findContainers(
+                                    containerSelection {
+                                        hasStack(selection)
+                                    }
+                                ).toList()
                             val indexedContainers = containers.withIndex()
 
                             suggestMatching(
                                 indexedContainers,
                                 builder,
-                                { (index, container) ->
-                                    "\"${index + 1}. ${container.name}\""
-                                },
-                                { (_, container) ->
-                                    container.description(selection)
-                                }
+                                { (index, container) -> "\"${index + 1}. ${container.name}\"" },
+                                { (_, container) -> container.descriptionAndStock(selection) }
                             )
                         } ?: builder.buildFuture()
                     }
                     required(string("to")) { to ->
                         suggests { ctx, builder ->
-                            val selection = selectStack(amount(ctx).value()) {
-                                isItem(stack(ctx).value().item)
-                            }
+                            val selection = stack(ctx).value().item.select(amount(ctx).value())
                             AutomationConfig.DEFAULT.runSafeAutomated {
-                                val containers = selection.findContainersWithSpace()
+                                val containers =
+                                    findContainers(
+                                        containerSelection {
+                                            hasSpace(selection)
+                                        }
+                                    ).toList()
                                 val indexedContainers = containers.withIndex()
 
                                 suggestMatching(
                                     indexedContainers,
                                     builder,
-                                    { (index, container) ->
-                                        "\"${index + 1}. ${container.name}\""
-                                    },
-                                    { (_, container) ->
-                                        container.description(selection)
-                                    }
+                                    { (index, container) -> "\"${index + 1}. ${container.name}\"" },
+                                    { (_, container) -> container.descriptionAndStock(selection) }
                                 )
                             } ?: builder.buildFuture()
                         }
                         executeWithResult {
-                            val selection = selectStack(amount().value()) {
-                                isItem(stack().value().item)
-                            }
                             AutomationConfig.DEFAULT.runSafeAutomated {
-                                val fromContainer = ContainerHandler.containers().find {
-                                    it.name == from().value().split(".").last().trim()
-                                } ?: return@executeWithResult failure("From container not found")
+                                val fromSelection =
+                                    containerSelection {
+                                        predicate { it.name == from().value().split(".").first().trim() }
+                                    }
 
-                                val toContainer = ContainerHandler.containers().find {
-                                    it.name == to().value().split(".").last().trim()
-                                } ?: return@executeWithResult failure("To container not found")
+                                val toSelection =
+                                    containerSelection {
+                                        predicate { it.name == to().value().split(".").first().trim() }
+                                    }
 
-	                            fromContainer.transferByTask(selection, toContainer).execute(RootTask)
+	                            lastContainerTransfer =
+                                    transfer(
+                                        stackSelection(amount().value()) {
+                                            isItem(stack().value().item)
+                                        },
+                                        fromSelection,
+                                        toSelection
+                                    ).start()
                             }
                             return@executeWithResult success()
                         }
@@ -115,9 +120,11 @@ object TransferCommand : LambdaCommand(
 
         required(literal("cancel")) {
             executeWithResult {
-                lastContainerTransfer?.cancel() ?: run {
-                    return@executeWithResult failure("No transfer to cancel")
-                }
+                lastContainerTransfer
+                    ?.cancel()
+                    ?: run {
+                        return@executeWithResult failure("No transfer to cancel")
+                    }
                 this@TransferCommand.info("$lastContainerTransfer cancelled")
                 lastContainerTransfer = null
                 success()
diff --git a/src/main/kotlin/com/lambda/config/ConfigCategory.kt b/src/main/kotlin/com/lambda/config/ConfigCategory.kt
index c490a65e1..35ec67f6c 100644
--- a/src/main/kotlin/com/lambda/config/ConfigCategory.kt
+++ b/src/main/kotlin/com/lambda/config/ConfigCategory.kt
@@ -25,7 +25,7 @@ import com.lambda.config.migration.ConfigMigrationHandler
 import com.lambda.core.Loadable
 import com.lambda.event.events.ClientEvent
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.interaction.handlers.BaritoneHandler.primary
+import com.lambda.interaction.handler.handlers.BaritoneHandler.primary
 import com.lambda.threading.runIO
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.CommunicationUtils.logError
diff --git a/src/main/kotlin/com/lambda/config/ConfigEditor.kt b/src/main/kotlin/com/lambda/config/ConfigEditor.kt
index 8700d9889..df332bd5d 100644
--- a/src/main/kotlin/com/lambda/config/ConfigEditor.kt
+++ b/src/main/kotlin/com/lambda/config/ConfigEditor.kt
@@ -21,7 +21,7 @@ package com.lambda.config
 
 import com.lambda.config.entries.Property
 import com.lambda.config.entries.Setting
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.context.SafeContext
 import kotlin.reflect.KProperty0
 import kotlin.reflect.jvm.isAccessible
diff --git a/src/main/kotlin/com/lambda/config/automation/AutomationConfig.kt b/src/main/kotlin/com/lambda/config/automation/AutomationConfig.kt
index 4272fdf96..d90420e89 100644
--- a/src/main/kotlin/com/lambda/config/automation/AutomationConfig.kt
+++ b/src/main/kotlin/com/lambda/config/automation/AutomationConfig.kt
@@ -31,6 +31,9 @@ import com.lambda.config.categories.AutomationCategory
 import com.lambda.context.Automated
 import com.lambda.module.Module
 
+@DslMarker
+private annotation class AutomationConfigMarker
+
 open class AutomationConfig(
 	name: String,
 	configCategory: ConfigCategory = AutomationCategory
@@ -55,20 +58,17 @@ open class AutomationConfig(
 		private const val HOTBAR_TAB = "Hotbar"
 		private const val EAT_TAB = "Eat"
 
-		@DslMarker
-		private annotation class AutomationConfigMarker
-
-		@AutomationConfigMarker
-		context(module: Module)
-        fun IMutableAutomationConfig.setDefaultAutomationConfig(
-	        name: String = module.name
-		) = AutomationConfig("$name Automation Config").also { this.defaultAutomationConfig = it }
-
-		@AutomationConfigMarker
-        fun IMutableAutomationConfig.setDefaultAutomationConfig(
-	        name: String
-		) = AutomationConfig("$name Automation Config").also { this.defaultAutomationConfig = it }
-
 		val DEFAULT = AutomationConfig("Default")
     }
-}
\ No newline at end of file
+}
+
+@AutomationConfigMarker
+context(module: Module)
+fun IMutableAutomationConfig.setDefaultAutomationConfig(
+	name: String = module.name
+) = AutomationConfig("$name Automation Config").also { this.defaultAutomationConfig = it }
+
+@AutomationConfigMarker
+fun IMutableAutomationConfig.setDefaultAutomationConfig(
+	name: String
+) = AutomationConfig("$name Automation Config").also { this.defaultAutomationConfig = it }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/blocks/BreakSettings.kt b/src/main/kotlin/com/lambda/config/blocks/BreakSettings.kt
index e44e1ed4f..1449738a6 100644
--- a/src/main/kotlin/com/lambda/config/blocks/BreakSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/BreakSettings.kt
@@ -28,7 +28,7 @@ import com.lambda.config.blocks.BreakConfig.SwapMode
 import com.lambda.config.blocks.BreakConfig.SwingMode
 import com.lambda.config.blocks.BreakConfig.WhitelistMode
 import com.lambda.config.blocks.BuildConfig.SwingType
-import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
+import com.lambda.config.entries.onValueChangeUnsafe
 import com.lambda.config.hide
 import com.lambda.config.withEdits
 import com.lambda.event.events.TickEvent
diff --git a/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt b/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
index 7e98fc835..6e7922008 100644
--- a/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
@@ -17,8 +17,8 @@
 
 package com.lambda.config.blocks
 
-import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.dist
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
 import com.lambda.util.math.distSq
diff --git a/src/main/kotlin/com/lambda/config/blocks/EatConfig.kt b/src/main/kotlin/com/lambda/config/blocks/EatConfig.kt
index 1e0e5714b..548540934 100644
--- a/src/main/kotlin/com/lambda/config/blocks/EatConfig.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/EatConfig.kt
@@ -19,7 +19,8 @@ package com.lambda.config.blocks
 
 import com.lambda.context.Automated
 import com.lambda.context.AutomatedSafeContext
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
+import com.lambda.interaction.container.selection.StackAndSlot
+import com.lambda.interaction.container.selection.stackSelection
 import com.lambda.threading.runSafe
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
@@ -55,17 +56,17 @@ interface EatConfig {
 
     @Suppress("unused")
     enum class SelectionPriority(
-        val comparator: Comparator,
+        val comparator: Comparator>,
         override val displayName: String,
         override val description: String
     ): NamedEnum, Describable {
         LeastNutritious(
-            compareBy { it.item.nutrition },
+            compareBy { it.stack.item.nutrition },
             "Least Nutritious",
             "Eats food items with the least nutritional value."
         ),
         MostNutritious(
-            compareByDescending { it.item.nutrition },
+            compareByDescending { it.stack.item.nutrition },
             "Most Nutritious",
             "Eats food items with the most nutritional value."
         )
@@ -94,14 +95,17 @@ interface EatConfig {
         } ?: false
 
         context(c: Automated)
-        fun selector() = selectStack(sorter = c.eatConfig.selectionPriority.comparator) {
-            when(this@Reason) {
-                None -> any()
-                Hunger -> isOneOfItems(c.eatConfig.nutritiousFood)
-                Damage -> isOneOfItems(c.eatConfig.regenerationFood)
-                Fire -> isOneOfItems(c.eatConfig.resistanceFood)
-            } and if (c.eatConfig.ignoreBadFood) isNoneOfItems(c.eatConfig.badFood) else any()
-        }
+        fun selector() =
+            stackSelection {
+                when(this@Reason) {
+                    Hunger -> ofAnyItems(c.eatConfig.nutritiousFood)
+                    Damage -> ofAnyItems(c.eatConfig.regenerationFood)
+                    Fire -> ofAnyItems(c.eatConfig.resistanceFood)
+	                else -> {}
+                }
+                if (c.eatConfig.ignoreBadFood) noneOfItems(c.eatConfig.badFood)
+                sortedWith(c.eatConfig.selectionPriority.comparator)
+            }
     }
 
     companion object {
diff --git a/src/main/kotlin/com/lambda/config/blocks/EntityColorSettings.kt b/src/main/kotlin/com/lambda/config/blocks/EntityColorSettings.kt
index 4545e7d1c..ea27b3e0b 100644
--- a/src/main/kotlin/com/lambda/config/blocks/EntityColorSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/EntityColorSettings.kt
@@ -20,7 +20,7 @@ package com.lambda.config.blocks
 import com.lambda.config.Config
 import com.lambda.config.ConfigBlock
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.FriendHandler.isFriend
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
 import com.lambda.util.EntityUtils.EntityGroup
 import com.lambda.util.EntityUtils.entityGroup
 import com.lambda.util.extension.blockColor
diff --git a/src/main/kotlin/com/lambda/config/blocks/HotbarSettings.kt b/src/main/kotlin/com/lambda/config/blocks/HotbarSettings.kt
index ce76c94c3..3c8b1e9c2 100644
--- a/src/main/kotlin/com/lambda/config/blocks/HotbarSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/HotbarSettings.kt
@@ -28,5 +28,5 @@ class HotbarSettings(override val c: Config) : HotbarConfig, ConfigBlock {
     override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks")
     override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { swapDelay <= 0 }
     override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks")
-    override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true)
+    override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Pre, TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/blocks/InventoryConfig.kt b/src/main/kotlin/com/lambda/config/blocks/InventoryConfig.kt
index 6a0c26116..c63843619 100644
--- a/src/main/kotlin/com/lambda/config/blocks/InventoryConfig.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/InventoryConfig.kt
@@ -17,68 +17,12 @@
 
 package com.lambda.config.blocks
 
-import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
-import com.lambda.interaction.material.ContainerSelection
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.util.Describable
-import com.lambda.util.NamedEnum
 import net.minecraft.item.Item
 
 interface InventoryConfig {
 	val tickStageMask: Collection
 	val disposables: Collection
-	val swapWithDisposables: Boolean
-	val providerPriority: Priority
-	val storePriority: Priority
-
-	val accessShulkerBoxes: Boolean
-	val accessChests: Boolean
-	val accessEnderChest: Boolean
-	val accessStashes: Boolean
-
-	val containerSelection: ContainerSelection
-		get() = ContainerSelection.selectContainer {
-			val allowedContainers = buildSet {
-				addAll(MaterialContainer.Rank.entries)
-				if (!accessShulkerBoxes) remove(MaterialContainer.Rank.ShulkerBox)
-				if (!accessEnderChest) remove(MaterialContainer.Rank.EnderChest)
-				if (!accessChests) remove(MaterialContainer.Rank.Chest)
-				if (!accessStashes) remove(MaterialContainer.Rank.Stash)
-			}
-			ofAnyType(*allowedContainers.toTypedArray())
-		}
-
-	enum class Priority(
-		override val displayName: String,
-		override val description: String
-	) : NamedEnum, Describable {
-		WithMinItems("With Min Items", "Pick containers with the fewest matching items (or least space) first; useful for topping off or clearing leftovers."),
-		WithMaxItems("With Max Items", "Pick containers with the most matching items (or most space) first; ideal for bulk moves with fewer transfers.");
-
-		context(_: SafeContext)
-		fun materialComparator(selection: StackSelection) =
-			when (this) {
-				WithMaxItems -> compareBy { it.rank }
-					.thenByDescending { it.materialAvailable(selection) }
-					.thenBy { it.name }
-
-				WithMinItems -> compareBy { it.rank }
-					.thenBy { it.materialAvailable(selection) }
-					.thenBy { it.name }
-			}
-
-		context(_: SafeContext)
-		fun spaceComparator(selection: StackSelection) =
-			when (this) {
-				WithMaxItems -> compareBy { it.rank }
-					.thenByDescending { it.spaceAvailable(selection) }
-					.thenBy { it.name }
-
-				WithMinItems -> compareBy { it.rank }
-					.thenBy { it.spaceAvailable(selection) }
-					.thenBy { it.name }
-			}
-	}
+	val allowedContainers: Collection
+	val enderChestSearchRadius: Int
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/blocks/InventorySettings.kt b/src/main/kotlin/com/lambda/config/blocks/InventorySettings.kt
index ee64c7e13..84f059189 100644
--- a/src/main/kotlin/com/lambda/config/blocks/InventorySettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/InventorySettings.kt
@@ -21,22 +21,17 @@ import com.lambda.config.Config
 import com.lambda.config.ConfigBlock
 import com.lambda.config.Group
 import com.lambda.event.events.TickEvent.Companion.ALL_STAGES
+import com.lambda.interaction.container.ContainerType
 import com.lambda.util.item.ItemUtils
 
 class InventorySettings(override val c: Config) : InventoryConfig, ConfigBlock {
     companion object {
-        private const val CONTAINER_GROUP = "Container"
-        private const val ACCESS_GROUP = "Access"
+        const val ENDER_CHEST_GROUP = "Ender Chest"
     }
 
     override val tickStageMask by c.setting("Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true)
-    @Group(CONTAINER_GROUP) override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot")
-    @Group(CONTAINER_GROUP) override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones")
-    @Group(CONTAINER_GROUP) override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when retrieving the item from")
-    @Group(CONTAINER_GROUP) override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when storing the item to")
+    override val disposables by c.setting("Disposables", ItemUtils.DEFAULT_DISPOSABLES, description = "Items that will be ignored when checking for a free slot")
+    override val allowedContainers by c.setting("Allowed Containers", ContainerType.entries, description = "What containers are accessible")
 
-    @Group(ACCESS_GROUP) override val accessShulkerBoxes by c.setting("Access Shulker Boxes", false, "Allow access to the player's shulker boxes")
-    @Group(ACCESS_GROUP) override val accessChests by c.setting("Access Chests", false, "Allow access to the player's normal chests")
-    @Group(ACCESS_GROUP) override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest")
-    @Group(ACCESS_GROUP) override val accessStashes by c.setting("Access Stashes", false, "Allow access to the player's stashes")
+    @Group(ENDER_CHEST_GROUP) override val enderChestSearchRadius by c.setting("Initial Search Radius", 10, 0..20, unit = " blocks", description = "Initial search radius to scan for an already placed ender chest. If one isn't found, one will be placed. If an ender chest isn't found in the inventory, it scans the rest of the loaded area to find an already placed one")
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/blocks/RotationConfig.kt b/src/main/kotlin/com/lambda/config/blocks/RotationConfig.kt
index a20530cf4..3f13dfa0b 100644
--- a/src/main/kotlin/com/lambda/config/blocks/RotationConfig.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/RotationConfig.kt
@@ -18,8 +18,8 @@
 package com.lambda.config.blocks
 
 import com.lambda.event.events.TickEvent
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.RotationMode
 
 interface RotationConfig {
     /**
diff --git a/src/main/kotlin/com/lambda/config/blocks/RotationSettings.kt b/src/main/kotlin/com/lambda/config/blocks/RotationSettings.kt
index cdaaf397c..c927e65a5 100644
--- a/src/main/kotlin/com/lambda/config/blocks/RotationSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/RotationSettings.kt
@@ -21,7 +21,7 @@ import com.lambda.config.Config
 import com.lambda.config.ConfigBlock
 import com.lambda.event.events.TickEvent
 import com.lambda.event.events.TickEvent.Companion.ALL_STAGES
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.RotationMode
 import kotlin.math.PI
 import kotlin.math.abs
 import kotlin.math.cos
diff --git a/src/main/kotlin/com/lambda/config/blocks/TargetingSettings.kt b/src/main/kotlin/com/lambda/config/blocks/TargetingSettings.kt
index d29bf56af..071f78564 100644
--- a/src/main/kotlin/com/lambda/config/blocks/TargetingSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/TargetingSettings.kt
@@ -22,10 +22,10 @@ import com.lambda.config.ConfigBlock
 import com.lambda.config.hide
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.FriendHandler.isFriend
-import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.dist
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.fullHealth
 import com.lambda.util.math.distSq
diff --git a/src/main/kotlin/com/lambda/config/categories/FriendCategory.kt b/src/main/kotlin/com/lambda/config/categories/FriendCategory.kt
index 3484222a8..130c1e786 100644
--- a/src/main/kotlin/com/lambda/config/categories/FriendCategory.kt
+++ b/src/main/kotlin/com/lambda/config/categories/FriendCategory.kt
@@ -18,7 +18,7 @@
 package com.lambda.config.categories
 
 import com.lambda.config.ConfigCategory
-import com.lambda.interaction.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler
 import com.lambda.threading.runIO
 import com.lambda.util.FolderRegistry
 import java.io.File
diff --git a/src/main/kotlin/com/lambda/config/entries/Setting.kt b/src/main/kotlin/com/lambda/config/entries/Setting.kt
index e68c0def7..17777d42a 100644
--- a/src/main/kotlin/com/lambda/config/entries/Setting.kt
+++ b/src/main/kotlin/com/lambda/config/entries/Setting.kt
@@ -31,6 +31,7 @@ import com.lambda.config.ConfigEntry
 import com.lambda.config.EntryCore
 import com.lambda.config.EntryLayer
 import com.lambda.config.MultipleLayerType
+import com.lambda.config.entries.Setting.ValueListener
 import com.lambda.context.SafeContext
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.threading.runSafe
@@ -229,42 +230,40 @@ abstract class Setting(
 
 	override fun toString() = "Setting $name: $value"
 
-	companion object {
-		/**
-		 * Will only register changes of the variable, not the content of the variable!
-		 * E.g., if the variable is a list, it will only register if the list reference changes, not if the content of the list changes.
-		 */
-		@ConfigEntryDsl
-		fun , T> S.onValueChange(block: SafeContext.(from: T, to: T) -> Unit) =
-			apply {
-				listeners.add(ValueListener(true) { from, to ->
-					runSafe { block(from, to) }
-				})
-			}
-
-		@ConfigEntryDsl
-		fun , T> S.onValueChangeUnsafe(block: (from: T, to: T) -> Unit) =
-			apply { listeners.add(ValueListener(true, block)) }
+	class ValueListener(val requiresValueChange: Boolean, val execute: (from: T, to: T) -> Unit)
+}
 
-		@ConfigEntryDsl
-		fun , T> S.onValueSet(block: SafeContext.(from: T, to: T) -> Unit) =
-			apply {
-				listeners.add(ValueListener(false) { from, to ->
-					runSafe { block(from, to) }
-				})
-			}
+/**
+ * Will only register changes of the variable, not the content of the variable!
+ * E.g., if the variable is a list, it will only register if the list reference changes, not if the content of the list changes.
+ */
+@ConfigEntryDsl
+fun , T> S.onValueChange(block: SafeContext.(from: T, to: T) -> Unit) =
+	apply {
+		listeners.add(ValueListener(true) { from, to ->
+			runSafe { block(from, to) }
+		})
+	}
 
-		@ConfigEntryDsl
-		fun , T> S.onValueSetUnsafe(block: (from: T, to: T) -> Unit) =
-			apply { listeners.add(ValueListener(false, block)) }
+@ConfigEntryDsl
+fun , T> S.onValueChangeUnsafe(block: (from: T, to: T) -> Unit) =
+	apply { listeners.add(ValueListener(true, block)) }
 
-		@ConfigEntryDsl
-		fun , T> S.disabled(predicate: () -> Boolean) =
-			apply { disabled = predicate }
+@ConfigEntryDsl
+fun , T> S.onValueSet(block: SafeContext.(from: T, to: T) -> Unit) =
+	apply {
+		listeners.add(ValueListener(false) { from, to ->
+			runSafe { block(from, to) }
+		})
 	}
 
-	class ValueListener(val requiresValueChange: Boolean, val execute: (from: T, to: T) -> Unit)
-}
+@ConfigEntryDsl
+fun , T> S.onValueSetUnsafe(block: (from: T, to: T) -> Unit) =
+	apply { listeners.add(ValueListener(false, block)) }
+
+@ConfigEntryDsl
+fun , T> S.disabled(predicate: () -> Boolean) =
+	apply { disabled = predicate }
 
 class SettingEntryLayer, U>(
 	override val parent: EntryLayer.Multiple>,
diff --git a/src/main/kotlin/com/lambda/config/serializers/EnderChestContainerSerializer.kt b/src/main/kotlin/com/lambda/config/serializers/EnderChestContainerSerializer.kt
new file mode 100644
index 000000000..08c442617
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/serializers/EnderChestContainerSerializer.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+@file:Suppress("unused")
+
+package com.lambda.config.serializers
+
+import com.lambda.config.Deserializer
+import com.lambda.config.JsonOps
+import com.lambda.config.Serializer
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.containers.external.EnderChestContainer
+import net.minecraft.item.ItemStack
+import tools.jackson.core.JsonGenerator
+import tools.jackson.core.JsonParser
+import tools.jackson.databind.DeserializationContext
+import tools.jackson.databind.SerializationContext
+import tools.jackson.databind.node.ArrayNode
+import tools.jackson.databind.node.ObjectNode
+
+object EnderChestContainerSerializer : Serializer(EnderChestContainer::class.java) {
+    override fun serialize(container: EnderChestContainer, gen: JsonGenerator, ctxt: SerializationContext) {
+        gen.writeStartObject()
+        gen.writeStringProperty("Type", ContainerType.EnderChest.name)
+
+        gen.writeArrayPropertyStart("Stacks")
+        container.stacks.forEach { stack ->
+            if (stack.isEmpty) {
+                gen.writeNull()
+            } else {
+                val encoded = ItemStack.CODEC
+                    .encodeStart(JsonOps.UNCOMPRESSED, stack)
+                    .result()
+                if (encoded.isPresent) {
+                    gen.writePOJO(encoded.get())
+                } else {
+                    gen.writeNull()
+                }
+            }
+        }
+        gen.writeEndArray()
+
+        gen.writeEndObject()
+    }
+}
+
+object EnderChestContainerDeserializer : Deserializer(EnderChestContainer::class.java) {
+    override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): EnderChestContainer? {
+        throw initFromJsonException("EnderChestContainer")
+    }
+
+    override fun deserialize(p: JsonParser, ctxt: DeserializationContext, intoValue: EnderChestContainer): EnderChestContainer {
+        val root = p.readValueAsTree()
+        val stackArray = root.get("Stacks") as? ArrayNode
+        val decodedStacks =
+            stackArray?.mapNotNull { element ->
+                if (element.isNull) {
+                    ItemStack.EMPTY
+                } else {
+                    ItemStack.CODEC
+                        .parse(JsonOps.UNCOMPRESSED, element)
+                        .result()
+                        .orElse(ItemStack.EMPTY)
+                }
+            } ?: emptyList()
+
+        val stacks =
+            if (decodedStacks.size < 27) {
+                decodedStacks + List(27 - decodedStacks.size) { ItemStack.EMPTY }
+            } else decodedStacks
+
+        EnderChestContainer.update(stacks)
+        EnderChestContainer.scanStacksForNestedContainers()
+        return EnderChestContainer
+    }
+}
diff --git a/src/main/kotlin/com/lambda/config/serializers/PlacedContainerSerializer.kt b/src/main/kotlin/com/lambda/config/serializers/PlacedContainerSerializer.kt
new file mode 100644
index 000000000..ce2872caf
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/serializers/PlacedContainerSerializer.kt
@@ -0,0 +1,131 @@
+@file:Suppress("unused")
+
+package com.lambda.config.serializers
+
+import com.lambda.config.FallbackDeserializer
+import com.lambda.config.FallbackSerializer
+import com.lambda.config.JsonOps
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.container.containers.external.ChestContainer
+import com.lambda.interaction.container.containers.external.DoubleChestContainer
+import com.lambda.interaction.container.containers.external.PlacedShulkerBoxContainer
+import com.lambda.util.world.Dimension
+import net.minecraft.block.Blocks
+import net.minecraft.item.ItemStack
+import net.minecraft.registry.Registries
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import tools.jackson.core.JsonGenerator
+import tools.jackson.core.JsonParser
+import tools.jackson.databind.DeserializationContext
+import tools.jackson.databind.SerializationContext
+import tools.jackson.databind.node.ArrayNode
+import tools.jackson.databind.node.ObjectNode
+
+object PlacedContainerSerializer : FallbackSerializer(PlacedContainer::class.java) {
+    override fun serialize(container: PlacedContainer, gen: JsonGenerator, ctxt: SerializationContext) {
+        gen.writeStartObject()
+        gen.writeStringProperty("Type", container.type.name)
+        gen.writeStringProperty("Dimension", container.dimension.name)
+        gen.writePOJOProperty("Pos", BlockPos.CODEC.encodeStart(JsonOps.UNCOMPRESSED, container.pos).orThrow)
+        
+        when (container) {
+            is PlacedShulkerBoxContainer -> {
+                val blockId = Registries.BLOCK.getId(container.block).toString()
+                gen.writeStringProperty("Block", blockId)
+            }
+            is DoubleChestContainer -> {
+                gen.writeBooleanProperty("Double", true)
+                gen.writePOJOProperty("Left Pos", BlockPos.CODEC.encodeStart(JsonOps.UNCOMPRESSED, container.leftPos).orThrow)
+                gen.writePOJOProperty("Right Pos", BlockPos.CODEC.encodeStart(JsonOps.UNCOMPRESSED, container.rightPos).orThrow)
+            }
+            else -> {}
+        }
+
+        gen.writeArrayPropertyStart("Stacks")
+        container.stacks.forEach { stack ->
+            if (stack.isEmpty) {
+                gen.writeNull()
+            } else {
+                val encoded = ItemStack.CODEC
+                    .encodeStart(JsonOps.UNCOMPRESSED, stack)
+                    .result()
+                if (encoded.isPresent) {
+                    gen.writePOJO(encoded.get())
+                } else {
+                    gen.writeNull()
+                }
+            }
+        }
+        gen.writeEndArray()
+        gen.writeEndObject()
+    }
+}
+
+object PlacedContainerDeserializer : FallbackDeserializer(PlacedContainer::class.java) {
+    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PlacedContainer? {
+        val root = p.readValueAsTree() ?: return null
+        
+        val typeName = root.get("Type")?.asString() ?: return null
+        val type = ContainerType.entries.find { it.name == typeName } ?: return null
+        val dimension = root.get("Dimension")?.asString()
+            ?.takeUnless { it.isBlank() }
+            ?.let { name -> Dimension.entries.firstOrNull { it.name == name } }
+            ?: Dimension.Unknown
+        
+        val posNode = root.get("Pos") ?: return null
+        val pos = BlockPos.CODEC.parse(JsonOps.UNCOMPRESSED, posNode)
+            .result()
+            .orElse(null)
+            ?: return null
+        
+        val stackArray = root.get("Stacks") as? ArrayNode
+        val decodedStacks =
+            stackArray?.mapNotNull { element ->
+                if (element.isNull) {
+                    ItemStack.EMPTY
+                } else {
+                    ItemStack.CODEC
+                        .parse(JsonOps.UNCOMPRESSED, element)
+                        .result()
+                        .orElse(ItemStack.EMPTY)
+                }
+            } ?: emptyList()
+
+        fun padStacks(expectedSize: Int): List =
+            if (decodedStacks.size < expectedSize) {
+                decodedStacks + List(expectedSize - decodedStacks.size) { ItemStack.EMPTY }
+            } else decodedStacks
+
+        val container =
+            when (type) {
+                ContainerType.PlacedShulkerBox -> {
+                    val blockId = root.get("Block")?.asString()
+                    val block = blockId?.let { Registries.BLOCK.get(Identifier.of(it)) } ?: Blocks.SHULKER_BOX
+                    PlacedShulkerBoxContainer(pos, block, null, padStacks(27), dimension)
+                }
+                ContainerType.Chest -> {
+                    if (root.get("Double")?.asBoolean() == true) {
+                        val leftPosNode = root.get("Left Pos") ?: return null
+                        val leftPos = BlockPos.CODEC.parse(JsonOps.UNCOMPRESSED, leftPosNode)
+                            .result()
+                            .orElse(null)
+                            ?: return null
+                        val rightPosNode = root.get("Right Pos") ?: return null
+                        val rightPos = BlockPos.CODEC.parse(JsonOps.UNCOMPRESSED, rightPosNode)
+                            .result()
+                            .orElse(null)
+                            ?: return null
+                        DoubleChestContainer(pos, leftPos, rightPos, padStacks(54), dimension = dimension)
+                    } else {
+                        ChestContainer(pos, padStacks(27), dimension = dimension)
+                    }
+                }
+                else -> return null
+            }
+        
+        container.scanStacksForNestedContainers()
+        return container
+    }
+}
diff --git a/src/main/kotlin/com/lambda/config/settings/StringSetting.kt b/src/main/kotlin/com/lambda/config/settings/StringSetting.kt
index cb379f4ec..026ee1a0a 100644
--- a/src/main/kotlin/com/lambda/config/settings/StringSetting.kt
+++ b/src/main/kotlin/com/lambda/config/settings/StringSetting.kt
@@ -15,6 +15,8 @@
  * along with this program.  If not, see .
  */
 
+@file:Suppress("unused")
+
 package com.lambda.config.settings
 
 import com.lambda.brigadier.argument.greedyString
@@ -44,7 +46,6 @@ class StringSetting(
     var multiline: Boolean = false,
     var flags: Int = ImGuiInputTextFlags.None,
 ) : Setting(name, description, defaultValue, layer, config, visibility) {
-
     override fun ImGuiBuilder.buildLayout() {
         if (multiline) {
             inputTextMultiline(name, ::value, flags = flags)
diff --git a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt
index 6dc59d4cf..bc01d1ef2 100644
--- a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt
+++ b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt
@@ -15,6 +15,8 @@
  * along with this program.  If not, see .
  */
 
+@file:Suppress("unused")
+
 package com.lambda.config.settings.collections
 
 import com.lambda.config.Config
diff --git a/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt b/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt
index 00629b99f..5b069fd72 100644
--- a/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt
+++ b/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt
@@ -15,6 +15,8 @@
  * along with this program.  If not, see .
  */
 
+@file:Suppress("unused")
+
 package com.lambda.config.settings.complex
 
 import com.fasterxml.jackson.annotation.JsonIncludeProperties
@@ -218,26 +220,23 @@ class KeybindSetting(
         }
     }
 
-    @Suppress("unused")
-    companion object {
-        @ConfigEntryDsl
-        fun KeybindSetting.onPress(block: SafeContext.(ButtonEvent) -> Unit) = apply { pressListeners.add(block) }
+    @ConfigEntryDsl
+    fun onPress(block: SafeContext.(ButtonEvent) -> Unit) = apply { pressListeners.add(block) }
 
-        @ConfigEntryDsl
-        fun KeybindSetting.onPressUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafePressListeners.add(block) }
+    @ConfigEntryDsl
+    fun onPressUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafePressListeners.add(block) }
 
-        @ConfigEntryDsl
-        fun KeybindSetting.onRepeat(block: SafeContext.(ButtonEvent) -> Unit) = apply { repeatListeners.add(block) }
+    @ConfigEntryDsl
+    fun onRepeat(block: SafeContext.(ButtonEvent) -> Unit) = apply { repeatListeners.add(block) }
 
-        @ConfigEntryDsl
-        fun KeybindSetting.onRepeatUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeRepeatListeners.add(block) }
+    @ConfigEntryDsl
+    fun onRepeatUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeRepeatListeners.add(block) }
 
-        @ConfigEntryDsl
-        fun KeybindSetting.onRelease(block: SafeContext.(ButtonEvent) -> Unit) = apply { releaseListeners.add(block) }
+    @ConfigEntryDsl
+    fun onRelease(block: SafeContext.(ButtonEvent) -> Unit) = apply { releaseListeners.add(block) }
 
-        @ConfigEntryDsl
-        fun KeybindSetting.onReleaseUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeReleaseListeners.add(block) }
-    }
+    @ConfigEntryDsl
+    fun onReleaseUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeReleaseListeners.add(block) }
 }
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/context/AutomatedSafeContext.kt b/src/main/kotlin/com/lambda/context/AutomatedSafeContext.kt
index 96bb48367..3d59c2143 100644
--- a/src/main/kotlin/com/lambda/context/AutomatedSafeContext.kt
+++ b/src/main/kotlin/com/lambda/context/AutomatedSafeContext.kt
@@ -17,7 +17,14 @@
 
 package com.lambda.context
 
-class AutomatedSafeContext(
-	safeContext: SafeContext,
-	automated: Automated
-) : IAutomatedSafeContext, SafeContext by safeContext, Automated by automated
\ No newline at end of file
+interface AutomatedSafeContext : SafeContext, Automated {
+	companion object {
+		fun create(
+			safeContext: SafeContext,
+			automated: Automated
+		): AutomatedSafeContext =
+			object : AutomatedSafeContext,
+				SafeContext by safeContext,
+				Automated by automated {}
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/event/EventFlow.kt b/src/main/kotlin/com/lambda/event/EventFlow.kt
index 91db873f4..e21723c90 100644
--- a/src/main/kotlin/com/lambda/event/EventFlow.kt
+++ b/src/main/kotlin/com/lambda/event/EventFlow.kt
@@ -301,8 +301,7 @@ object EventFlow {
      * @return `true` if the listener should not be notified, `false` otherwise.
      */
     private fun  shouldNotNotify(listener: Listener, event: Event) =
-        (listener.owner is Muteable &&
-                (listener.owner as Muteable).isMuted &&
+        ((listener.owner as? Muteable)?.isMuted == true &&
                 !listener.alwaysListen) ||
                 (event is ICancellable && event.isCanceled())
 }
diff --git a/src/main/kotlin/com/lambda/event/events/ContainerEvent.kt b/src/main/kotlin/com/lambda/event/events/ContainerEvent.kt
index 2c0031cb1..0bfdb1899 100644
--- a/src/main/kotlin/com/lambda/event/events/ContainerEvent.kt
+++ b/src/main/kotlin/com/lambda/event/events/ContainerEvent.kt
@@ -19,14 +19,14 @@ package com.lambda.event.events
 
 import com.lambda.event.callback.Cancellable
 import com.lambda.event.callback.ICancellable
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.Container
 import net.minecraft.screen.slot.Slot
 
 sealed class ContainerEvent {
 	data class Transfer(
 		val fromSlot: Slot,
 		val toSlot: Slot,
-		val from: MaterialContainer,
-		val to: MaterialContainer
+		val from: Container,
+		val to: Container
 	) : ICancellable by Cancellable()
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/event/events/PlayerEvent.kt b/src/main/kotlin/com/lambda/event/events/PlayerEvent.kt
index 1498eaa59..9a21f9fcf 100644
--- a/src/main/kotlin/com/lambda/event/events/PlayerEvent.kt
+++ b/src/main/kotlin/com/lambda/event/events/PlayerEvent.kt
@@ -142,5 +142,13 @@ sealed class PlayerEvent {
         val button: Int,
         val action: SlotActionType,
         val screenHandler: ScreenHandler,
-    ) : ICancellable by Cancellable()
+    ) : ICancellable by Cancellable() {
+        data class Post(
+            val syncId: Int,
+            val slot: Int,
+            val button: Int,
+            val action: SlotActionType,
+            val screenHandler: ScreenHandler,
+        ) : Event
+    }
 }
diff --git a/src/main/kotlin/com/lambda/event/events/RotationEvent.kt b/src/main/kotlin/com/lambda/event/events/RotationEvent.kt
index c6f6f2a8a..c807c825e 100644
--- a/src/main/kotlin/com/lambda/event/events/RotationEvent.kt
+++ b/src/main/kotlin/com/lambda/event/events/RotationEvent.kt
@@ -18,7 +18,7 @@
 package com.lambda.event.events
 
 import com.lambda.event.Event
-import com.lambda.interaction.managers.rotating.RotationRequest
+import com.lambda.interaction.manager.managers.rotating.RotationRequest
 import net.minecraft.client.input.Input
 
 sealed class RotationEvent {
diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
index d7aebfca2..767c56ca6 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
@@ -63,7 +63,7 @@ class ChunkedRenderer(
 	private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>()
 
 	init {
-		owner.listenUnsafe { event ->
+		owner.listenUnsafe(alwaysListen = true) { event ->
 			val pos = event.pos
 			val world = mc.world ?: return@listenUnsafe
 			world.getWorldChunk(pos)?.chunkData?.markDirty()
@@ -77,9 +77,9 @@ class ChunkedRenderer(
 			if (zInChunk == 15) world.getWorldChunk(pos.south())?.chunkData?.markDirty()
 		}
 
-		owner.listenUnsafe { event -> event.chunk.chunkData.markDirty() }
-		owner.listenUnsafe { chunkMap.remove(it.chunk.chunkKey)?.clearData() }
-		owner.listenUnsafe { clear() }
+		owner.listenUnsafe(alwaysListen = true) { event -> event.chunk.chunkData.markDirty() }
+		owner.listenUnsafe(alwaysListen = true) { chunkMap.remove(it.chunk.chunkKey)?.clearData() }
+		owner.listenUnsafe(alwaysListen = true) { clear() }
 
 		owner.listenConcurrentlyUnsafe {
 			if (pauseUpdates()) return@listenConcurrentlyUnsafe
diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
index edb196076..197e207fc 100644
--- a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
+++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
@@ -20,7 +20,7 @@ package com.lambda.graphics.text
 import com.lambda.Lambda.LOG
 import com.lambda.config.Config
 import com.lambda.config.categories.FontCategory
-import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
+import com.lambda.config.entries.onValueChangeUnsafe
 import com.lambda.core.Loadable
 import com.lambda.event.events.ClientEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt
index 2dc94faec..df50b2a31 100644
--- a/src/main/kotlin/com/lambda/gui/MenuBar.kt
+++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt
@@ -40,10 +40,10 @@ import com.lambda.imgui.flag.ImGuiCol
 import com.lambda.imgui.flag.ImGuiStyleVar
 import com.lambda.imgui.flag.ImGuiWindowFlags
 import com.lambda.imgui.type.ImBoolean
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.ModuleRegistry
 import com.lambda.module.ModuleRegistry.moduleNameMap
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.network.LambdaAPI
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.info
diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
index f4e708bd8..973f570a0 100644
--- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
+++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
@@ -21,8 +21,7 @@ import com.lambda.Lambda.mc
 import com.lambda.config.Config
 import com.lambda.config.Tab
 import com.lambda.config.categories.GuiCategory
-import com.lambda.config.entries.Setting.Companion.onValueChange
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPressUnsafe
+import com.lambda.config.entries.onValueChange
 import com.lambda.core.Loadable
 import com.lambda.event.events.GuiEvent
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
@@ -47,10 +46,10 @@ import com.lambda.imgui.flag.ImGuiCond
 import com.lambda.imgui.flag.ImGuiHoveredFlags
 import com.lambda.imgui.flag.ImGuiWindowFlags
 import com.lambda.module.ModuleRegistry
+import com.lambda.module.ModuleTag
+import com.lambda.module.ModuleTag.Companion.shownTags
 import com.lambda.module.modules.client.Client
 import com.lambda.module.modules.combat.autodisconnect.AutoDisconnectScreen
-import com.lambda.module.tag.ModuleTag
-import com.lambda.module.tag.ModuleTag.Companion.shownTags
 import com.lambda.sound.LambdaSound
 import com.lambda.sound.SoundHandler.play
 import com.lambda.util.Describable
@@ -137,11 +136,9 @@ object ClickGuiLayout : Loadable, Config(
 	const val BASE_SCALE = 100
 	const val BASE_SCALE_MULTI = 1.8
 
-	fun deviceScaleMultiplier() = try {
-		val monitorWidth = mc.window.monitor!!.currentVideoMode!!.width.toDouble()
-		(monitorWidth / 1920.0).coerceIn(0.5, 4.0)
-	} catch (_: Throwable) {
-		1.0
+	fun deviceScaleMultiplier(): Double {
+		val monitorWidth = mc.window.monitor?.currentVideoMode?.width?.toDouble() ?: return 1.0
+		return (monitorWidth / 1920.0).coerceIn(0.5, 4.0)
 	}
 
 	// General
@@ -157,8 +154,7 @@ object ClickGuiLayout : Loadable, Config(
 				mc.window.setIcon(mc.defaultResourcePack, icon)
 			}
 		}
-	@JvmStatic
-	@Tab(GENERAL_TAB) val setLambdaWindowTitle by setting("Set Lambda Window Title", true).onValueChange { _, _ -> mc.updateWindowTitle() }
+	@Tab(GENERAL_TAB) @JvmStatic val setLambdaWindowTitle by setting("Set Lambda Window Title", true).onValueChange { _, _ -> mc.updateWindowTitle() }
 	@Tab(GENERAL_TAB) val lambdaTitleAppendixName by setting("Append Username", true) { setLambdaWindowTitle }.onValueChange { _, _ -> mc.updateWindowTitle() }
 	@Tab(GENERAL_TAB) val backgroundBlur by setting("Background Blur", true)
 	@Tab(GENERAL_TAB) val backgroundDarkening by setting("Background Darkening", true)
diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt
index b070261d3..908805e6f 100644
--- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt
+++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt
@@ -123,9 +123,11 @@ object HudGuiLayout : Loadable, Config(
 
 	                if (!isLocked) {
 		                activeDragHudName?.let { drag ->
-			                if (mouseDown) updateDragAndSnapping(
-				                drag, lastBounds[drag]!!, dragOffsetX, dragOffsetY, pendingPositions, snapOverlays
-			                )
+			                lastBounds[drag]?.let { bounds ->
+				                if (mouseDown) updateDragAndSnapping(
+					                drag, bounds, dragOffsetX, dragOffsetY, pendingPositions, snapOverlays
+				                )
+			                }
 			                drawDragGrid()
 		                }
 	                }
diff --git a/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt b/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt
index f06de623e..f5e6e3176 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt
@@ -23,8 +23,6 @@ data class StaticBlueprint(
     override val structure: Structure,
 ) : Blueprint() {
     override fun toString() = "Static Blueprint at ${center?.toShortString()}"
-
-    companion object {
-        fun Structure.toBlueprint() = StaticBlueprint(this)
-    }
 }
+
+fun Structure.toBlueprint() = StaticBlueprint(this)
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt
index 046855d72..ee00b43f4 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt
@@ -44,9 +44,24 @@ import net.minecraft.util.math.Vec3d
 context(automatedSafeContext: AutomatedSafeContext)
 fun Structure.sim(
 	pov: Vec3d = automatedSafeContext.player.eyePos
-) = BuildSimulator.sim(this, pov)
+) = sim(this, pov)
 
-class BuildSimulator private constructor(
+@JvmName("simExt")
+@SimDsl
+context(automatedSafeContext: AutomatedSafeContext)
+fun sim(
+	structure: Structure,
+	pov: Vec3d = automatedSafeContext.player.eyePos
+): Set =
+	with(automatedSafeContext) {
+		runBlocking(Dispatchers.Default) {
+			val results = ConcurrentSet()
+			with(BuildSimulator(structure, pov, results)) { sim() }
+			results
+		}
+	}
+
+class BuildSimulator internal constructor(
 	private val structure: Structure,
 	private val pov: Vec3d,
 	private val resultsCollection: MutableSet
@@ -66,20 +81,4 @@ class BuildSimulator private constructor(
 			}
 		}
 	}
-
-	companion object {
-		@SimDsl
-		context(automatedSafeContext: AutomatedSafeContext)
-		internal fun sim(
-			structure: Structure,
-			pov: Vec3d = automatedSafeContext.player.eyePos
-		): Set =
-			with(automatedSafeContext) {
-				runBlocking(Dispatchers.Default) {
-					val results = ConcurrentSet()
-					with(BuildSimulator(structure, pov, results)) { sim() }
-					results
-				}
-			}
-	}
 }
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt
index 2ecda1e9b..4b6ee893d 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt
@@ -21,7 +21,7 @@ import com.lambda.context.AutomatedSafeContext
 import com.lambda.interaction.construction.simulation.processing.PreProcessingData
 import com.lambda.interaction.construction.simulation.result.BuildResult
 import com.lambda.interaction.construction.simulation.result.results.GenericResult
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
 import com.lambda.util.math.distSq
 import com.lambda.util.math.vec3d
 import com.lambda.util.player.CheckedHit
@@ -84,7 +84,7 @@ abstract class Sim : Results {
         dependencyStack.pop()
     }
 
-    protected abstract suspend fun AutomatedSafeContext.sim()
+    abstract suspend fun AutomatedSafeContext.sim()
 
     /**
      * Scans a [voxelShape] on the given [sides] at the [pos] from the [pov].
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
index 2222f6791..d3b142bcf 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt
@@ -19,8 +19,8 @@ package com.lambda.interaction.construction.simulation.context
 
 import com.lambda.context.Automated
 import com.lambda.graphics.mc.RenderBuilder
-import com.lambda.interaction.managers.rotating.RotationRequest
-import com.lambda.interaction.material.StackSelection
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.manager.managers.rotating.RotationRequest
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.emptyState
 import net.minecraft.block.BlockState
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BuildContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BuildContext.kt
index 676d99b47..de4669021 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BuildContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BuildContext.kt
@@ -20,7 +20,7 @@ package com.lambda.interaction.construction.simulation.context
 import com.lambda.config.blocks.ActionConfig
 import com.lambda.context.Automated
 import com.lambda.interaction.construction.simulation.result.Drawable
-import com.lambda.interaction.managers.rotating.RotationRequest
+import com.lambda.interaction.manager.managers.rotating.RotationRequest
 import com.lambda.threading.runSafe
 import net.minecraft.block.BlockState
 import net.minecraft.util.hit.BlockHitResult
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
index a431da618..6ab4617b6 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt
@@ -20,8 +20,8 @@ package com.lambda.interaction.construction.simulation.context
 import com.lambda.context.Automated
 import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.interaction.construction.simulation.processing.PreProcessingInfo
-import com.lambda.interaction.managers.interacting.InteractRequest
-import com.lambda.interaction.managers.rotating.RotationRequest
+import com.lambda.interaction.manager.managers.interacting.InteractRequest
+import com.lambda.interaction.manager.managers.rotating.RotationRequest
 import net.minecraft.block.BlockState
 import net.minecraft.util.hit.BlockHitResult
 import net.minecraft.util.math.BlockPos
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/FarmlandStateProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/FarmlandStateProcessor.kt
index b4586a199..c6817df2f 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/FarmlandStateProcessor.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/FarmlandStateProcessor.kt
@@ -75,8 +75,8 @@ object FarmlandStateProcessor : StateProcessor {
 	}
 
 	private fun ClientPlayerEntity.findHoe(): Item? =
-		mainHandStack.item.takeIf { it in ItemUtils.hoes }
-			?: hotbarAndInventoryStacks.firstOrNull { it.item in ItemUtils.hoes }?.item
+		mainHandStack.item.takeIf { it in ItemUtils.HOES }
+			?: hotbarAndInventoryStacks.firstOrNull { it.item in ItemUtils.HOES }?.item
 
 	private fun ClientPlayerEntity.carries(item: Item) =
 		hotbarAndInventoryStacks.any { it.item == item }
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Contextual.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Contextual.kt
index 9f25ed94f..fd67a73dd 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Contextual.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Contextual.kt
@@ -21,8 +21,8 @@ import com.lambda.config.blocks.ActionConfig
 import com.lambda.interaction.construction.simulation.context.BreakContext
 import com.lambda.interaction.construction.simulation.context.BuildContext
 import com.lambda.interaction.construction.simulation.context.InteractContext
-import com.lambda.interaction.managers.hotbar.HotbarManager
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils
 import net.minecraft.block.Blocks
@@ -36,8 +36,8 @@ interface Contextual : ComparableResult {
     override fun compareResult(other: ComparableResult) = runSafe {
         when (other) {
             is Contextual -> compareBy {
-                if (it is InteractContext) BlockUtils.fluids.indexOf(it.cachedState.fluidState.fluid)
-                else BlockUtils.fluids.size - 1
+                if (it is InteractContext) BlockUtils.FLUIDS.indexOf(it.cachedState.fluidState.fluid)
+                else BlockUtils.FLUIDS.size - 1
             }.thenByDescending {
                 if (it is InteractContext && it.cachedState.fluidState.level != 0) it.blockPos.y
                 else Int.MIN_VALUE
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Resolvable.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Resolvable.kt
index 533123ef0..496faa8cc 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Resolvable.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Resolvable.kt
@@ -17,13 +17,13 @@
 
 package com.lambda.interaction.construction.simulation.result
 
-import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.Automated
 import com.lambda.task.Task
 
 /**
  * Represents a [BuildResult] with a resolvable [Task]
  */
 interface Resolvable {
-    context(task: Task<*>, _: AutomatedSafeContext)
-    fun resolve()
+    context(_: Automated)
+    fun resolve(): Task<*>
 }
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
index 58b684006..ce2cb1f35 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt
@@ -19,7 +19,7 @@ package com.lambda.interaction.construction.simulation.result.results
 
 import baritone.api.pathing.goals.GoalBlock
 import baritone.api.pathing.goals.GoalInverted
-import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.Automated
 import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.graphics.util.DirectionMask.mask
 import com.lambda.interaction.construction.simulation.context.BreakContext
@@ -31,12 +31,11 @@ import com.lambda.interaction.construction.simulation.result.Drawable
 import com.lambda.interaction.construction.simulation.result.Navigable
 import com.lambda.interaction.construction.simulation.result.Rank
 import com.lambda.interaction.construction.simulation.result.Resolvable
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.handlers.ContainerHandler.transferByTask
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
-import com.lambda.interaction.material.container.containers.HotbarContainer
-import com.lambda.task.Task
-import com.lambda.task.wrappers.softFail
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.task.tasks.transfer
 import net.minecraft.block.BlockState
 import net.minecraft.item.Item
 import net.minecraft.util.math.BlockPos
@@ -118,12 +117,10 @@ sealed class BreakResult : BuildResult() {
     ) : Resolvable, BreakResult() {
         override val rank = Rank.BreakItemCantMine
 
-        context(task: Task<*>, _: AutomatedSafeContext)
-        override fun resolve() {
-            selectStack {
-                isItem(badItem).not()
-            }.transferByTask(HotbarContainer)?.softFail()?.execute(task)
-        }
+        context(_: Automated)
+        override fun resolve() =
+            stackSelection { notItem(badItem) }
+                .transfer(toSelection = HotbarContainer.select())
 
         override fun compareResult(other: ComparableResult) =
             when (other) {
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
index 54b8fd2fd..b02445f0e 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt
@@ -18,7 +18,7 @@
 package com.lambda.interaction.construction.simulation.result.results
 
 import baritone.api.pathing.goals.GoalNear
-import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.Automated
 import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.interaction.construction.simulation.result.BuildResult
 import com.lambda.interaction.construction.simulation.result.ComparableResult
@@ -26,12 +26,11 @@ import com.lambda.interaction.construction.simulation.result.Drawable
 import com.lambda.interaction.construction.simulation.result.Navigable
 import com.lambda.interaction.construction.simulation.result.Rank
 import com.lambda.interaction.construction.simulation.result.Resolvable
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.handlers.ContainerHandler.transferByTask
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.containers.HotbarContainer
-import com.lambda.task.Task
-import com.lambda.task.wrappers.softFail
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.task.tasks.transfer
 import net.minecraft.client.data.TextureMap.side
 import net.minecraft.item.ItemStack
 import net.minecraft.util.math.BlockPos
@@ -99,10 +98,9 @@ sealed class GenericResult : BuildResult() {
         override val rank = Rank.WrongItem
         private val color = Color(3, 252, 169, 25)
 
-        context(task: Task<*>, _: AutomatedSafeContext)
-        override fun resolve() {
-            neededSelection.transferByTask(HotbarContainer)?.softFail()?.execute(task)
-        }
+        context(_: Automated)
+        override fun resolve() =
+            neededSelection.transfer(toSelection = HotbarContainer.select())
 
         override fun RenderBuilder.render() {
             val center = pos.toCenterPos()
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
index 6c45686a3..c0946491d 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt
@@ -27,7 +27,7 @@ import com.lambda.interaction.construction.simulation.result.Dependent
 import com.lambda.interaction.construction.simulation.result.Drawable
 import com.lambda.interaction.construction.simulation.result.Navigable
 import com.lambda.interaction.construction.simulation.result.Rank
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import net.minecraft.block.BlockState
 import net.minecraft.entity.Entity
 import net.minecraft.item.Item
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
index efa0cbaf6..62d15fb0f 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt
@@ -24,7 +24,7 @@ import com.lambda.interaction.construction.simulation.result.ComparableResult
 import com.lambda.interaction.construction.simulation.result.Drawable
 import com.lambda.interaction.construction.simulation.result.Navigable
 import com.lambda.interaction.construction.simulation.result.Rank
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import net.minecraft.block.BlockState
 import net.minecraft.util.math.BlockPos
 import java.awt.Color
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/BreakSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/BreakSim.kt
index 682d3f0b0..69adb7cbb 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/BreakSim.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/BreakSim.kt
@@ -28,15 +28,15 @@ import com.lambda.interaction.construction.simulation.result.results.BreakResult
 import com.lambda.interaction.construction.simulation.result.results.GenericResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.ContainerHandler.findContainersWithMaterial
-import com.lambda.interaction.managers.hotbar.HotbarManager
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.StackSelection.Companion.EVERYTHING
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.StackAndSlot
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta
 import com.lambda.util.BlockUtils.instantBreakable
@@ -138,56 +138,33 @@ class BreakSim internal constructor(simInfo: SimInfo)
 	}
 
 	private fun AutomatedSafeContext.getSwapStack(): Pair? {
-		// Stack size 0 to account for attacking with an empty hand. Empty slots have stack size 0
-		val stackSelection = selectStack(
-			count = 0,
-			sorter = compareByDescending {
-				it.canBreak(CachedBlockPosition(world, pos, false))
-			}.thenByDescending {
-				state.calcItemBlockBreakingDelta(pos, it)
-			}.thenByDescending {
-				it.inventoryIndex == HotbarManager.serverSlot
-			}
-		) {
-			EVERYTHING
-				.andIf(breakConfig.efficientOnly) {
-					isEfficientForBreaking(state)
-				}.andIf(breakConfig.suitableToolsOnly) {
-					isSuitableForBreaking(state)
-				}.andIf(breakConfig.forceSilkTouch) {
-					hasEnchantment(Enchantments.SILK_TOUCH)
-				}.andIf(breakConfig.avoidSilkTouch) {
-					hasEnchantment(Enchantments.SILK_TOUCH).not()
-				}.andIf(breakConfig.forceFortunePickaxe) {
-					hasEnchantment(Enchantments.FORTUNE)
+		val stackSelection =
+			stackSelection(1) {
+				countEmptyAsOne()
+				alwaysWholeStacks()
+				if (breakConfig.efficientOnly) isEfficientForBreaking(state)
+				if (breakConfig.suitableToolsOnly) isSuitableForBreaking(state)
+				if (breakConfig.forceSilkTouch) withEnchantment(Enchantments.SILK_TOUCH)
+				if (breakConfig.avoidSilkTouch) withoutEnchantment(Enchantments.SILK_TOUCH)
+				if (breakConfig.forceFortunePickaxe) withEnchantment(Enchantments.FORTUNE)
+				sortedWith {
+					compareByDescending> {
+						it.stack.canBreak(CachedBlockPosition(world, pos, false))
+					}.thenByDescending {
+						state.calcItemBlockBreakingDelta(pos, it.stack)
+					}.thenByDescending {
+						it.stack.inventoryIndex == HotbarManager.serverSlot
+					}
 				}
-		}
-
-		val silentSwapSelection = selectContainer {
-			ofAnyType(MaterialContainer.Rank.Hotbar)
-		}
-
-		val hotbarCandidates = stackSelection
-			.findContainersWithMaterial(silentSwapSelection)
-			.flatMap { it.matchingStacks(stackSelection) }
-		if (hotbarCandidates.isEmpty()) {
-			result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack))
-			return null
-		}
+			}
 
-		var bestStack = ItemStack.EMPTY
-		var bestBreakDelta = -1f
-		hotbarCandidates.forEach { stack ->
-			val breakDelta = state.calcItemBlockBreakingDelta(pos, stack)
-			if (breakDelta > bestBreakDelta ||
-				(stack == player.mainHandStack && breakDelta >= bestBreakDelta)
-			) {
-				bestBreakDelta = breakDelta
-				bestStack = stack
+		val bestStack = findStack(stackSelection, HotbarContainer.select())
+			?: run {
+				result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack))
+				return null
 			}
-		}
-		return if (bestBreakDelta == -1f) null
-		else Pair(bestStack, stackSelection)
+
+		return Pair(bestStack, stackSelection)
 	}
 
 	private suspend fun AutomatedSafeContext.affectsFluids(): Boolean {
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/InteractSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/InteractSim.kt
index df60f11ff..74eaccae2 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/InteractSim.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/sims/InteractSim.kt
@@ -27,19 +27,17 @@ import com.lambda.interaction.construction.simulation.result.results.GenericResu
 import com.lambda.interaction.construction.simulation.result.results.InteractResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.ContainerHandler.findContainersWithMaterial
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.StackSelection.Companion.select
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotation
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.EntityUtils.getPositionsWithinHitboxXZ
 import com.lambda.util.PlaceDirection
-import com.lambda.util.item.ItemStackUtils.inventoryIndex
 import com.lambda.util.item.ItemUtils.blockItem
 import com.lambda.util.math.MathUtils.floorToInt
 import com.lambda.util.math.minus
@@ -60,7 +58,6 @@ import net.minecraft.entity.Entity
 import net.minecraft.item.BlockItem
 import net.minecraft.item.Item
 import net.minecraft.item.ItemPlacementContext
-import net.minecraft.item.Items
 import net.minecraft.screen.slot.Slot
 import net.minecraft.state.property.Properties
 import net.minecraft.util.Hand
@@ -205,17 +202,20 @@ class InteractSim internal constructor(simInfo: InteractSimInfo)
 			supervisorScope.cancel()
 			return null
 		}
-		val stackSelection = item?.select()?.apply { if (item == Items.AIR) count = 0 }
-			?: StackSelection.selectStack(0, sorter = compareByDescending { it.inventoryIndex == player.inventory.selectedSlot })
-		val containerSelection = selectContainer { ofAnyType(MaterialContainer.Rank.Hotbar) }
-		val container = stackSelection.findContainersWithMaterial(containerSelection).firstOrNull() ?: run {
-			result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack))
-			return null
-		}
-		return stackSelection.filterSlots(container.slots).run {
-			firstOrNull { it.index == player.inventory.selectedSlot }
-				?: firstOrNull()
-		}
+
+		val stackSelection =
+			stackSelection(1) {
+				countEmptyAsOne()
+				alwaysWholeStacks()
+				item?.let { isItem(it) }
+				sortedWith { compareByDescending { it.slot?.index == player.inventory.selectedSlot } }
+			}
+
+		return findSlot(stackSelection, HotbarContainer.select())
+			?: run {
+				result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack))
+				null
+			}
 	}
 
 	private suspend fun AutomatedSafeContext.simRotation(
diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt
index 9f30d19e2..7c5d72c22 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt
@@ -34,7 +34,9 @@ interface StateMatcher {
 
     context(automatedSafeContext: AutomatedSafeContext)
     fun getStack(pos: BlockPos): ItemStack
+
     context(automatedSafeContext: AutomatedSafeContext)
     fun getState(pos: BlockPos): BlockState
+
     fun isEmpty(): Boolean
 }
diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt
index 91cb32184..e2451a63f 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt
@@ -19,7 +19,8 @@ package com.lambda.interaction.construction.verify
 
 import com.lambda.context.AutomatedSafeContext
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.ContainerHandler.findDisposable
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.BlockUtils.emptyState
 import com.lambda.util.BlockUtils.isEmpty
@@ -86,9 +87,12 @@ sealed class TargetState : StateMatcher {
         context(automatedSafeContext: AutomatedSafeContext)
         override fun getStack(pos: BlockPos) =
             with(automatedSafeContext) {
-                findDisposable()?.stacks?.firstOrNull {
-                    it.item in inventoryConfig.disposables && it.item.block !in replace
-                } ?: ItemStack(Items.NETHERRACK)
+                findStack(
+                    stackSelection {
+                        ofAnyItems(inventoryConfig.disposables)
+                        predicate { stack, _ -> stack.item.block !in replace }
+                    }
+                ) ?: ItemStack(Items.NETHERRACK)
             }
 
         context(_: AutomatedSafeContext)
@@ -113,9 +117,8 @@ sealed class TargetState : StateMatcher {
         context(automatedSafeContext: AutomatedSafeContext)
         override fun getStack(pos: BlockPos) =
             with(automatedSafeContext) {
-                findDisposable()?.stacks?.firstOrNull {
-                    it.item in inventoryConfig.disposables
-                } ?: ItemStack(Items.NETHERRACK)
+                findStack(stackSelection { ofAnyItems(inventoryConfig.disposables) })
+                    ?: ItemStack(Items.NETHERRACK)
             }
 
         context(_: AutomatedSafeContext)
@@ -176,12 +179,12 @@ sealed class TargetState : StateMatcher {
             ignoredProperties: Collection>
         ) = state.block == block
 
-        context(automatedSafeContext: AutomatedSafeContext)
+        context(_: AutomatedSafeContext)
         override fun getStack(pos: BlockPos): ItemStack = itemStack
 
         context(_: AutomatedSafeContext)
         override fun getState(pos: BlockPos): BlockState = block.defaultState
 
-        override fun isEmpty() = false
+        override fun isEmpty() = block.defaultState.isEmpty
     }
 }
diff --git a/src/main/kotlin/com/lambda/interaction/container/Container.kt b/src/main/kotlin/com/lambda/interaction/container/Container.kt
new file mode 100644
index 000000000..ed2875296
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/Container.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.context.Automated
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
+import com.lambda.event.EventFlow.post
+import com.lambda.event.events.ContainerEvent
+import com.lambda.interaction.container.containers.CursorContainer
+import com.lambda.interaction.container.containers.external.ShulkerBoxContainer
+import com.lambda.interaction.container.selection.StackAndSlot
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.mutate
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
+import com.lambda.interaction.manager.managers.inventory.InventoryRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.util.Nameable
+import com.lambda.util.item.ItemStackUtils.count
+import com.lambda.util.item.ItemStackUtils.emptySpace
+import com.lambda.util.item.ItemStackUtils.shulkerBoxStacks
+import com.lambda.util.item.ItemStackUtils.spaceLeft
+import com.lambda.util.item.ItemUtils.SHULKER_BOXES
+import com.lambda.util.item.ItemUtils.toItemCount
+import com.lambda.util.text.TextBuilder
+import com.lambda.util.text.TextDsl
+import com.lambda.util.text.buildText
+import com.lambda.util.text.highlighted
+import com.lambda.util.text.literal
+import com.lambda.util.text.text
+import com.lambda.util.world.Dimension
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.slot.Slot
+import net.minecraft.text.Text
+
+@Suppress("unused")
+abstract class Container(
+    val type: ContainerType
+) : Nameable, Comparable {
+    override val name: String
+        get() = description.string
+
+    abstract val slots: List
+    abstract var stacks: List
+    val storedContainers = mutableMapOf()
+
+    open val dimension = Dimension.Unknown
+
+    open val swapMethodPriority = 0
+    abstract val description: Text
+
+    context(automated: Automated)
+    open val replaceSorter
+        get() = compareByDescending> {
+            it.stack.isEmpty
+        }.thenByDescending {
+            it.stack.item in automated.inventoryConfig.disposables
+        }.thenByDescending {
+            !it.stack.item.components.contains(DataComponentTypes.TOOL)
+        }.thenByDescending {
+            !it.stack.item.components.contains(DataComponentTypes.FOOD)
+        }.thenByDescending {
+            !it.stack.item.components.contains(DataComponentTypes.CONSUMABLE)
+        }.thenByDescending {
+            it.stack.isStackable
+        }
+
+    open val isAccessed get() = true
+
+    context(_: SafeContext)
+    fun descriptionAndStock(selection: StackSelection) =
+        buildText {
+            text(description)
+            stock(selection)
+        }
+
+    @TextDsl
+    context(_: SafeContext)
+    fun TextBuilder.stock(selection: StackSelection) {
+        literal("\n")
+        literal("Contains ")
+        val available = count(selection)
+        highlighted(if (available == Int.MAX_VALUE) "∞" else available.toItemCount())
+        literal(" of ")
+        highlighted("${selection.optimalStack?.name?.string}")
+        literal("\n")
+        literal("Could store ")
+        val left = spaceLeft(selection)
+        highlighted(if (left == Int.MAX_VALUE) "∞" else left.toItemCount())
+        literal(" of ")
+        highlighted("${selection.optimalStack?.name?.string}")
+    }
+
+    fun update(slots: List) {
+        this.stacks = slots
+    }
+
+    fun scanStacksForNestedContainers() {
+        stacks.forEachIndexed { index, stack ->
+            if (stack.item in SHULKER_BOXES) {
+                storedContainers[index] =
+                    ShulkerBoxContainer(
+                        stack.name.string,
+                        stack.item,
+                        stack.shulkerBoxStacks,
+                        this,
+                        index
+                    ).also { it.scanStacksForNestedContainers() }
+            } else storedContainers.remove(index)
+        }
+    }
+
+    @Ta5kBuilder
+    context(automated: Automated)
+    open fun access(): OpenContainerTask<*>? = null
+
+    open fun count(selection: StackSelection) =
+		slots
+			.takeUnless { it.isEmpty() }
+			?.let {
+				selection.filter(it, false).count(selection.countEmptyAsOne)
+			} ?: selection.filter(stacks, false).count(selection.countEmptyAsOne)
+
+    open fun spaceLeft(selection: StackSelection) =
+        slots
+            .takeUnless { it.isEmpty() }
+            ?.let {
+                selection.filter(it, false).spaceLeft + selection.emptySpace(it)
+            } ?: (selection.filter(stacks, false).spaceLeft + stacks.emptySpace)
+
+    open fun findSlots(selection: StackSelection) = selection.filter(slots)
+
+    fun findSlot(selection: StackSelection) = findSlots(selection).firstOrNull()
+
+    open fun findStacks(selection: StackSelection) = selection.filter(stacks)
+
+    fun findStack(selection: StackSelection) = findStacks(selection).firstOrNull()
+
+    context(automated: Automated)
+    open val isAccessible: Boolean
+        get() = type in automated.inventoryConfig.allowedContainers
+
+    open fun requiredContainers(toContainer: Container): List =
+        if (swapMethodPriority == 0 && toContainer.swapMethodPriority == 0) listOf(CursorContainer)
+        else emptyList()
+
+    context(_: Automated)
+    fun canSwapWith(toContainer: Container): Boolean =
+        requiredContainers(toContainer).all { it.isAccessible }
+
+    context(_: Automated)
+    fun findMoveSlots(
+        selection: StackSelection,
+        toContainer: Container,
+        toStackSelection: StackSelection = StackSelection.EVERYTHING
+    ): Pair {
+        if (!canSwapWith(toContainer)) return Pair(null, null)
+        val fromSlot = selection.bestMatch(slots)
+        val toSlot =
+            fromSlot?.let {
+                toContainer.findReplaceSlot(it.stack, toStackSelection)
+            } ?: toContainer.findReplaceSlot(toStackSelection)
+        val validToSlot =
+            toSlot?.takeIf {
+                it.stack.isEmpty ||
+                        fromSlot == null ||
+                        fromSlot.canInsert(it.stack)
+            }
+        return Pair(fromSlot, validToSlot)
+    }
+
+    context(_: Automated)
+    open fun findReplaceSlot(
+        selection: StackSelection = StackSelection.EVERYTHING
+    ) = selection
+        .mutate { sortedWith(replaceSorter) }
+        .bestMatch(slots)
+
+    context(_: Automated)
+    open fun findReplaceSlot(
+        incomingStack: ItemStack,
+        selection: StackSelection = StackSelection.EVERYTHING
+    ) = findReplaceSlot(selection.mutate { canInsert(incomingStack) })
+
+    context(automatedSafeContext: AutomatedSafeContext)
+    internal fun swapRequest(fromSlot: Slot, toSlot: Slot, toContainer: Container): InventoryRequest? {
+        if (!canSwapWith(toContainer)) return null
+        val transferEvent = ContainerEvent.Transfer(fromSlot, toSlot, this@Container, toContainer)
+        if (transferEvent.post().isCanceled()) return null
+        return automatedSafeContext.inventoryRequest {
+            if (swapMethodPriority > toContainer.swapMethodPriority) swap(fromSlot, toSlot)
+            else with(toContainer) { swap(toSlot, fromSlot) }
+        }
+    }
+
+    context(automatedSafeContext: AutomatedSafeContext)
+    internal fun swap(fromSlot: Slot, toSlot: Slot, toContainer: Container): Boolean =
+        swapRequest(fromSlot, toSlot, toContainer)?.submit()?.done ?: false
+
+    context(safeContext: SafeContext)
+    protected open fun InvRequestBuilder.swap(fromHere: Slot, toSlot: Slot) {
+        if (fromHere.stack.isEmpty) moveSlot(toSlot.id, fromHere.id)
+        else {
+            moveSlot(fromHere.id, toSlot.id)
+            if (!toSlot.stack.isEmpty) pickup(fromHere.id)
+        }
+    }
+
+    override fun compareTo(other: Container) =
+        compareBy { it.type }
+            .compare(this, other)
+
+    open fun haveMatchingInventories(other: Container) =
+        this == other ||
+                (this is PlacedContainer &&
+                        other is PlacedContainer &&
+                        this::class == other::class &&
+                        dimension == other.dimension &&
+                        pos == other.pos)
+}
diff --git a/src/main/kotlin/com/lambda/interaction/container/ContainerDslMarker.kt b/src/main/kotlin/com/lambda/interaction/container/ContainerDslMarker.kt
new file mode 100644
index 000000000..9269e464e
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/ContainerDslMarker.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+@DslMarker
+annotation class ContainerDslMarker
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/ContainerSerializer.kt b/src/main/kotlin/com/lambda/interaction/container/ContainerSerializer.kt
new file mode 100644
index 000000000..ba46de0a6
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/ContainerSerializer.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.Lambda.LOG
+import com.lambda.Lambda.mapper
+import com.lambda.Lambda.mc
+import com.lambda.interaction.container.containers.external.EnderChestContainer
+import com.lambda.util.FolderRegistry
+import com.lambda.util.StringUtils.sanitizeForFilename
+import com.lambda.util.extension.dim
+import com.lambda.util.world.Dimension
+import net.minecraft.util.WorldSavePath
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.ChunkPos
+import java.net.InetSocketAddress
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.*
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.exists
+import kotlin.io.path.notExists
+
+object ContainerSerializer {
+	private const val ENDER_CHEST_FILE = "ender_chest.json"
+
+	private val index =
+		mutableMapOf>>>()
+
+	val serializedContainers =
+		sequence {
+			val worldDir = currentWorldDirectory() ?: return@sequence
+			ensureIndex()
+
+			index.forEach { (dimension, chunks) ->
+				val dimensionDir = worldDir.resolve(dimension.name)
+				chunks.forEach { (_, byType) ->
+					byType.forEach { (type, positions) ->
+						positions.forEach { pos ->
+							val file = dimensionDir.resolve(containerFileName(pos, type))
+							readContainer(file)?.let { yield(it) }
+						}
+					}
+				}
+			}
+		}
+
+	@Volatile
+	private var indexed = false
+	private var indexedDirectory: Path? = null
+
+	fun serializedByDimensions(vararg dimensions: Dimension) =
+		sequence {
+			val worldDir = currentWorldDirectory() ?: return@sequence
+			ensureIndex()
+
+			index.forEach { (dimension, chunks) ->
+				if (dimension !in dimensions) return@forEach
+				val dimensionDir = worldDir.resolve(dimension.name)
+				chunks.forEach { (_, byType) ->
+					byType.forEach { (type, positions) ->
+						positions.forEach { pos ->
+							val file = dimensionDir.resolve(containerFileName(pos, type))
+							readContainer(file)?.let { yield(it) }
+						}
+					}
+				}
+			}
+		}
+
+	fun prepareIndex() {
+		synchronized(this) {
+			index.clear()
+			indexed = false
+			indexedDirectory = null
+		}
+		ensureIndex()
+	}
+
+	fun reset() {
+		synchronized(this) {
+			index.clear()
+			indexed = false
+			indexedDirectory = null
+		}
+		clearEnderChest()
+	}
+
+	fun savedIn(chunk: ChunkPos): Map {
+		val dimension = mc.world?.dim ?: return emptyMap()
+		return savedIn(dimension, chunk)
+	}
+
+	fun savedIn(dimension: Dimension, chunk: ChunkPos): Map {
+		ensureIndex()
+		val byType = index[dimension]?.get(chunk) ?: return emptyMap()
+		return buildMap {
+			byType.forEach { (type, positions) ->
+				positions.forEach { put(it, type) }
+			}
+		}
+	}
+
+	@Synchronized
+	fun saveContainer(container: PlacedContainer) {
+		ensureIndex()
+		val worldDir = currentWorldDirectory() ?: return
+		val dir = ensureDirectory(worldDir.resolve(container.dimension.name)) ?: return
+		val file = dir.resolve(containerFileName(container.pos, container.type))
+		mapper.writeValue(file, container)
+		record(container.dimension, container.pos, container.type)
+	}
+
+	@Synchronized
+	fun removeContainer(pos: BlockPos) {
+		val worldDir = currentWorldDirectory() ?: return
+		val dimension = mc.world?.dim ?: return
+		ensureIndex()
+		val type = removeRecord(dimension, pos) ?: return
+		worldDir.resolve(dimension.name).resolve(containerFileName(pos, type)).deleteIfExists()
+	}
+
+	fun saveEnderChest() {
+		val dir = ensureDirectory(currentWorldDirectory()) ?: return
+		val file = dir.resolve(ENDER_CHEST_FILE).toFile()
+		mapper.writeValue(file, EnderChestContainer)
+	}
+
+	fun loadEnderChest() {
+		clearEnderChest()
+		val dir = currentWorldDirectory() ?: return
+		val file = dir.resolve(ENDER_CHEST_FILE)
+		if (!file.exists()) return
+		runCatching {
+			mapper.readValue(file.toFile(), EnderChestContainer::class.java)
+		}.onFailure {
+			LOG.warn("Failed to load ender chest from $file", it)
+		}
+	}
+
+	private fun clearEnderChest() {
+		EnderChestContainer.update(emptyList())
+		EnderChestContainer.storedContainers.clear()
+	}
+
+	private fun ensureDirectory(dir: Path?): Path? {
+		if (dir == null) return null
+		if (dir.notExists()) dir.createDirectories()
+		return dir
+	}
+
+	private fun currentWorldDirectory(): Path? {
+		if (mc.world == null) return null
+		val name =
+			mc.server
+				?.getSavePath(WorldSavePath.ROOT)
+				?.normalize()
+				?.fileName
+				?.toString()
+				?: mc.server?.saveProperties?.levelName
+				?: mc.currentServerEntry?.address
+				?: (mc.networkHandler?.connection?.address as? InetSocketAddress)?.hostName
+				?: "unknown"
+		val folder = name
+			.sanitizeForFilename()
+			.takeUnless {
+				it.isBlank() ||
+						it == "." ||
+						it == ".."
+			}
+			?: "unknown"
+		return FolderRegistry.containers.resolve(folder)
+	}
+
+	private fun ensureIndex() {
+		val worldDir = currentWorldDirectory() ?: return
+		if (indexed && indexedDirectory == worldDir) return
+		synchronized(this) {
+			if (indexed && indexedDirectory == worldDir) return
+			index.clear()
+			indexed = false
+			indexedDirectory = worldDir
+
+			if (worldDir.notExists()) {
+				indexed = true
+				return
+			}
+			val dimensionDirectories =
+				runCatching {
+					Files.newDirectoryStream(worldDir).use { stream ->
+						stream.filter { Files.isDirectory(it) }
+					}
+				}.onFailure {
+					LOG.warn("Failed to list saved container dimensions", it)
+				}.getOrDefault(emptyList())
+
+			var migrated = 0
+			dimensionDirectories.forEach dimensionLoop@{ dir ->
+				val dimension = Dimension.entries.firstOrNull { it.name == dir.fileName.toString() }
+					?: return@dimensionLoop
+				val files =
+					runCatching {
+						Files.newDirectoryStream(dir, "*.json").use { stream -> stream.toList() }
+					}.onFailure {
+						LOG.warn("Failed to list saved containers in ${dir.fileName}", it)
+					}.getOrDefault(emptyList())
+
+				files.forEach fileLoop@{ path ->
+					runCatching {
+						val name = path.fileName.toString()
+						if (name == ENDER_CHEST_FILE) return@fileLoop
+						val parts = name.removeSuffix(".json").split('_')
+						val chunkX = parts[0].toIntOrNull() ?: return@fileLoop
+						val chunkZ = parts[1].toIntOrNull() ?: return@fileLoop
+						val pos =
+							BlockPos(
+								parts.getOrNull(2)?.toIntOrNull() ?: return@fileLoop,
+								parts.getOrNull(3)?.toIntOrNull() ?: return@fileLoop,
+								parts.getOrNull(4)?.toIntOrNull() ?: return@fileLoop
+							)
+						if (chunkX != pos.x shr 4 || chunkZ != pos.z shr 4) return@fileLoop
+
+						val named = parts
+							.getOrNull(5)
+							?.let { token ->
+								ContainerType.entries.firstOrNull { it.name == token }
+							}
+						val type = named
+							?: run {
+								val stored = readContainer(path)
+								if (stored == null) {
+									LOG.warn("Dropping unreadable container file $name")
+									Files.deleteIfExists(path)
+									return@fileLoop
+								}
+								Files.move(path, dir.resolve(containerFileName(pos, stored.type)))
+								migrated++
+								stored.type
+							}
+						record(dimension, pos, type)
+					}.onFailure {
+						LOG.warn("Failed to index saved container $path", it)
+					}
+				}
+			}
+			if (migrated > 0) LOG.info("Named $migrated saved container(s) with their container type")
+			indexed = true
+		}
+	}
+
+	private fun readContainer(file: Path) =
+		runCatching {
+			mapper.readValue(file.toFile(), PlacedContainer::class.java)
+		}.onFailure {
+			LOG.warn("Failed to read container from ${file.fileName}", it)
+		}.getOrNull()
+
+	private fun record(dimension: Dimension, pos: BlockPos, type: ContainerType) {
+		index.getOrPut(dimension) { HashMap() }
+			.getOrPut(ChunkPos(pos.x shr 4, pos.z shr 4)) { EnumMap(ContainerType::class.java) }
+			.getOrPut(type) { HashSet() }
+			.add(pos)
+	}
+
+	private fun removeRecord(dimension: Dimension, pos: BlockPos): ContainerType? {
+		val chunk = ChunkPos(pos.x shr 4, pos.z shr 4)
+		val chunks = index[dimension] ?: return null
+		val byType = chunks[chunk] ?: return null
+		val entry = byType.entries.firstOrNull { pos in it.value } ?: return null
+		entry.value.remove(pos)
+		if (entry.value.isEmpty()) byType.remove(entry.key)
+		if (byType.isEmpty()) chunks.remove(chunk)
+		if (chunks.isEmpty()) index.remove(dimension)
+		return entry.key
+	}
+
+	private fun containerFileName(pos: BlockPos, type: ContainerType): String {
+		val chunkX = pos.x shr 4
+		val chunkZ = pos.z shr 4
+		return "${chunkX}_${chunkZ}_${pos.x}_${pos.y}_${pos.z}_${type.name}.json"
+	}
+}
diff --git a/src/main/kotlin/com/lambda/interaction/container/ContainerType.kt b/src/main/kotlin/com/lambda/interaction/container/ContainerType.kt
new file mode 100644
index 000000000..cfc76c5f5
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/ContainerType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+enum class ContainerType {
+	OffHand,
+	Hotbar,
+	Cursor,
+	Inventory,
+	Armor,
+	Creative,
+	ShulkerBox,
+	EnderChest,
+	PlacedShulkerBox,
+	PlacedEnderChest,
+	Chest,
+	Stash
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/context/IAutomatedSafeContext.kt b/src/main/kotlin/com/lambda/interaction/container/ExternalContainer.kt
similarity index 89%
rename from src/main/kotlin/com/lambda/context/IAutomatedSafeContext.kt
rename to src/main/kotlin/com/lambda/interaction/container/ExternalContainer.kt
index a41eee489..500d2f6e2 100644
--- a/src/main/kotlin/com/lambda/context/IAutomatedSafeContext.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/ExternalContainer.kt
@@ -15,6 +15,6 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.context
+package com.lambda.interaction.container
 
-interface IAutomatedSafeContext : SafeContext, Automated
\ No newline at end of file
+interface ExternalContainer
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/NestedContainer.kt b/src/main/kotlin/com/lambda/interaction/container/NestedContainer.kt
new file mode 100644
index 000000000..3b18a6700
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/NestedContainer.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.context.Automated
+
+abstract class NestedContainer(type: ContainerType) : Container(type), ExternalContainer {
+	abstract val containedIn: Container
+	abstract val index: Int
+
+	override val dimension get() = containedIn.dimension
+
+	context(_: Automated)
+	override val isAccessible: Boolean
+		get() = super.isAccessible && containedIn.isAccessible
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/ExternalContainer.kt b/src/main/kotlin/com/lambda/interaction/container/OpenContainerTask.kt
similarity index 70%
rename from src/main/kotlin/com/lambda/interaction/material/container/ExternalContainer.kt
rename to src/main/kotlin/com/lambda/interaction/container/OpenContainerTask.kt
index 7bad81182..27cc94c61 100644
--- a/src/main/kotlin/com/lambda/interaction/material/container/ExternalContainer.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/OpenContainerTask.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.material.container
+package com.lambda.interaction.container
 
-import com.lambda.context.AutomatedSafeContext
 import com.lambda.task.Task
-import com.lambda.task.wrappers.TaskSupplier
+import net.minecraft.text.Text
 
-interface ExternalContainer {
-	context(_: AutomatedSafeContext)
-	fun accessThen(exitAfter: Boolean = true, taskSupplier: TaskSupplier): Task<*>?
+abstract class OpenContainerTask @Ta5kBuilder internal constructor(
+	description: Text
+) : Task() {
+	override val name = "accessing container: ${description.string}"
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/OpenedContainerContext.kt b/src/main/kotlin/com/lambda/interaction/container/OpenedContainerContext.kt
new file mode 100644
index 000000000..dfb78544b
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/OpenedContainerContext.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.context.Automated
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.wrappers.actionTask
+
+interface OpenedContainerContext {
+	@Ta5kBuilder
+	context(automated: Automated)
+	fun close(): Task<*>?
+}
+
+open class BasicOpenedContainerContext(
+	private val isAccessed: () -> Boolean
+) : OpenedContainerContext {
+	context(_: Automated)
+	final override fun close() =
+		if (!isAccessed()) null
+		else actionTask { player.closeHandledScreen() }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/PlacedContainer.kt b/src/main/kotlin/com/lambda/interaction/container/PlacedContainer.kt
new file mode 100644
index 000000000..2383ae0af
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/PlacedContainer.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.interaction.container.containers.external.StashContainer
+import com.lambda.util.world.Dimension
+import net.minecraft.util.math.BlockPos
+
+abstract class PlacedContainer(
+	type: ContainerType,
+	override val dimension: Dimension
+) : Container(type), ExternalContainer {
+	abstract val pos: BlockPos
+	abstract val stash: StashContainer?
+	open val positions: Set = setOf(pos)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/SlotIdentity.kt b/src/main/kotlin/com/lambda/interaction/container/SlotIdentity.kt
new file mode 100644
index 000000000..5753ef868
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/SlotIdentity.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container
+
+import com.lambda.context.SafeContext
+import com.lambda.interaction.handler.handlers.ContainerHandler
+import net.minecraft.block.entity.EnderChestBlockEntity
+import net.minecraft.entity.player.PlayerInventory
+import net.minecraft.inventory.SimpleInventory
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+
+sealed interface SlotIdentity {
+	data class Player(val index: Int) : SlotIdentity
+	data class Placed(val positions: Set, val index: Int) : SlotIdentity
+	data class Ender(val index: Int) : SlotIdentity
+
+	context(safeContext: SafeContext)
+	fun resolve(): Slot? {
+		val handler = safeContext.player.currentScreenHandler
+		val (key, ender) = currentContainerAnchor()
+		return resolveIn(handler.slots, key, ender)
+	}
+
+	fun resolveIn(
+		slots: List,
+		containerKey: Set?,
+		enderOpen: Boolean
+	): Slot? =
+		when (this) {
+			is Player ->
+				slots.firstOrNull { it.inventory is PlayerInventory && it.index == index }
+			is Placed ->
+				if (containerKey != null && containerKey == positions)
+					slots.firstOrNull { it.inventory is SimpleInventory && it.index == index }
+				else null
+			is Ender ->
+				if (enderOpen)
+					slots.firstOrNull { it.inventory is SimpleInventory && it.index == index }
+				else null
+		}
+
+	context(safeContext: SafeContext)
+	fun holds(stack: ItemStack): Boolean {
+		val live = resolve()?.stack ?: return false
+		if (live.isEmpty || stack.isEmpty) return false
+		return live.item == stack.item && live.name.string == stack.name.string
+	}
+}
+
+fun Slot.identity(): SlotIdentity? {
+	if (inventory is PlayerInventory) return SlotIdentity.Player(index)
+	if (inventory !is SimpleInventory) return null
+	val (key, ender) = currentContainerAnchor()
+	if (key != null) return SlotIdentity.Placed(key, index)
+	if (ender) return SlotIdentity.Ender(index)
+	return null
+}
+
+fun currentContainerAnchor(): Pair?, Boolean> =
+	ContainerHandler.accessedPlacedContainer?.positions to
+			(ContainerHandler.lastInteractedBlockEntity is EnderChestBlockEntity)
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/ArmorContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/ArmorContainer.kt
new file mode 100644
index 000000000..2a76c37ab
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/ArmorContainer.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers
+
+import com.lambda.Lambda.mc
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.util.player.SlotUtils.armorSlots
+import com.lambda.util.player.SlotUtils.armorStacks
+import com.lambda.util.text.buildText
+import com.lambda.util.text.literal
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.slot.Slot
+
+object ArmorContainer : Container(ContainerType.Armor) {
+	override val slots: List
+		get() = mc.player?.armorSlots ?: emptyList()
+	override var stacks: List
+		get() = mc.player?.armorStacks ?: emptyList()
+		set(_) {}
+
+	override val description = buildText { literal("Armor") }
+
+	override val isAccessed get() = mc.player?.currentScreenHandler?.syncId == 0
+}
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/CreativeContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/CreativeContainer.kt
new file mode 100644
index 000000000..6f9d1181d
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/CreativeContainer.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers
+
+import com.lambda.Lambda.mc
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
+import com.lambda.util.text.buildText
+import com.lambda.util.text.literal
+import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.inventory.SingleStackInventory
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.PlayerScreenHandler
+import net.minecraft.screen.slot.Slot
+
+@Suppress("unused")
+data object CreativeContainer : Container(ContainerType.Creative) {
+	override val slots = emptyList()
+	override var stacks = emptyList()
+
+	override val swapMethodPriority = 11
+
+	override val description = buildText { literal("Creative") }
+
+	override fun count(selection: StackSelection): Int =
+		if (mc.player?.isCreative == true && correctScreenHandler && selection.optimalStack != null) Int.MAX_VALUE else -1
+
+	override fun spaceLeft(selection: StackSelection): Int =
+		if (mc.player?.isCreative == true && correctScreenHandler && selection.optimalStack != null) Int.MAX_VALUE else -1
+
+	context(safeContext: SafeContext)
+	override fun InvRequestBuilder.swap(fromHere: Slot, toSlot: Slot) {
+		clickCreativeStack(fromHere.stack, toSlot.id)
+		safeContext.player.currentScreenHandler.slots.getOrNull(toSlot.id)?.stack = fromHere.stack
+	}
+
+	override fun findSlots(selection: StackSelection) =
+		listOfNotNull(
+			selection.optimalStack?.let { stack ->
+				Slot(
+					object : SingleStackInventory {
+						override fun getStack() = stack
+						override fun setStack(stack: ItemStack?) {}
+						override fun markDirty() {}
+						override fun canPlayerUse(player: PlayerEntity?) = false
+					},
+					0, 0, 0
+				)
+			}
+		)
+
+	override fun findStacks(selection: StackSelection) =
+		listOfNotNull(selection.optimalStack)
+
+	private val correctScreenHandler
+		get() = mc.player?.currentScreenHandler is PlayerScreenHandler || mc.player?.currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler
+}
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/CursorContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/CursorContainer.kt
new file mode 100644
index 000000000..b19df139d
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/CursorContainer.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers
+
+import com.lambda.Lambda.mc
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
+import com.lambda.util.text.buildText
+import com.lambda.util.text.literal
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.inventory.SingleStackInventory
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.ScreenHandler
+import net.minecraft.screen.slot.Slot
+
+object CursorContainer : Container(ContainerType.Cursor) {
+	override val slots: List
+		get() =
+			mc.player?.currentScreenHandler?.let {
+				listOf(CursorSlot(it))
+			} ?: emptyList()
+
+	override var stacks: List
+		get() =
+			mc.player?.currentScreenHandler?.cursorStack?.let {
+				listOf(it)
+			} ?: emptyList()
+		set(_) {}
+
+	override val swapMethodPriority = 12
+
+	override val description = buildText { literal("Cursor") }
+
+	context(safeContext: SafeContext)
+	override fun InvRequestBuilder.swap(fromHere: Slot, toSlot: Slot) {
+		pickup(toSlot.id, 0)
+	}
+
+	class CursorSlot(val screenHandler: ScreenHandler) : Slot(
+		object : SingleStackInventory {
+			override fun getStack(): ItemStack = screenHandler.cursorStack
+			override fun setStack(stack: ItemStack?) {
+				if (stack != null) screenHandler.cursorStack = stack
+			}
+			override fun markDirty() {}
+			override fun canPlayerUse(player: PlayerEntity?) = true
+		},
+		0, 0, 0
+	) {
+		override fun canInsert(stack: ItemStack): Boolean = true
+		override fun canTakeItems(playerEntity: PlayerEntity?): Boolean = true
+	}
+}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/HotbarContainer.kt
similarity index 71%
rename from src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt
rename to src/main/kotlin/com/lambda/interaction/container/containers/HotbarContainer.kt
index f2dc5d3f4..3e5ba3744 100644
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/HotbarContainer.kt
@@ -15,22 +15,22 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.material.container.containers
+package com.lambda.interaction.container.containers
 
 import com.lambda.Lambda.mc
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
 import com.lambda.util.player.SlotUtils.hotbarSlots
 import com.lambda.util.player.SlotUtils.hotbarStacks
 import com.lambda.util.text.buildText
 import com.lambda.util.text.literal
 import net.minecraft.screen.slot.Slot
 
-object HotbarContainer : MaterialContainer(Rank.Hotbar) {
-    context(safeContext: SafeContext)
+object HotbarContainer : Container(ContainerType.Hotbar) {
     override val slots: List
-        get() = safeContext.player.hotbarSlots
+        get() = mc.player?.hotbarSlots ?: emptyList()
     override var stacks
         get() = mc.player?.hotbarStacks ?: emptyList()
         set(_) {}
@@ -40,7 +40,7 @@ object HotbarContainer : MaterialContainer(Rank.Hotbar) {
     override val description = buildText { literal("Hotbar") }
 
     context(safeContext: SafeContext)
-    override fun InventoryRequest.InvRequestBuilder.transfer(fromHere: Slot, toSlot: Slot) {
-        swap(toSlot.id, safeContext.player.hotbarSlots.indexOf(fromHere))
+    override fun InvRequestBuilder.swap(fromHere: Slot, toSlot: Slot) {
+        swapWithHotbar(toSlot.id, safeContext.player.hotbarSlots.indexOf(fromHere))
     }
 }
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/InventoryContainer.kt
similarity index 78%
rename from src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt
rename to src/main/kotlin/com/lambda/interaction/container/containers/InventoryContainer.kt
index 10867bdb2..f1c56e1cf 100644
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/InventoryContainer.kt
@@ -15,11 +15,11 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.material.container.containers
+package com.lambda.interaction.container.containers
 
 import com.lambda.Lambda.mc
-import com.lambda.context.SafeContext
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
 import com.lambda.util.player.SlotUtils.inventorySlots
 import com.lambda.util.player.SlotUtils.inventoryStacks
 import com.lambda.util.text.buildText
@@ -27,10 +27,9 @@ import com.lambda.util.text.literal
 import net.minecraft.item.ItemStack
 import net.minecraft.screen.slot.Slot
 
-object InventoryContainer : MaterialContainer(Rank.Inventory) {
-    context(safeContext: SafeContext)
+object InventoryContainer : Container(ContainerType.Inventory) {
     override val slots: List
-        get() = safeContext.player.inventorySlots
+        get() = mc.player?.inventorySlots ?: emptyList()
     override var stacks: List
         get() = mc.player?.inventoryStacks ?: emptyList()
         set(_) {}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/OffhandContainer.kt
similarity index 73%
rename from src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt
rename to src/main/kotlin/com/lambda/interaction/container/containers/OffhandContainer.kt
index 2ae235e42..b2b5fefb6 100644
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/OffhandContainer.kt
@@ -15,22 +15,22 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.material.container.containers
+package com.lambda.interaction.container.containers
 
 import com.lambda.Lambda.mc
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
 import com.lambda.util.player.SlotUtils.offHandSlots
 import com.lambda.util.text.buildText
 import com.lambda.util.text.literal
 import net.minecraft.item.ItemStack
 import net.minecraft.screen.slot.Slot
 
-object OffHandContainer : MaterialContainer(Rank.OffHand) {
-    context(safeContext: SafeContext)
+object OffhandContainer : Container(ContainerType.OffHand) {
     override val slots: List
-        get() = safeContext.player.offHandSlots
+        get() = mc.player?.offHandSlots ?: emptyList()
     override var stacks: List
         get() = mc.player?.offHandStack?.let { listOf(it) } ?: emptyList()
         set(_) {}
@@ -40,7 +40,7 @@ object OffHandContainer : MaterialContainer(Rank.OffHand) {
     override val description = buildText { literal("OffHand") }
 
     context(_: SafeContext)
-    override fun InventoryRequest.InvRequestBuilder.transfer(fromHere: Slot, toSlot: Slot) {
-        swap(toSlot.id, 40)
+    override fun InvRequestBuilder.swap(fromHere: Slot, toSlot: Slot) {
+        swapWithHotbar(toSlot.id, 40)
     }
 }
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/external/ChestContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/ChestContainer.kt
new file mode 100644
index 000000000..ae38f8a45
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/ChestContainer.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers.external
+
+import com.lambda.Lambda.mc
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.BasicOpenedContainerContext
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.OpenContainerTask
+import com.lambda.interaction.container.OpenedContainerContext
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.handler.handlers.ContainerHandler.lastInteractedBlockEntity
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.openContainer
+import com.lambda.util.extension.containerSlots
+import com.lambda.util.extension.dim
+import com.lambda.util.player.SlotUtils.typeSafe
+import com.lambda.util.text.buildText
+import com.lambda.util.text.highlighted
+import com.lambda.util.text.literal
+import com.lambda.util.world.Dimension
+import net.minecraft.block.ChestBlock
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.ScreenHandlerType
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+
+data class ChestContainer(
+	override val pos: BlockPos,
+	override var stacks: List,
+	override val stash: StashContainer? = null,
+	override val dimension: Dimension = mc.world?.dim ?: Dimension.Unknown
+) : PlacedContainer(ContainerType.Chest, dimension) {
+    override val slots
+        get(): List =
+            if (isAccessed) mc.player?.currentScreenHandler?.containerSlots ?: emptyList()
+            else emptyList()
+
+    override val description =
+	    buildText {
+		    literal("Chest at ")
+		    highlighted(pos.toShortString())
+		    stash?.let { stash ->
+			    literal(" (contained in ")
+			    highlighted(stash.name)
+			    literal(")")
+		    }
+	    }
+
+	override val isAccessed
+		get() = lastInteractedBlockEntity?.let { blockEntity ->
+			blockEntity.pos == pos &&
+					blockEntity.cachedState.block is ChestBlock &&
+					mc.player?.currentScreenHandler?.typeSafe == ScreenHandlerType.GENERIC_9X3
+		} ?: false
+
+	@Ta5kBuilder
+	context(automated: Automated)
+	override fun access() =
+		if (isAccessed) null
+		else AccessChestTask(automated)
+
+	inner class AccessChestTask @Ta5kBuilder internal constructor(
+		automated: Automated
+	) : OpenContainerTask(description), Automated by automated {
+		override fun SafeContext.onStart() {
+			openContainer(pos)
+				.onSuccess { success(BasicOpenedContainerContext(::isAccessed)) }
+				.start()
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/external/DoubleChestContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/DoubleChestContainer.kt
new file mode 100644
index 000000000..4b5dd9d14
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/DoubleChestContainer.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers.external
+
+import com.lambda.Lambda.mc
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.BasicOpenedContainerContext
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.OpenContainerTask
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.handler.handlers.ContainerHandler.lastInteractedBlockEntity
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.openContainer
+import com.lambda.task.tasks.wrappers.withRecovery
+import com.lambda.util.extension.containerSlots
+import com.lambda.util.extension.dim
+import com.lambda.util.math.distSq
+import com.lambda.util.player.SlotUtils.typeSafe
+import com.lambda.util.text.buildText
+import com.lambda.util.text.highlighted
+import com.lambda.util.text.literal
+import com.lambda.util.world.Dimension
+import net.minecraft.block.ChestBlock
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.ScreenHandlerType
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+
+data class DoubleChestContainer(
+	override val pos: BlockPos,
+	val leftPos: BlockPos,
+	val rightPos: BlockPos,
+	override var stacks: List,
+	override val stash: StashContainer? = null,
+	override var dimension: Dimension = mc.world?.dim ?: Dimension.Unknown
+) : PlacedContainer(ContainerType.Chest, dimension) {
+	override val slots
+		get(): List =
+			if (isAccessed) mc.player?.currentScreenHandler?.containerSlots ?: emptyList()
+			else emptyList()
+
+	override val positions = setOf(leftPos, rightPos)
+
+	override val description =
+		buildText {
+			literal("Double Chest at ")
+			highlighted(leftPos.toShortString())
+			literal(" and ")
+			highlighted(rightPos.toShortString())
+			stash?.let { stash ->
+				literal(" (contained in ")
+				highlighted(stash.name)
+				literal(")")
+			}
+		}
+
+	override val isAccessed
+		get() = lastInteractedBlockEntity?.let { blockEntity ->
+			(blockEntity.pos == leftPos || blockEntity.pos == rightPos) &&
+					blockEntity.cachedState.block is ChestBlock &&
+					mc.player?.currentScreenHandler?.typeSafe == ScreenHandlerType.GENERIC_9X6
+		} ?: false
+
+	@Ta5kBuilder
+	context(automated: Automated)
+	override fun access() =
+		if (isAccessed) null
+		else AccessChestTask(automated)
+
+	inner class AccessChestTask @Ta5kBuilder internal constructor(
+		automated: Automated
+	) : OpenContainerTask(description), Automated by automated {
+		override fun SafeContext.onStart() {
+			val leftDist = leftPos distSq player.pos
+			val rightDist = rightPos distSq player.pos
+			val closestPos = if (leftDist <= rightDist) leftPos else rightPos
+			val farthestPos = if (leftDist > rightDist) leftPos else rightPos
+			openContainer(closestPos)
+				.withRecovery { openContainer(farthestPos) }
+				.onSuccess { success(BasicOpenedContainerContext(::isAccessed)) }
+				.start()
+		}
+	}
+
+	override fun haveMatchingInventories(other: Container) =
+		other is DoubleChestContainer &&
+				dimension == other.dimension &&
+				(other.leftPos == leftPos || other.rightPos == rightPos)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/external/EnderChestContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/EnderChestContainer.kt
new file mode 100644
index 000000000..a187c5cb3
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/EnderChestContainer.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers.external
+
+import com.lambda.Lambda.mc
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.ExternalContainer
+import com.lambda.interaction.container.OpenContainerTask
+import com.lambda.interaction.container.OpenedContainerContext
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.lastInteractedBlockEntity
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.acquireStack
+import com.lambda.task.tasks.breakAndCollect
+import com.lambda.task.tasks.findBlock
+import com.lambda.task.tasks.openContainer
+import com.lambda.task.tasks.placeContainer
+import com.lambda.task.tasks.wrappers.actionTask
+import com.lambda.task.tasks.wrappers.taskOrNull
+import com.lambda.task.tasks.wrappers.then
+import com.lambda.task.tasks.wrappers.thenOrNull
+import com.lambda.task.tasks.wrappers.withBranch
+import com.lambda.util.extension.containerSlots
+import com.lambda.util.player.SlotUtils.typeSafe
+import com.lambda.util.text.buildText
+import com.lambda.util.text.literal
+import net.minecraft.block.Blocks
+import net.minecraft.block.entity.EnderChestBlockEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.screen.ScreenHandlerType
+import net.minecraft.util.math.BlockPos
+
+object EnderChestContainer : Container(ContainerType.EnderChest), ExternalContainer {
+	override val slots
+		get() =
+			if (isAccessed) mc.player?.currentScreenHandler?.containerSlots ?: emptyList()
+			else emptyList()
+	override var stacks = emptyList()
+
+	override val description = buildText { literal("Ender Chest") }
+
+	override val isAccessed
+		get() =
+			lastInteractedBlockEntity?.let { blockEntity ->
+				blockEntity is EnderChestBlockEntity &&
+						mc.player?.currentScreenHandler?.typeSafe == ScreenHandlerType.GENERIC_9X3
+			} ?: false
+
+	@Ta5kBuilder
+	context(automated: Automated)
+	override fun access() =
+		if (isAccessed) null
+		else OpenEnderChestContainerTask(automated)
+
+	class OpenEnderChestContainerTask @Ta5kBuilder internal constructor(
+		automated: Automated
+	) : OpenContainerTask(description), Automated by automated {
+		override fun SafeContext.onStart() {
+			findBlock(Blocks.ENDER_CHEST, inventoryConfig.enderChestSearchRadius).withBranch(
+				onSuccess = { pos ->
+					openContainer(pos).onSuccess {
+						success(OpenedEnderChestContext(null))
+					}
+				},
+				onFailure = {
+					acquireStack(Items.ENDER_CHEST.select())
+						.then { placeContainer(findSlot(it, HotbarContainer.select())) }
+						.then { pos ->
+							openContainer(pos).onSuccess {
+								success(OpenedEnderChestContext(pos))
+							}
+						}
+				}
+			).start()
+		}
+	}
+
+	class OpenedEnderChestContext(
+		val blockPos: BlockPos?
+	) : OpenedContainerContext {
+		context(_: Automated)
+		override fun close() =
+			taskOrNull {
+				if (isAccessed) actionTask { player.closeHandledScreen() }
+				else null
+			}.thenOrNull {
+				if (blockPos != null) breakAndCollect(blockPos)
+				else null
+			}
+	}
+}
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/external/PlacedShulkerBoxContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/PlacedShulkerBoxContainer.kt
new file mode 100644
index 000000000..5fbab6e76
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/PlacedShulkerBoxContainer.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers.external
+
+import com.lambda.Lambda.mc
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.BasicOpenedContainerContext
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.OpenContainerTask
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.handler.handlers.ContainerHandler.lastInteractedBlockEntity
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.openContainer
+import com.lambda.util.extension.containerSlots
+import com.lambda.util.extension.dim
+import com.lambda.util.player.SlotUtils.typeSafe
+import com.lambda.util.text.buildText
+import com.lambda.util.text.highlighted
+import com.lambda.util.text.literal
+import com.lambda.util.world.Dimension
+import net.minecraft.block.Block
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.ScreenHandlerType
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+
+class PlacedShulkerBoxContainer(
+	override val pos: BlockPos,
+	val block: Block,
+	override val stash: StashContainer?,
+	override var stacks: List,
+	override var dimension: Dimension = mc.world?.dim ?: Dimension.Unknown
+) : PlacedContainer(ContainerType.PlacedShulkerBox, dimension) {
+	override val slots: List
+		get() =
+			if (isAccessed) mc.player?.currentScreenHandler?.containerSlots ?: emptyList()
+			else emptyList()
+
+	override val description =
+		buildText {
+			literal("Placed shulker box at ")
+			highlighted(pos.toShortString())
+			stash?.let { stash ->
+				literal(" (contained in ")
+				highlighted(stash.name)
+				literal(")")
+			}
+		}
+
+	override val isAccessed
+		get() = lastInteractedBlockEntity?.let { blockEntity ->
+			blockEntity.pos == pos &&
+					blockEntity.cachedState.block == block &&
+					mc.player?.currentScreenHandler?.typeSafe == ScreenHandlerType.SHULKER_BOX
+		} ?: false
+
+	@Ta5kBuilder
+	context(automated: Automated)
+	override fun access() =
+		if (isAccessed) null
+		else OpenPlacedShulkerBoxTask(automated)
+
+	inner class OpenPlacedShulkerBoxTask @Ta5kBuilder internal constructor(
+		automated: Automated
+	) : OpenContainerTask(description), Automated by automated {
+		override fun SafeContext.onStart() {
+			openContainer(pos)
+				.onSuccess { success(BasicOpenedContainerContext(::isAccessed)) }
+				.start()
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/containers/external/ShulkerBoxContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/ShulkerBoxContainer.kt
new file mode 100644
index 000000000..0ccaca878
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/ShulkerBoxContainer.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.containers.external
+
+import com.lambda.Lambda.mc
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.ExternalContainer
+import com.lambda.interaction.container.NestedContainer
+import com.lambda.interaction.container.OpenContainerTask
+import com.lambda.interaction.container.OpenedContainerContext
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.mutate
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerSearchScope
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.breakAndCollect
+import com.lambda.task.tasks.openContainer
+import com.lambda.task.tasks.placeContainer
+import com.lambda.task.tasks.transfer
+import com.lambda.task.tasks.wrappers.actionTask
+import com.lambda.task.tasks.wrappers.taskOrNull
+import com.lambda.task.tasks.wrappers.then
+import com.lambda.util.extension.containerSlots
+import com.lambda.util.text.buildText
+import com.lambda.util.text.highlighted
+import com.lambda.util.text.literal
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+import java.util.Objects.hash
+
+data class ShulkerBoxContainer(
+	val itemName: String,
+	val shulkerItem: Item,
+	override var stacks: List,
+	override val containedIn: Container,
+	override val index: Int,
+) : NestedContainer(ContainerType.ShulkerBox), ExternalContainer {
+	override val slots
+		get(): List =
+			if (isAccessed) mc.player?.currentScreenHandler?.containerSlots ?: emptyList()
+			else emptyList()
+
+	override val description =
+		buildText {
+			highlighted(itemName)
+			literal(" in ")
+			highlighted(containedIn.name)
+			literal(" in slot index $index")
+		}
+
+	override val isAccessed = false
+
+	@Ta5kBuilder
+	context(automated: Automated)
+	override fun access() = OpenShulkerBoxTask(automated)
+
+	inner class OpenShulkerBoxTask @Ta5kBuilder internal constructor(
+		automated: Automated
+	) : OpenContainerTask(description), Automated by automated {
+		override fun SafeContext.onStart() {
+			transfer(
+				stackSelection {
+					sortedWith { compareByDescending { it.slot?.index == index } }
+					isItem(shulkerItem)
+					withName(itemName)
+					sortedByBestContentMatch(stacks)
+				},
+				containedIn.select(ContainerSearchScope.Loaded),
+				HotbarContainer.select()
+			).then { result ->
+				placeContainer(
+					findSlot(
+						result.stackSelection.mutate {
+							sortedByRecentChange()
+						},
+						result.containerSelection
+					)
+				)
+			}.then { pos ->
+				openContainer(pos).onSuccess {
+					success(OpenedShulkerBoxContext(pos))
+				}
+			}.start()
+		}
+	}
+
+	inner class OpenedShulkerBoxContext(
+		val blockPos: BlockPos,
+	) : OpenedContainerContext {
+		context(automated: Automated)
+		override fun close(): Task<*> {
+			return taskOrNull {
+				if (!isAccessed) null
+				else actionTask { player.closeHandledScreen() }
+			}.then { breakAndCollect(blockPos) }
+				.then { outcome ->
+					val pickedUp = outcome.pickups
+						.asSequence()
+						.filter { it.stack.item == shulkerItem && it.stack.name.string == itemName }
+						.firstNotNullOfOrNull { it.slot }
+
+					stackSelection {
+						isItem(shulkerItem)
+						withName(itemName)
+						if (pickedUp != null) sortedBySlotMatch(pickedUp)
+						sortedByBestContentMatch(stacks)
+						sortedByRecentChange()
+					}.transfer(
+						ContainerSelection.HOTBAR_AND_INVENTORY,
+						containedIn.select(ContainerSearchScope.Loaded),
+						stackSelection {
+							sortedByMatches {
+								stackSelection {
+									predicate { _, slot -> slot?.index == index }
+									isEmpty()
+								}
+							}
+						},
+						allowQuickMove = false
+					)
+				}
+		}
+	}
+
+	override fun hashCode() = hash(containedIn.hashCode(), index)
+
+	override fun equals(other: Any?) =
+		other is ShulkerBoxContainer &&
+				containedIn == other.containedIn &&
+				index == other.index
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt b/src/main/kotlin/com/lambda/interaction/container/containers/external/StashContainer.kt
similarity index 67%
rename from src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt
rename to src/main/kotlin/com/lambda/interaction/container/containers/external/StashContainer.kt
index e84b3ce4d..7d1276ac1 100644
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt
+++ b/src/main/kotlin/com/lambda/interaction/container/containers/external/StashContainer.kt
@@ -15,11 +15,11 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.material.container.containers
+package com.lambda.interaction.container.containers.external
 
-import com.lambda.context.SafeContext
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.MaterialContainer
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.selection.StackSelection
 import com.lambda.util.math.roundedBlockPos
 import com.lambda.util.text.buildText
 import com.lambda.util.text.highlighted
@@ -31,22 +31,19 @@ import net.minecraft.util.math.Box
 data class StashContainer(
     val chests: Set,
     val pos: Box,
-) : MaterialContainer(Rank.Stash) {
-    context(_: SafeContext)
+) : Container(ContainerType.Stash) {
     override val slots: List
         get() = chests.flatMap { it.slots }
     override var stacks: List
         get() = chests.flatMap { it.stacks }
         set(_) {}
 
-    override val description = buildText {
-        literal("Stash at ")
-        highlighted(pos.center.roundedBlockPos.toShortString())
-    }
-
-    context(_: SafeContext)
-    override fun materialAvailable(selection: StackSelection): Int =
-        chests.sumOf {
-            it.materialAvailable(selection)
+    override val description =
+        buildText {
+            literal("Stash at ")
+            highlighted(pos.center.roundedBlockPos.toShortString())
         }
+
+    override fun count(selection: StackSelection): Int =
+        chests.sumOf { it.count(selection) }
 }
diff --git a/src/main/kotlin/com/lambda/interaction/container/selection/ContainerSelection.kt b/src/main/kotlin/com/lambda/interaction/container/selection/ContainerSelection.kt
new file mode 100644
index 000000000..646e42925
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/selection/ContainerSelection.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.container.selection
+
+import com.lambda.Lambda.mc
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerDslMarker
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.NestedContainer
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.handler.handlers.ContainerSearchScope
+import com.lambda.util.extension.dim
+import com.lambda.util.world.Dimension
+
+@ContainerDslMarker
+fun Container.select(
+    scope: ContainerSearchScope = ContainerSearchScope.Loaded
+) = containerSelection(scope) { ofAny(this@select) }
+
+@ContainerDslMarker
+fun Iterable.select(
+    scope: ContainerSearchScope = ContainerSearchScope.Loaded
+) = containerSelection(scope) { ofAny(this@select) }
+
+@ContainerDslMarker
+fun selectContainers(
+    vararg containers: Container,
+    scope: ContainerSearchScope = ContainerSearchScope.Loaded
+) = containerSelection(scope) { ofAny(*containers) }
+
+fun containerSelection(
+    scope: ContainerSearchScope = ContainerSearchScope.Loaded,
+    builder: ContainerSelectionBuilder.() -> Unit = {}
+) = ContainerSelectionBuilder(scope).apply(builder).build()
+
+fun ContainerSelection.mutate(
+    scope: ContainerSearchScope = this.scope,
+    builder: ContainerSelectionBuilder.() -> Unit = {}
+) = ContainerSelectionBuilder(this, scope).apply(builder).build()
+
+@Suppress("unused")
+class ContainerSelection @ContainerDslMarker internal constructor(
+    val selector: (Container) -> Boolean = { true },
+    val scope: ContainerSearchScope = ContainerSearchScope.Loaded,
+    val containersWhitelist: Collection = emptyList(),
+    val loadedContainers: Collection = emptyList(),
+    val containersBlacklist: Collection = emptyList(),
+    val accessScope: AccessScope = AccessScope.Both,
+    val comparator: Comparator = compareBy { it.type },
+    val dimensionWhitelist: Set = emptySet(),
+    val dimensionBlacklist: Set = emptySet()
+) {
+    @ContainerDslMarker
+    fun bestMatch(containers: Iterable) = filter(containers).firstOrNull()
+
+    @ContainerDslMarker
+    fun matches(container: Container): Boolean = selector(container)
+
+    @ContainerDslMarker
+    fun filter(containers: Iterable, sorted: Boolean = true) =
+        run {
+            containersWhitelist
+                .takeIf { it.isNotEmpty() }
+                ?.asSequence()
+                ?.filter { it in containers && (accessScope.accessed?.equals(it.isAccessed) != false) }
+                ?: containers.asSequence()
+        }.filter { it !in containersBlacklist }
+            .filter(::matches)
+            .let {
+                if (sorted) it.sortedWith(comparator)
+                else it
+            }
+            .toList()
+
+    companion object {
+        val ACCESSED = ContainerSelection({ true })
+        val NOTHING = ContainerSelection({ false })
+        val HOTBAR_AND_INVENTORY =
+            selectContainers(
+                HotbarContainer,
+                InventoryContainer,
+                scope = ContainerSearchScope.Player
+            )
+        val PLAYER = ContainerSelection(scope = ContainerSearchScope.Player)
+    }
+}
+
+@Suppress("unused")
+@ContainerDslMarker
+class ContainerSelectionBuilder @ContainerDslMarker internal constructor(
+    private val scope: ContainerSearchScope
+) {
+    @ContainerDslMarker
+    internal constructor(
+        selection: ContainerSelection,
+        scope: ContainerSearchScope
+    ) : this(scope) {
+        this.selector = selection.selector
+        this.containersWhitelist.addAll(selection.containersWhitelist)
+        this.loadedContainers.addAll(selection.loadedContainers)
+        this.containersBlacklist.addAll(selection.containersBlacklist)
+        this.accessScope = selection.accessScope
+        this.comparator = selection.comparator
+        this.dimensionWhitelist.addAll(selection.dimensionWhitelist)
+        this.dimensionBlacklist.addAll(selection.dimensionBlacklist)
+    }
+
+    private var selector: (Container) -> Boolean = { true }
+    private val containersWhitelist = mutableListOf()
+    private val loadedContainers = mutableListOf()
+    private val containersBlacklist = mutableListOf()
+    private var accessScope: AccessScope = AccessScope.Both
+    private var comparator: Comparator? = null
+    private val dimensionWhitelist = mutableSetOf()
+    private val dimensionBlacklist = mutableSetOf()
+
+    fun withContainers(vararg containers: Container) {
+        loadedContainers.addAll(containers)
+    }
+
+    fun withContainers(containers: Iterable) {
+        loadedContainers.addAll(containers)
+    }
+
+    fun ofAny(vararg containers: Container) {
+        containersWhitelist.addAll(containers)
+        withContainers(*containers)
+        appendSelector { container -> container in containersWhitelist }
+    }
+
+    fun ofAny(containers: Iterable) {
+        containersWhitelist.addAll(containers)
+        withContainers(containers)
+        appendSelector { container -> container in containersWhitelist }
+    }
+
+    fun noneOf(vararg containers: Container) {
+        containersBlacklist.addAll(containers)
+        appendSelector { container -> container !in containersBlacklist }
+    }
+
+    fun noneOf(containers: Iterable) {
+        containersBlacklist.addAll(containers)
+        appendSelector { container -> container !in containersBlacklist }
+    }
+
+    fun ofAnyType(vararg types: ContainerType) {
+        appendSelector { container -> types.contains(container.type) }
+    }
+
+    fun ofAnyType(types: Iterable) {
+        appendSelector { container -> types.contains(container.type) }
+    }
+
+    fun noneOfType(vararg types: ContainerType) {
+        appendSelector { container -> !types.contains(container.type) }
+    }
+
+    fun noneOfType(types: Iterable) {
+        appendSelector { container -> !types.contains(container.type) }
+    }
+
+    fun inAnyDimension(vararg dimensions: Dimension) {
+        dimensionWhitelist.addAll(dimensions)
+        appendSelector { it !is PlacedContainer || it.dimension in dimensions }
+    }
+
+    fun inAnyDimension(dimensions: Iterable) {
+        dimensionWhitelist.addAll(dimensions)
+        appendSelector { it !is PlacedContainer || it.dimension in dimensions }
+    }
+
+    fun noneOfDimensions(vararg dimensions: Dimension) {
+        dimensionBlacklist.addAll(dimensions)
+        appendSelector { it !is PlacedContainer || it.dimension !in dimensions }
+    }
+
+    fun noneOfDimensions(dimensions: Iterable) {
+        dimensionBlacklist.addAll(dimensions)
+        appendSelector { it !is PlacedContainer || it.dimension !in dimensions }
+    }
+
+    fun inCurrentDimension() {
+        val dimension = mc.world?.dim ?: Dimension.Unknown
+        dimensionWhitelist.add(dimension)
+        appendSelector { it.dimension == dimension }
+    }
+
+    fun notInCurrentDimension() {
+        val dimension = mc.world?.dim ?: Dimension.Unknown
+        dimensionBlacklist.add(dimension)
+        appendSelector { it.dimension != dimension }
+    }
+
+    fun isNested() {
+        appendSelector { container -> container is NestedContainer }
+    }
+
+    fun notNested() {
+        appendSelector { container -> container !is NestedContainer }
+    }
+
+    fun matches(containerSelection: ContainerSelection) {
+        appendSelector { container -> containerSelection.matches(container) }
+    }
+
+    fun noMatch(containerSelection: ContainerSelection) {
+        appendSelector { container -> !containerSelection.matches(container) }
+    }
+
+    fun hasStack(stackSelection: StackSelection) {
+        appendSelector { container -> stackSelection isIn container }
+    }
+
+    fun noStack(stackSelection: StackSelection) {
+        appendSelector { container -> !stackSelection.isIn(container) }
+    }
+
+    fun hasSpace(stackSelection: StackSelection) {
+        appendSelector { container -> stackSelection spaceIn container }
+    }
+
+    fun noSpace(stackSelection: StackSelection) {
+        appendSelector { container -> !stackSelection.spaceIn(container) }
+    }
+
+    fun isAccessed() {
+        accessScope = AccessScope.Accessed
+        appendSelector { it.isAccessed }
+    }
+
+    fun notAccessed() {
+        accessScope = AccessScope.NotAccessed
+        appendSelector { !it.isAccessed }
+    }
+
+    fun predicate(predicate: (Container) -> Boolean) {
+        appendSelector { predicate(it) }
+    }
+
+    fun sortedWith(comparator: Comparator) {
+        this.comparator = this.comparator
+            ?.then(comparator)
+            ?: comparator
+    }
+
+    fun sortedWith(comparatorSupplier: () -> Comparator) {
+        this.comparator = this.comparator
+            ?.then(comparatorSupplier())
+            ?: comparatorSupplier()
+    }
+
+    private fun appendSelector(selector: (Container) -> Boolean) {
+        val currentSelector = this.selector
+        this.selector = { currentSelector(it) && selector(it) }
+    }
+
+    internal fun build() =
+        ContainerSelection(
+            selector = selector,
+            scope = scope,
+            containersWhitelist = containersWhitelist,
+            loadedContainers = loadedContainers,
+            containersBlacklist = containersBlacklist,
+            accessScope = accessScope,
+            comparator = comparator
+                .takeUnless { it == null }
+                ?.thenBy { it.type }
+                ?: compareBy { it.type },
+            dimensionWhitelist = dimensionWhitelist.toSet(),
+            dimensionBlacklist = dimensionBlacklist.toSet()
+        )
+}
+
+enum class AccessScope(val accessed: Boolean?) {
+    Both(null),
+    Accessed(true),
+    NotAccessed(false)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/container/selection/StackSelection.kt b/src/main/kotlin/com/lambda/interaction/container/selection/StackSelection.kt
new file mode 100644
index 000000000..dc987933a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/container/selection/StackSelection.kt
@@ -0,0 +1,623 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+@file:Suppress("unused")
+
+package com.lambda.interaction.container.selection
+
+import com.lambda.Lambda.mc
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerDslMarker
+import com.lambda.interaction.container.SlotIdentity
+import com.lambda.interaction.container.identity
+import com.lambda.interaction.manager.managers.inventory.InventoryManager
+import com.lambda.util.EnchantmentUtils.getEnchantment
+import com.lambda.util.item.ItemStackUtils.orZero
+import com.lambda.util.item.ItemStackUtils.shulkerBoxStacks
+import com.lambda.util.item.ItemUtils
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.component.ComponentType
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.enchantment.Enchantment
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.item.consume.UseAction
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.tag.TagKey
+import net.minecraft.screen.slot.Slot
+import java.util.*
+import kotlin.time.Instant
+
+@ContainerDslMarker
+fun Item.select(count: Int = 1) = stackSelection(count) { isItem(this@select) }
+
+@ContainerDslMarker
+fun ItemStack.select(count: Int = 1) = stackSelection(count) { isItemStack(this@select) }
+
+fun stackSelection(
+	count: Int = 1,
+	builder: StackSelectionBuilder.() -> Unit = {}
+) = StackSelectionBuilder(count).apply(builder).build()
+
+fun StackSelection.mutate(
+	count: Int = this.count,
+	builder: StackSelectionBuilder.() -> Unit = {}
+) = StackSelectionBuilder(this, count).apply(builder).build()
+
+class StackSelection @ContainerDslMarker internal constructor(
+	val count: Int = 1,
+	val whitelistedSlots: Collection = emptyList(),
+	val blacklistedSlots: Collection = emptyList(),
+	val item: Item? = null,
+	val itemStack: ItemStack? = null,
+	val shulkerBoxScope: ShulkerBoxScope = ShulkerBoxScope.Both,
+	val countEmptyAsOne: Boolean = false,
+	val alwaysWholeStacks: Boolean = false,
+	val comparator: Comparator> = compareBy { it.stack.count },
+	val selector: (ItemStack, Slot?) -> Boolean,
+) {
+	val optimalStack = itemStack ?: item?.let { ItemStack(it, count) }
+
+	val isUnbounded: Boolean get() = count < 0
+
+	@ContainerDslMarker
+	fun bestMatch(stacks: Iterable) = filter(stacks).firstOrNull()
+
+	@ContainerDslMarker
+	fun bestMatch(slots: Iterable) = filter(slots).firstOrNull()
+
+	@ContainerDslMarker
+	fun bestMatch(stackAndSlots: Iterable>) = filter(stackAndSlots).firstOrNull()
+
+	@ContainerDslMarker
+	fun match(stacks: Iterable) = stacks.find(::matches)
+
+	@ContainerDslMarker
+	fun match(slots: Iterable) = slots.find(::matches)
+
+	@ContainerDslMarker
+	fun match(stackAndSlots: Iterable>) = stackAndSlots.find(::matches)
+
+	@ContainerDslMarker
+	fun matches(stack: ItemStack): Boolean {
+		val matchesSelf = selector(stack, null)
+		return when (shulkerBoxScope.inShulkerBox) {
+			true -> stack.shulkerBoxStacks.any { selector(it, null) }
+			false -> matchesSelf
+			null -> matchesSelf || stack.shulkerBoxStacks.any { selector(it, null) }
+		}
+	}
+
+	@ContainerDslMarker
+	fun matches(slot: Slot): Boolean {
+		val matchesSelf = selector(slot.stack, slot)
+		return when (shulkerBoxScope.inShulkerBox) {
+			true -> slot.stack.shulkerBoxStacks.any { selector(it, null) }
+			false -> matchesSelf
+			null -> matchesSelf || slot.stack.shulkerBoxStacks.any { selector(it, null) }
+		}
+	}
+
+	@ContainerDslMarker
+	fun matches(stackAndSlot: StackAndSlot<*>) =
+		stackAndSlot.slot?.let(::matches) ?: matches(stackAndSlot.stack)
+
+	@ContainerDslMarker
+	@JvmName("filter1")
+	fun filter(
+		stacks: Iterable,
+		sorted: Boolean = true
+	) = stacks
+		.let { if (whitelistedSlots.isNotEmpty()) emptyList() else it }
+		.asSequence()
+		.filter(::matches)
+		.let {
+			if (sorted)
+				it.map { StackAndSlot(it, null) }
+					.sortedWith(comparator)
+					.map { it.stack }
+			else it
+		}
+		.toList()
+
+	@ContainerDslMarker
+	@JvmName("filter2")
+	fun filter(
+		slots: Iterable,
+		sorted: Boolean = true
+	) = slots
+		.let { slots ->
+			if (whitelistedSlots.isNotEmpty()) {
+				slots.filter { it.identity() in whitelistedSlots }
+			} else slots
+		}
+		.filter { it.identity() !in blacklistedSlots }
+		.filter(::matches)
+		.let {
+			if (sorted)
+				it.map { StackAndSlot(it.stack, it) }
+					.sortedWith(comparator)
+					.map { it.slot }
+			else it
+		}
+		.toList()
+
+	@ContainerDslMarker
+	@JvmName("filter3")
+	fun filter(
+		stackAndSlots: Iterable>,
+		sorted: Boolean = true
+	) = stackAndSlots
+		.asSequence()
+		.let { stackAndSlots ->
+			if (whitelistedSlots.isNotEmpty()) stackAndSlots.filter { it.slot?.identity() in whitelistedSlots }
+			else stackAndSlots
+		}
+		.filter { it.slot?.identity() !in blacklistedSlots }
+		.filter(::matches)
+		.let {
+			if (sorted) it.sortedWith(comparator)
+			else it
+		}
+		.toList()
+
+	@ContainerDslMarker
+	infix fun isIn(container: Container) =
+		container.count(this) >= count
+
+	@ContainerDslMarker
+	fun isIn(vararg containers: Container) =
+		containers.sumOf { it.count(this).orZero } >= count
+
+	@ContainerDslMarker
+	infix fun isIn(containers: Iterable) =
+		containers.sumOf { it.count(this).orZero } >= count
+
+	@ContainerDslMarker
+	infix fun spaceIn(container: Container) =
+		container.spaceLeft(this) >= count
+
+	@ContainerDslMarker
+	fun spaceIn(vararg containers: Container) =
+		containers.sumOf { it.spaceLeft(this) } >= count
+
+	@ContainerDslMarker
+	infix fun spaceIn(containers: Iterable) =
+		containers.sumOf { it.spaceLeft(this) } >= count
+
+	@ContainerDslMarker
+	fun canInsert(slot: Slot) =
+		optimalStack?.let { !it.isEmpty && slot.canInsert(it) } ?: true
+
+	@ContainerDslMarker
+	fun emptySpace(slots: Iterable) =
+		slots.count { it.stack.isEmpty && canInsert(it) }
+
+	companion object {
+		const val UNBOUNDED = -1
+
+		val EVERYTHING = StackSelection(UNBOUNDED) { _, _ -> true }
+		val NOTHING = StackSelection { _, _ -> false }
+		val ALL_EMPTY = stackSelection(UNBOUNDED) { isEmpty(); countEmptyAsOne() }
+		val ALL_NOT_EMPTY = stackSelection(UNBOUNDED) { notEmpty() }
+	}
+}
+
+@ContainerDslMarker
+class StackSelectionBuilder @ContainerDslMarker internal constructor(
+	private var count: Int = 1
+) {
+	private val whitelistedSlots = mutableListOf()
+	private val blacklistedSlots = mutableListOf()
+	private var selector: (ItemStack, Slot?) -> Boolean = { _, _ -> true }
+	private var item: Item? = null
+	private var itemStack: ItemStack? = null
+	private var comparator: Comparator>? = null
+	private var shulkerBoxScope: ShulkerBoxScope = ShulkerBoxScope.NotInShulkerBox
+	private var countEmptyAsOne = false
+	private var alwaysWholeStacks = false
+
+	@ContainerDslMarker
+	internal constructor(
+		selection: StackSelection,
+		count: Int = selection.count
+	) : this(count) {
+		this.whitelistedSlots.addAll(selection.whitelistedSlots)
+		this.blacklistedSlots.addAll(selection.blacklistedSlots)
+		this.selector = selection.selector
+		this.item = selection.item
+		this.itemStack = selection.itemStack
+		this.comparator = selection.comparator
+		this.shulkerBoxScope = selection.shulkerBoxScope
+		this.countEmptyAsOne = selection.countEmptyAsOne
+		this.alwaysWholeStacks = selection.alwaysWholeStacks
+	}
+
+	fun withSelection(stackSelection: StackSelection) {
+		appendSelector(stackSelection.selector)
+		whitelistedSlots.addAll(stackSelection.whitelistedSlots)
+		blacklistedSlots.addAll(stackSelection.blacklistedSlots)
+	}
+
+	fun withoutSelection(stackSelection: StackSelection) {
+		appendSelector { stack, slot -> !stackSelection.selector(stack, slot) }
+		whitelistedSlots.addAll(stackSelection.blacklistedSlots)
+		blacklistedSlots.addAll(stackSelection.whitelistedSlots)
+	}
+
+	fun isItem(item: Item) {
+		this.item = item
+		appendSelector { stack, _ -> stack.item == item }
+	}
+
+	fun notItem(item: Item) {
+		appendSelector { stack, _ -> stack.item != item }
+	}
+
+	inline fun  isItem() {
+		val kClass = T::class
+		appendSelector { stack, _ -> stack.item::class == kClass }
+	}
+
+	inline fun  notItem() {
+		val kClass = T::class
+		appendSelector { stack, _ -> stack.item::class != kClass }
+	}
+
+	fun isItemStack(stack: ItemStack) {
+		this.itemStack = stack
+		appendSelector { s, _ -> ItemStack.areEqual(s, stack) }
+	}
+
+	fun notItemStack(stack: ItemStack) {
+		appendSelector { s, _ -> !ItemStack.areEqual(s, stack) }
+	}
+
+	fun isItemStackByRef(stack: ItemStack) {
+		this.itemStack = stack
+		appendSelector { s, _ -> s === stack }
+	}
+
+	fun notItemStackByRef(stack: ItemStack) {
+		appendSelector { s, _ -> s !== stack }
+	}
+
+	fun alsoInShulkerBoxes() {
+		shulkerBoxScope = ShulkerBoxScope.Both
+	}
+
+	fun onlyInShulkerBoxes() {
+		shulkerBoxScope = ShulkerBoxScope.InShulkerBox
+	}
+
+	fun ofAnyItems(items: Iterable) {
+		appendSelector { stack, _ -> items.contains(stack.item) }
+	}
+
+	fun noneOfItems(items: Iterable) {
+		appendSelector { stack, _ -> !items.contains(stack.item) }
+	}
+
+	fun isShulkerBox() {
+		appendSelector { stack, _ -> stack.item in ItemUtils.SHULKER_BOXES }
+	}
+
+	fun notShulkerBox() {
+		appendSelector { stack, _ -> stack.item !in ItemUtils.SHULKER_BOXES }
+	}
+
+	fun withName(name: String) {
+		appendSelector { stack, _ -> stack.name.string == name }
+	}
+
+	fun withoutName(name: String) {
+		appendSelector { stack, _ -> stack.name.string != name }
+	}
+
+	fun ofAnyStacks(stacks: Iterable) {
+		appendSelector { stack, _ -> stacks.contains(stack) }
+	}
+
+	fun noneOfStacks(stacks: Iterable) {
+		appendSelector { stack, _ -> !stacks.contains(stack) }
+	}
+
+	fun isEfficientForBreaking(blockState: BlockState) {
+		appendSelector { itemStack, _ ->
+			if (hasEfficientTool(blockState)) itemStack.item.getMiningSpeed(itemStack, blockState) > 1f
+			else true
+		}
+	}
+
+	fun notEfficientForBreaking(blockState: BlockState) {
+		appendSelector { itemStack, _ ->
+			if (hasEfficientTool(blockState)) itemStack.item.getMiningSpeed(itemStack, blockState) <= 1f
+			else true
+		}
+	}
+
+	private fun hasEfficientTool(blockState: BlockState) =
+		efficientToolCache.getOrPut(blockState) {
+			ItemUtils.TOOLS.any { it.getMiningSpeed(it.defaultStack, blockState) > 1f }
+		}
+
+	fun isSuitableForBreaking(blockState: BlockState) {
+		appendSelector { itemStack, _ ->
+			!blockState.isToolRequired || itemStack.isSuitableFor(blockState)
+		}
+	}
+
+	fun notSuitableForBreaking(blockState: BlockState) {
+		appendSelector { itemStack, _ ->
+			blockState.isToolRequired && !itemStack.isSuitableFor(blockState)
+		}
+	}
+
+	fun withTag(tag: TagKey) {
+		appendSelector { stack, _ -> stack.isIn(tag) }
+	}
+
+	fun withoutTag(tag: TagKey) {
+		appendSelector { stack, _ -> !stack.isIn(tag) }
+	}
+
+	fun withUseAction(action: UseAction) {
+		appendSelector { stack, _ -> stack.useAction == action }
+	}
+
+	fun withoutUseAction(action: UseAction) {
+		appendSelector { stack, _ -> stack.useAction != action }
+	}
+
+	fun isTool() = withComponent(DataComponentTypes.TOOL)
+
+	fun notTool() = withoutComponent(DataComponentTypes.TOOL)
+
+	fun isFood() = withComponent(DataComponentTypes.FOOD)
+
+	fun notFood() = withoutComponent(DataComponentTypes.FOOD)
+
+	fun withComponent(type: ComponentType<*>) {
+		appendSelector { stack, _ -> stack.components.contains(type) }
+	}
+
+	fun withoutComponent(type: ComponentType<*>) {
+		appendSelector { stack, _ -> !stack.components.contains(type) }
+	}
+
+	fun isBlock(block: Block) {
+		item = block.asItem()
+		appendSelector { stack, _ -> stack.item == block.asItem() }
+	}
+
+	fun notBlock(block: Block) {
+		appendSelector { stack, _ -> stack.item != block.asItem() }
+	}
+
+	fun withDamage(damage: Int) {
+		appendSelector { stack, _ -> stack.damage == damage }
+	}
+
+	fun withoutDamage(damage: Int) {
+		appendSelector { stack, _ -> stack.damage != damage }
+	}
+
+	fun withEnchantment(enchantment: RegistryKey, level: Int = -1) {
+		appendSelector { stack, _ ->
+			if (level < 0) stack.getEnchantment(enchantment) > 0
+			else stack.getEnchantment(enchantment) == level
+		}
+	}
+
+	fun withoutEnchantment(enchantment: RegistryKey, level: Int = -1) {
+		appendSelector { stack, _ ->
+			if (level < 0) stack.getEnchantment(enchantment) <= 0
+			else stack.getEnchantment(enchantment) != level
+		}
+	}
+
+	fun isEmpty() {
+		appendSelector { stack, _ -> stack.isEmpty }
+	}
+
+	fun countEmptyAsOne() {
+		this.countEmptyAsOne = true
+	}
+
+	fun alwaysWholeStacks() {
+		this.alwaysWholeStacks = true
+	}
+
+	fun notEmpty() {
+		appendSelector { stack, _ -> !stack.isEmpty }
+	}
+
+	fun predicate(predicate: (ItemStack, Slot?) -> Boolean) {
+		appendSelector { stack, slot -> predicate(stack, slot) }
+	}
+
+	fun canInsert(stack: ItemStack) {
+		appendSelector { _, slot -> slot?.canInsert(stack) == true }
+	}
+
+	fun cannotInsert(stack: ItemStack) {
+		appendSelector { _, slot -> slot?.canInsert(stack) == false }
+	}
+
+	fun canInsert(selection: StackSelection) {
+		selection.optimalStack?.let { canInsert(it) }
+	}
+
+	fun cannotInsert(selection: StackSelection) {
+		selection.optimalStack?.let { cannotInsert(it) }
+	}
+
+	fun canInsert() {
+		val optimal = itemStack ?: item?.let { ItemStack(it, count) }
+		optimal?.let { canInsert(it) }
+	}
+
+	fun cannotInsert() {
+		val optimal = itemStack ?: item?.let { ItemStack(it, count) }
+		optimal?.let { cannotInsert(it) }
+	}
+
+	fun sortedWith(newComparator: Comparator>) {
+		comparator = comparator?.thenComparing(newComparator) ?: newComparator
+	}
+
+	fun sortedWith(comparatorSupplier: () -> Comparator>) {
+		sortedWith(comparatorSupplier())
+	}
+
+	fun sortedByMatches(stackSelection: StackSelection) {
+		sortedWith {
+			compareByDescending { stackSelection.matches(it) }
+		}
+	}
+
+	fun sortedByMatches(stackSelectionSupplier: () -> StackSelection) {
+		sortedWith {
+			compareByDescending { stackSelectionSupplier().matches(it) }
+		}
+	}
+
+	fun sortedByMismatches(stackSelection: StackSelection) {
+		sortedWith {
+			compareBy { stackSelection.matches(it) }
+		}
+	}
+
+	fun sortedByMismatches(stackSelectionSupplier: () -> StackSelection) {
+		sortedWith {
+			compareBy { stackSelectionSupplier().matches(it) }
+		}
+	}
+
+	fun sortedByBestContentMatch(expectedContents: List) {
+		val expectedFrequencies = expectedContents
+			.filter { !it.isEmpty }
+			.groupingBy { it.item }
+			.eachCount()
+		sortedWith(
+			compareByDescending {
+				val currentFrequencies = it.stack.shulkerBoxStacks
+					.filter { !it.isEmpty }
+					.groupingBy { it.item }
+					.eachCount()
+				expectedFrequencies.asSequence().fold(0) { acc, (item, count) ->
+					 acc + minOf(count, currentFrequencies[item] ?: 0)
+				}
+			}
+		)
+	}
+
+	fun sortedByWorstContentMatch(expectedContents: List) {
+		val expectedFrequencies = expectedContents
+			.filter { !it.isEmpty }
+			.groupingBy { it.item }
+			.eachCount()
+		sortedWith(
+			compareBy {
+				val currentFrequencies = it.stack.shulkerBoxStacks
+					.filter { !it.isEmpty }
+					.groupingBy { it.item }
+					.eachCount()
+				expectedFrequencies.asSequence().fold(0) { acc, (item, count) ->
+					acc + minOf(count, currentFrequencies[item] ?: 0)
+				}
+			}
+		)
+	}
+
+	fun sortedBySlotMatch(slot: SlotIdentity) {
+		sortedWith {
+			compareByDescending {
+				it.slot?.identity() == slot
+			}
+		}
+	}
+
+	fun sortedBySlotMismatch(slot: SlotIdentity) {
+		sortedWith {
+			compareBy {
+				it.slot?.identity() == slot
+			}
+		}
+	}
+
+	fun sortedByRecentChange() {
+		sortedWith(
+			compareByDescending {
+				val syncId = mc.player?.currentScreenHandler?.syncId
+				it.slot?.let { slot ->
+					syncId?.let { id ->
+						InventoryManager.lastSlotChange(id, slot.id)
+					}
+				} ?: Instant.DISTANT_PAST
+			}
+		)
+	}
+
+	fun sortedByOldestChange() {
+		sortedWith {
+			compareBy {
+				val syncId = mc.player?.currentScreenHandler?.syncId
+				it.slot?.let { slot ->
+					syncId?.let { id ->
+						InventoryManager.lastSlotChange(id, slot.id)
+					}
+				} ?: Instant.DISTANT_PAST
+			}
+		}
+	}
+
+	@PublishedApi
+	internal fun appendSelector(selector: (ItemStack, Slot?) -> Boolean) {
+		val currentSelector = this.selector
+		this.selector = { stack, slot ->
+			currentSelector(stack, slot) && selector(stack, slot)
+		}
+	}
+
+	internal fun build() =
+		StackSelection(
+			count,
+			whitelistedSlots,
+			blacklistedSlots,
+			item,
+			itemStack,
+			shulkerBoxScope,
+			countEmptyAsOne,
+			alwaysWholeStacks,
+			comparator ?: compareBy { it.stack.count },
+			selector
+		)
+
+	companion object {
+		private val efficientToolCache = Collections.synchronizedMap(mutableMapOf())
+	}
+}
+
+data class StackAndSlot(val stack: ItemStack, val slot: T)
+
+enum class ShulkerBoxScope(val inShulkerBox: Boolean?) {
+	Both(null),
+	InShulkerBox(true),
+	NotInShulkerBox(false)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/managers/PostActionHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/PostActionHandler.kt
similarity index 84%
rename from src/main/kotlin/com/lambda/interaction/managers/PostActionHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/PostActionHandler.kt
index b1d2b7a44..0c64982fb 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/PostActionHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/PostActionHandler.kt
@@ -15,14 +15,14 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.handler
 
 import com.lambda.context.Automated
 import com.lambda.event.events.ConnectionEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler
+import com.lambda.interaction.manager.ActionInfo
 import com.lambda.util.collections.LimitedDecayQueue
 
 /**
@@ -51,8 +51,10 @@ abstract class PostActionHandler {
 		pendingInteractionsList.remove(context)
 	}
 
-	fun Automated.setPendingConfigs() {
-		BrokenBlockHandler.pendingActions.setSizeLimit(buildConfig.maxPendingActions)
-		BrokenBlockHandler.pendingActions.setDecayTime(buildConfig.actionTimeout * 50L)
-	}
+	context(automated: Automated)
+	fun setPendingConfigs() =
+		with(automated) {
+			pendingActions.setSizeLimit(buildConfig.maxPendingActions)
+			pendingActions.setDecayTime(buildConfig.actionTimeout * 50L)
+		}
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/BaritoneHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/BaritoneHandler.kt
similarity index 99%
rename from src/main/kotlin/com/lambda/interaction/handlers/BaritoneHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/BaritoneHandler.kt
index ff939f26c..85fe0978f 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/BaritoneHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/BaritoneHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers
+package com.lambda.interaction.handler.handlers
 
 import baritone.api.BaritoneAPI
 import baritone.api.IBaritone
@@ -28,7 +28,7 @@ import com.lambda.config.Tab
 import com.lambda.config.automation.AutomationConfig
 import com.lambda.config.blocks.RotationSettings
 import com.lambda.config.categories.LambdaCategory
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.context.Automated
 import com.lambda.util.BlockUtils.blockPos
 import net.minecraft.util.BlockMirror
diff --git a/src/main/kotlin/com/lambda/interaction/handler/handlers/ContainerHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/ContainerHandler.kt
new file mode 100644
index 000000000..2e45dfb9e
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/ContainerHandler.kt
@@ -0,0 +1,467 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+@file:Suppress("unused")
+
+package com.lambda.interaction.handler.handlers
+
+import com.lambda.Lambda
+import com.lambda.context.Automated
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
+import com.lambda.core.Loadable
+import com.lambda.event.events.InventoryEvent
+import com.lambda.event.events.PacketEvent
+import com.lambda.event.events.PlayerEvent
+import com.lambda.event.events.WorldEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ContainerDslMarker
+import com.lambda.interaction.container.ContainerSerializer
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.container.containers.ArmorContainer
+import com.lambda.interaction.container.containers.CursorContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.containers.external.ChestContainer
+import com.lambda.interaction.container.containers.external.DoubleChestContainer
+import com.lambda.interaction.container.containers.external.EnderChestContainer
+import com.lambda.interaction.container.containers.external.PlacedShulkerBoxContainer
+import com.lambda.interaction.container.containers.external.ShulkerBoxContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.containerSelection
+import com.lambda.interaction.container.selection.mutate
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findContainer
+import com.lambda.util.BlockUtils.blockEntity
+import com.lambda.util.ReflectionUtils.getInstances
+import com.lambda.util.extension.containerStacks
+import com.lambda.util.item.ItemStackUtils.count
+import com.lambda.util.item.ItemStackUtils.shulkerBoxStacks
+import com.lambda.util.item.ItemUtils.SHULKER_BOXES
+import com.lambda.util.player.SlotUtils.typeSafe
+import com.lambda.util.world.Dimension
+import net.minecraft.block.ChestBlock
+import net.minecraft.block.ShulkerBoxBlock
+import net.minecraft.block.entity.BlockEntity
+import net.minecraft.block.entity.ChestBlockEntity
+import net.minecraft.block.entity.EnderChestBlockEntity
+import net.minecraft.block.entity.ShulkerBoxBlockEntity
+import net.minecraft.block.enums.ChestType
+import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket
+import net.minecraft.screen.ScreenHandler
+import net.minecraft.screen.ScreenHandlerType
+import net.minecraft.state.property.Properties
+
+@Suppress("unused")
+object ContainerHandler : Loadable {
+	val compileContainers = getInstances()
+	var accessedPlacedContainer: PlacedContainer? = null
+		internal set
+
+	var lastInteractedBlockEntity: BlockEntity? = null
+		private set
+	var pendingInteractedBlockEntity: BlockEntity? = null
+		private set
+	private var interactedSyncId: Int = 0
+
+	override fun load(): String {
+		return "Loaded ${compileContainers.size} containers"
+	}
+
+	init {
+		listen {
+			ContainerSerializer.prepareIndex()
+			ContainerSerializer.loadEnderChest()
+		}
+
+		listenUnsafe {
+			ContainerSerializer.reset()
+			lastInteractedBlockEntity = null
+			pendingInteractedBlockEntity = null
+			interactedSyncId = 0
+			accessedPlacedContainer = null
+		}
+
+		listen { event ->
+			val packet = event.packet as? PlayerInteractBlockC2SPacket ?: return@listen
+			pendingInteractedBlockEntity = blockEntity(packet.blockHitResult.blockPos)
+		}
+
+		listen { event ->
+			val sh = event.screenHandler
+			onContainerUpdate(sh)
+			if (interactedSyncId == sh.syncId) {
+				lastInteractedBlockEntity = null
+				interactedSyncId = 0
+			}
+			accessedPlacedContainer = null
+		}
+
+		listen { event ->
+			val sh = event.screenHandler
+			if (sh.syncId == 0) return@listen
+			val pending = pendingInteractedBlockEntity
+			if (pending == null) {
+				lastInteractedBlockEntity = null
+				interactedSyncId = 0
+			} else {
+				lastInteractedBlockEntity = pending
+				interactedSyncId = sh.syncId
+			}
+			pendingInteractedBlockEntity = null
+		}
+
+		listen { onContainerUpdate() }
+		listen { onContainerUpdate() }
+
+		listen { event ->
+			val oldBlock = event.oldState.block
+			if (oldBlock is ChestBlock || oldBlock is ShulkerBoxBlock) {
+				if (event.newState.block != oldBlock) {
+					ContainerSerializer.removeContainer(event.pos)
+				}
+			}
+		}
+
+		listen { event ->
+			val chunk = event.chunk
+			try {
+				ContainerSerializer.savedIn(chunk.pos).forEach { (pos, type) ->
+					val entity = chunk.blockEntities[pos]
+					val stillThere =
+						when (type) {
+							ContainerType.Chest -> entity is ChestBlockEntity
+							ContainerType.PlacedShulkerBox -> entity is ShulkerBoxBlockEntity
+							else -> entity is ChestBlockEntity || entity is ShulkerBoxBlockEntity
+						}
+					if (!stillThere) ContainerSerializer.removeContainer(pos)
+				}
+			} catch (e: Exception) {
+				Lambda.LOG.warn("Failed to clean up ghost containers for chunk ${chunk.pos}", e)
+			}
+		}
+	}
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findSlot(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findSlots(stackSelection, containerSelection, sorted)
+		?.firstOrNull()
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findSlots(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findContainers(containerSelection, sorted)
+		.flatMap { stackSelection.filter(it.slots, sorted) }
+		.takeIf { it.isNotEmpty() && (stackSelection.isUnbounded || it.count(stackSelection.countEmptyAsOne) >= stackSelection.count) }
+		?.takeUpTo(stackSelection.count) { it.stack.count(stackSelection.countEmptyAsOne) }
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun slotsAvailable(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED
+	) = findSlots(stackSelection, containerSelection, false)
+		?.isNotEmpty() == true
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findStack(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findStacks(stackSelection, containerSelection, sorted)
+		?.firstOrNull()
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findStacks(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findContainers(containerSelection, sorted)
+		.flatMap { stackSelection.filter(it.stacks, sorted) }
+		.takeIf { it.isNotEmpty() && (stackSelection.isUnbounded || it.count(stackSelection.countEmptyAsOne) >= stackSelection.count) }
+		?.takeUpTo(stackSelection.count) { it.count(stackSelection.countEmptyAsOne) }
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun stacksAvailable(
+		stackSelection: StackSelection = StackSelection.EVERYTHING,
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED
+	) = findStacks(stackSelection, containerSelection, false)
+		?.isNotEmpty() == true
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findContainer(
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findContainers(containerSelection, sorted)
+		.firstOrNull()
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findContainers(
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = containerSelection
+		.filter(searchContainers(containerSelection).asIterable(), sorted)
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun containerAvailable(
+		containerSelection: ContainerSelection= ContainerSelection.ACCESSED
+	) = findContainer(containerSelection, false) != null
+
+	@ContainerDslMarker
+	context(_: Automated)
+	fun findContainerWithDisposable(
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) = findContainersWithDisposable(containerSelection, sorted)
+		.firstOrNull()
+
+	@ContainerDslMarker
+	context(automated: Automated)
+	fun findContainersWithDisposable(
+		containerSelection: ContainerSelection = ContainerSelection.ACCESSED,
+		sorted: Boolean = true
+	) =
+		with(automated) {
+			findContainers(
+				containerSelection(containerSelection.scope) {
+					matches(containerSelection)
+					hasStack(
+						stackSelection(1) { ofAnyItems(inventoryConfig.disposables) }
+					)
+				},
+				sorted
+			)
+		}
+
+	context(_: Automated)
+	private fun searchContainers(selection: ContainerSelection): Sequence {
+		fun Container.allNested(): Sequence =
+			sequence {
+				yield(this@allNested)
+				storedContainers.values.forEach { nested ->
+					yieldAll(nested.allNested())
+				}
+			}
+
+		val compiled = compileContainers.asSequence()
+		val placedSeq = sequenceOf(accessedPlacedContainer).filterNotNull()
+		val savedContainers =
+			if (selection.dimensionWhitelist.isEmpty() && selection.dimensionBlacklist.isEmpty()) {
+				ContainerSerializer.serializedContainers
+			} else {
+				val dimensions = selection.dimensionWhitelist.ifEmpty { Dimension.entries }
+				ContainerSerializer.serializedByDimensions(
+					*dimensions.filterNot { it in selection.dimensionBlacklist }.toTypedArray()
+				)
+			}
+
+		val baseContainers =
+			when (selection.scope) {
+				ContainerSearchScope.Loaded -> compiled + placedSeq + selection.loadedContainers
+				ContainerSearchScope.Accessed -> placedSeq + compiled.filter { it.isAccessed }
+				ContainerSearchScope.Compiled -> compiled
+				ContainerSearchScope.Player -> compiled - EnderChestContainer
+				ContainerSearchScope.All -> compiled + placedSeq +
+						savedContainers.filter { diskContainer ->
+							val accessed = accessedPlacedContainer
+							accessed == null || !diskContainer.haveMatchingInventories(accessed)
+						}
+			}
+
+		return baseContainers
+			.flatMap { it.allNested() }
+			.distinct()
+			.filter { it.isAccessible }
+	}
+
+	private fun  List.takeUpTo(need: Int, countOf: (T) -> Int): List {
+		if (need <= 0) return this
+		var sum = 0
+		val taken = ArrayList()
+		for (item in this) {
+			taken += item
+			sum += countOf(item)
+			if (sum >= need) break
+		}
+		return taken
+	}
+
+	private fun SafeContext.onContainerUpdate(sh: ScreenHandler = player.currentScreenHandler) {
+		InventoryContainer.scanContainerContents()
+		HotbarContainer.scanContainerContents()
+		OffhandContainer.scanContainerContents()
+		CursorContainer.scanContainerContents()
+		val blockEntity = lastInteractedBlockEntity
+		if (blockEntity != null && interactedSyncId == sh.syncId) {
+			updatePlacedContainer(sh, blockEntity)
+			return
+		}
+
+		ArmorContainer.scanContainerContents()
+	}
+
+	private fun SafeContext.updatePlacedContainer(
+		sh: ScreenHandler,
+		blockEntity: BlockEntity
+	) {
+		val pos = blockEntity.pos
+		if (blockEntity(pos) !== blockEntity) return
+		when (blockEntity) {
+			is EnderChestBlockEntity -> {
+				if (sh.typeSafe != ScreenHandlerType.GENERIC_9X3) return
+				EnderChestContainer.apply {
+					update(sh.containerStacks)
+					scanContainerContents()
+				}
+				ContainerSerializer.saveEnderChest()
+			}
+
+			is ChestBlockEntity -> {
+				val state = blockEntity.cachedState
+				if (state.block !is ChestBlock) return
+
+				val stacks = sh.containerStacks
+				val chestType = state.get(Properties.CHEST_TYPE) ?: return
+
+				if (chestType == ChestType.SINGLE) {
+					if (sh.typeSafe != ScreenHandlerType.GENERIC_9X3) return
+					(accessedPlacedContainer as? ChestContainer)
+						?.update(stacks)
+						?: run { accessedPlacedContainer = ChestContainer(pos, stacks) }
+				} else {
+					if (sh.typeSafe != ScreenHandlerType.GENERIC_9X6) return
+					val facing = state.get(Properties.HORIZONTAL_FACING) ?: return
+					val otherPos =
+						when (chestType) {
+							ChestType.LEFT -> pos.offset(facing.rotateYClockwise())
+							ChestType.RIGHT -> pos.offset(facing.rotateYCounterclockwise())
+						}
+					val leftPos = if (chestType == ChestType.LEFT) pos else otherPos
+					val rightPos = if (chestType == ChestType.RIGHT) pos else otherPos
+
+					val existing = accessedPlacedContainer as? DoubleChestContainer
+					if (existing != null && (existing.leftPos == leftPos || existing.rightPos == rightPos)) {
+						existing.update(stacks)
+					} else {
+						accessedPlacedContainer = DoubleChestContainer(pos, leftPos, rightPos, stacks)
+					}
+				}
+
+				accessedPlacedContainer?.let {
+					it.scanContainerContents()
+					ContainerSerializer.saveContainer(it)
+					if (it is DoubleChestContainer) {
+						val counterpartPos = if (it.pos == it.leftPos) it.rightPos else it.leftPos
+						val counterpart = DoubleChestContainer(counterpartPos, it.leftPos, it.rightPos, it.stacks, it.stash)
+						counterpart.storedContainers.putAll(it.storedContainers)
+						ContainerSerializer.saveContainer(counterpart)
+					}
+				}
+			}
+
+			is ShulkerBoxBlockEntity -> {
+				if (sh.typeSafe != ScreenHandlerType.SHULKER_BOX) return
+				val stacks = sh.containerStacks
+				(accessedPlacedContainer as? PlacedShulkerBoxContainer)
+					?.update(stacks)
+					?: run {
+						accessedPlacedContainer = PlacedShulkerBoxContainer(pos, blockEntity.cachedState.block, null, stacks)
+					}
+
+				accessedPlacedContainer?.let {
+					it.scanContainerContents()
+					ContainerSerializer.saveContainer(it)
+				}
+			}
+		}
+	}
+
+	private fun Container.scanContainerContents() {
+		slots.forEach { slot ->
+			val stack = slot.stack
+			val index = slot.index
+			if (stack.item in SHULKER_BOXES) {
+				storedContainers[index] =
+					ShulkerBoxContainer(
+						stack.name.string,
+						stack.item,
+						stack.shulkerBoxStacks,
+						this,
+						index
+					).also { it.scanContainerContents() }
+			} else storedContainers.remove(index)
+		}
+	}
+}
+
+@ContainerDslMarker
+context(_: AutomatedSafeContext)
+fun StackSelection.moveStack(
+	fromSelection: ContainerSelection = ContainerSelection.ACCESSED,
+	toSelection: ContainerSelection = ContainerSelection.ACCESSED,
+	replaceSelection: StackSelection = StackSelection.EVERYTHING
+): Boolean {
+	val fromContainer =
+		findContainer(
+			containerSelection(fromSelection.scope) {
+				matches(fromSelection)
+				hasStack(mutate(count.coerceAtMost(64)))
+				isAccessed()
+			}
+		)
+			?: return false
+
+	val toContainer =
+		findContainer(
+			containerSelection(toSelection.scope) {
+				matches(toSelection)
+				hasStack(replaceSelection)
+				isAccessed()
+			}
+		) ?: return false
+
+	val (fromSlot, toSlot) = fromContainer.findMoveSlots(this, toContainer, replaceSelection)
+	if (fromSlot == null || toSlot == null) return false
+
+	return fromContainer.swap(fromSlot, toSlot, toContainer)
+}
+
+enum class ContainerSearchScope {
+	Loaded,
+	Accessed,
+	Compiled,
+	Player,
+	All
+}
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/FriendHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/FriendHandler.kt
similarity index 99%
rename from src/main/kotlin/com/lambda/interaction/handlers/FriendHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/FriendHandler.kt
index d4690ce0a..5e491d245 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/FriendHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/FriendHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.Lambda
 import com.lambda.Lambda.LOG
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/FriendImporter.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/FriendImporter.kt
similarity index 95%
rename from src/main/kotlin/com/lambda/interaction/handlers/FriendImporter.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/FriendImporter.kt
index abf23e216..dfc282ff1 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/FriendImporter.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/FriendImporter.kt
@@ -20,8 +20,8 @@ package com.lambda.interaction.handlers
 import com.lambda.Lambda
 import com.lambda.config.categories.FriendCategory
 import com.lambda.config.serializers.FriendList
-import com.lambda.interaction.handlers.FriendHandler.befriend
-import com.lambda.interaction.handlers.FriendHandler.isFriend
+import com.lambda.interaction.handler.handlers.FriendHandler.befriend
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
 import com.lambda.network.mojang.getProfiles
 import com.lambda.threading.runGameScheduled
 import com.lambda.util.CommunicationUtils.info
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/GlideHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/GlideHandler.kt
similarity index 71%
rename from src/main/kotlin/com/lambda/interaction/handlers/GlideHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/GlideHandler.kt
index c519e2774..6c32f77c8 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/GlideHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/GlideHandler.kt
@@ -15,12 +15,15 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackAndSlot
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.stacksAvailable
 import com.lambda.module.modules.combat.AutoArmor
 import com.lambda.module.modules.movement.elytrafly.ElytraFly
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.mode
@@ -30,7 +33,6 @@ import com.lambda.module.modules.player.AutoElytraSwap.glideDelay
 import com.lambda.util.EnchantmentUtils.getEnchantment
 import com.lambda.util.player.PlayerUtils.canGlideWithChestPiece
 import com.lambda.util.player.PlayerUtils.canStartGliding
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
 import net.minecraft.component.DataComponentTypes
 import net.minecraft.enchantment.Enchantments
 import net.minecraft.entity.attribute.EntityAttributes
@@ -40,37 +42,41 @@ import net.minecraft.registry.tag.ItemTags
 
 object GlideHandler {
 	val ELYTRA_SELECTION =
-		selectStack {
-			sortByDescending {
-				it.getEnchantment(Enchantments.UNBREAKING)
-			}.thenByDescending {
-				it.getEnchantment(Enchantments.MENDING)
-			}
+		stackSelection {
 			isItem(Items.ELYTRA)
-				.and { it.damage < it.maxDamage }
+			predicate { stack, _ -> stack.damage < stack.maxDamage }
+			sortedWith {
+				compareByDescending> {
+					it.stack.getEnchantment(Enchantments.UNBREAKING)
+				}.thenByDescending {
+					it.stack.getEnchantment(Enchantments.MENDING)
+				}.thenBy { it.slot?.index }
+			}
 		}
 	val CHESTPLATE_SELECTION =
-		selectStack {
-			sortByDescending {
-				it.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
-					?.modifiers
-					?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR }
-					?.modifier?.value
-					?: 0.0
-			}.thenByDescending {
-				it.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
-					?.modifiers
-					?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR_TOUGHNESS }
-					?.modifier?.value
-					?: 0.0
-			}.thenByDescending {
-				it.getEnchantment(Enchantments.UNBREAKING)
-			}.thenByDescending {
-				it.getEnchantment(Enchantments.MENDING)
+		stackSelection {
+			withTag(ItemTags.CHEST_ARMOR)
+			notItem(Items.ELYTRA)
+			predicate { stack, _ -> stack.damage < stack.maxDamage }
+			sortedWith {
+				compareByDescending> {
+					it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
+						?.modifiers
+						?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR }
+						?.modifier?.value
+						?: 0.0
+				}.thenByDescending {
+					it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
+						?.modifiers
+						?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR_TOUGHNESS }
+						?.modifier?.value
+						?: 0.0
+				}.thenByDescending {
+					it.stack.getEnchantment(Enchantments.UNBREAKING)
+				}.thenByDescending {
+					it.stack.getEnchantment(Enchantments.MENDING)
+				}.thenBy { it.slot?.index }
 			}
-			hasTag(ItemTags.CHEST_ARMOR)
-				.and { it.item != Items.ELYTRA }
-				.and { it.damage < it.maxDamage }
 		}
 
 	@JvmStatic val overridingGlide
@@ -84,7 +90,10 @@ object GlideHandler {
 
 	init {
 		listen({ -1000 }) {
-			if (!player.isGliding && pendingGlides.isEmpty()) AutoElytraSwap.restore()
+			if (pendingGlides.isEmpty()) {
+				if (swapped && !player.isGliding) AutoElytraSwap.restore()
+				return@listen
+			}
 			tickPendingGlides()
 		}
 
@@ -163,9 +172,9 @@ object GlideHandler {
 			val fakeFly = ElytraFly.isEnabled && ElytraFly.fakeFly
 			val canGlideAlready = player.canGlideWithChestPiece() != fakeFly
 			if (!autoSwapChecking || canGlideAlready) return canGlideAlready
-			return player.hotbarAndInventorySlots.let { slots ->
-				if (fakeFly) CHESTPLATE_SELECTION.filterSlots(slots).isNotEmpty()
-				else ELYTRA_SELECTION.filterSlots(slots).isNotEmpty()
+			val selection = if (fakeFly) CHESTPLATE_SELECTION else ELYTRA_SELECTION
+			return with(ElytraFly) {
+				stacksAvailable(selection, ContainerSelection.HOTBAR_AND_INVENTORY)
 			}
 		}
 
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/interacting/InteractedBlockHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/InteractedBlockHandler.kt
similarity index 84%
rename from src/main/kotlin/com/lambda/interaction/handlers/interacting/InteractedBlockHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/InteractedBlockHandler.kt
index b1c1db359..829ce02ed 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/interacting/InteractedBlockHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/InteractedBlockHandler.kt
@@ -15,25 +15,25 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers.interacting
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.config.automation.AutomationConfig
 import com.lambda.config.blocks.InteractConfig
 import com.lambda.event.events.WorldEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.PostActionHandler
-import com.lambda.interaction.managers.interacting.InteractInfo
-import com.lambda.interaction.managers.interacting.InteractManager.placeSound
+import com.lambda.interaction.handler.PostActionHandler
+import com.lambda.interaction.manager.managers.interacting.InteractInfo
+import com.lambda.interaction.manager.managers.interacting.InteractManager.placeSound
 import com.lambda.module.modules.client.Client
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.matches
 import com.lambda.util.CommunicationUtils.warn
 import com.lambda.util.collections.LimitedDecayQueue
 
-object InteractedBlockHandler : PostActionHandler() {
+internal object InteractedBlockHandler : PostActionHandler() {
 	override val pendingActions = LimitedDecayQueue(
-		AutomationConfig.DEFAULT.buildConfig.maxPendingActions,
-		AutomationConfig.DEFAULT.buildConfig.actionTimeout * 50L
+		AutomationConfig.Companion.DEFAULT.buildConfig.maxPendingActions,
+		AutomationConfig.Companion.DEFAULT.buildConfig.actionTimeout * 50L
 	) {
 		if (Client.verboseDebug) warn("${it::class.simpleName} at ${it.context.blockPos.toShortString()} timed out")
 		if (it.interactConfig.interactConfirmationMode != InteractConfig.InteractConfirmationMode.AwaitThenPlace) {
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/packet/PacketLimitHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/PacketLimitHandler.kt
similarity index 86%
rename from src/main/kotlin/com/lambda/interaction/handlers/packet/PacketLimitHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/PacketLimitHandler.kt
index aa11a5d9e..8aae8ade3 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/packet/PacketLimitHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/PacketLimitHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers.packet
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.config.blocks.BuildConfig
 import com.lambda.context.Automated
@@ -26,7 +26,7 @@ import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.TimeSource
 
-object PacketLimitHandler {
+internal object PacketLimitHandler {
 	private val packetLimitMap = PacketType.entries.associateWith { LimitHandler() }
 
 	init {
@@ -37,14 +37,18 @@ object PacketLimitHandler {
 	}
 
 	context(automated: Automated)
-	fun canSendPackets(packetCount: Int, packetType: PacketType): Boolean {
-		val handler = packetLimitMap[packetType] ?: return false
+	fun availablePackets(packetType: PacketType): Int {
+		val handler = packetLimitMap[packetType] ?: return 0
 		handler.maxPacketsThisTimeframe = packetType.maxPacketsPerTimeframe(automated.buildConfig)
 		handler.timeframe = automated.buildConfig.limitTimeframe.milliseconds
 		handler.removeStale(TimeSource.Monotonic.markNow())
-		return handler.packetTimestamps.size + packetCount <= handler.maxPacketsThisTimeframe
+		return (handler.maxPacketsThisTimeframe - handler.packetTimestamps.size).coerceAtLeast(0)
 	}
 
+	context(automated: Automated)
+	fun canSendPackets(packetCount: Int, packetType: PacketType): Boolean =
+		availablePackets(packetType) >= packetCount
+
 	fun sentPackets(packetCount: Int, packetType: PacketType) {
 		packetLimitMap[packetType]?.let { handler ->
 			val currentTime = TimeSource.Monotonic.markNow()
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/PingHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/PingHandler.kt
similarity index 97%
rename from src/main/kotlin/com/lambda/interaction/handlers/PingHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/PingHandler.kt
index 6913519a0..e8720a75e 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/PingHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/PingHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.core.Loadable
 import com.lambda.event.events.PacketEvent
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/TimerHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/TimerHandler.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/interaction/handlers/TimerHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/TimerHandler.kt
index 0bd755ea9..d9d620358 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/TimerHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/TimerHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers
+package com.lambda.interaction.handler.handlers
 
 import com.lambda.core.Loadable
 import com.lambda.event.EventFlow.post
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/breaking/BrokenBlockHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/BrokenBlockHandler.kt
similarity index 92%
rename from src/main/kotlin/com/lambda/interaction/handlers/breaking/BrokenBlockHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/BrokenBlockHandler.kt
index 6279c1039..440466031 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/breaking/BrokenBlockHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/BrokenBlockHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers.breaking
+package com.lambda.interaction.handler.handlers.breaking
 
 import com.lambda.config.automation.AutomationConfig.Companion.DEFAULT
 import com.lambda.config.blocks.BreakConfig.BreakConfirmationMode
@@ -24,11 +24,11 @@ import com.lambda.event.events.EntityEvent
 import com.lambda.event.events.WorldEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.simulation.processing.ProcessorRegistry
-import com.lambda.interaction.handlers.breaking.RebreakHandler.reBreak
-import com.lambda.interaction.managers.PostActionHandler
-import com.lambda.interaction.managers.breaking.BreakInfo
-import com.lambda.interaction.managers.breaking.BreakManager.lastPosStarted
-import com.lambda.interaction.managers.breaking.BreakManager.matchesBlockItem
+import com.lambda.interaction.handler.PostActionHandler
+import com.lambda.interaction.handler.handlers.breaking.RebreakHandler.reBreak
+import com.lambda.interaction.manager.managers.breaking.BreakInfo
+import com.lambda.interaction.manager.managers.breaking.BreakManager.lastPosStarted
+import com.lambda.interaction.manager.managers.breaking.BreakManager.matchesBlockItem
 import com.lambda.module.modules.client.Client
 import com.lambda.module.modules.client.Client.verboseDebug
 import com.lambda.threading.runGameScheduled
@@ -51,7 +51,7 @@ import net.minecraft.util.math.ChunkSectionPos
  *
  * @see com.lambda.interaction.managers.breaking.BreakManager
  */
-object BrokenBlockHandler : PostActionHandler() {
+internal object BrokenBlockHandler : PostActionHandler() {
 	override val pendingActions = LimitedDecayQueue(
 		DEFAULT.buildConfig.maxPendingActions, DEFAULT.buildConfig.actionTimeout * 50L
 	) { info ->
@@ -109,6 +109,7 @@ object BrokenBlockHandler : PostActionHandler() {
 					) {
 					destroyBlock(pending)
 				}
+				pendingActions.refresh(pending)
 				pending.internalOnBreak()
 				if (pending.callbacksCompleted) {
 					pending.stopPending()
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakHandler.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakHandler.kt
similarity index 83%
rename from src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakHandler.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakHandler.kt
index fcf34a1a9..40f04a2bf 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakHandler.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakHandler.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers.breaking
+package com.lambda.interaction.handler.handlers.breaking
 
 import com.lambda.config.blocks.BreakConfig
 import com.lambda.context.SafeContext
@@ -24,23 +24,23 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
 import com.lambda.interaction.construction.simulation.context.BreakContext
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler.destroyBlock
-import com.lambda.interaction.handlers.breaking.RebreakHandler.reBreak
-import com.lambda.interaction.handlers.packet.PacketLimitHandler
-import com.lambda.interaction.handlers.packet.PacketType
-import com.lambda.interaction.managers.breaking.BreakInfo
-import com.lambda.interaction.managers.breaking.BreakManager
-import com.lambda.interaction.managers.breaking.BreakManager.calcBreakDelta
-import com.lambda.interaction.managers.breaking.BreakRequest
+import com.lambda.interaction.handler.handlers.PacketLimitHandler
+import com.lambda.interaction.handler.handlers.PacketType
+import com.lambda.interaction.handler.handlers.breaking.BrokenBlockHandler.destroyBlock
+import com.lambda.interaction.handler.handlers.breaking.RebreakHandler.reBreak
+import com.lambda.interaction.manager.managers.breaking.BreakInfo
+import com.lambda.interaction.manager.managers.breaking.BreakManager
+import com.lambda.interaction.manager.managers.breaking.BreakManager.calcBreakDelta
+import com.lambda.interaction.manager.managers.breaking.BreakRequest
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.player.PlayerUtils.swingHand
 import net.minecraft.util.Hand
 
 /**
- * Designed to track the latest primary-broken [com.lambda.interaction.managers.breaking.BreakInfo] in order to exploit a flaw in Minecraft's code that allows
+ * Designed to track the latest primary-broken [BreakInfo] in order to exploit a flaw in Minecraft's code that allows
  * the user to break any block placed in said position using the progress from the previously broken block.
  */
-object RebreakHandler {
+internal object RebreakHandler {
 	var reBreak: BreakInfo? = null
 
     init {
@@ -134,12 +134,12 @@ object RebreakHandler {
 			RebreakResult.StillBreaking(reBreak)
 		}
 	}
+}
 
-	enum class RebreakPotential {
-		Instant,
-		PartialProgress,
-		None;
+enum class RebreakPotential {
+	Instant,
+	PartialProgress,
+	None;
 
-		fun isPossible() = this != None
-	}
+	fun isPossible() = this != None
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakResult.kt b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakResult.kt
similarity index 83%
rename from src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakResult.kt
rename to src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakResult.kt
index 27dc43bbb..67982cf31 100644
--- a/src/main/kotlin/com/lambda/interaction/handlers/breaking/RebreakResult.kt
+++ b/src/main/kotlin/com/lambda/interaction/handler/handlers/breaking/RebreakResult.kt
@@ -15,11 +15,11 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.handlers.breaking
+package com.lambda.interaction.handler.handlers.breaking
 
-import com.lambda.interaction.managers.breaking.BreakInfo
+import com.lambda.interaction.manager.managers.breaking.BreakInfo
 
-sealed class RebreakResult {
+internal sealed class RebreakResult {
 	data object Ignored : RebreakResult()
 
 	data object Rebroke : RebreakResult()
diff --git a/src/main/kotlin/com/lambda/interaction/handlers/ContainerHandler.kt b/src/main/kotlin/com/lambda/interaction/handlers/ContainerHandler.kt
deleted file mode 100644
index e7a083287..000000000
--- a/src/main/kotlin/com/lambda/interaction/handlers/ContainerHandler.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.handlers
-
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.core.Loadable
-import com.lambda.event.events.InventoryEvent
-import com.lambda.event.events.PlayerEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.material.ContainerSelection
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.StackSelection.Companion.select
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.interaction.material.container.containers.ChestContainer
-import com.lambda.interaction.material.container.containers.EnderChestContainer
-import com.lambda.util.BlockUtils.blockEntity
-import com.lambda.util.ReflectionUtils
-import com.lambda.util.extension.containerStacks
-import net.minecraft.block.entity.BlockEntity
-import net.minecraft.block.entity.ChestBlockEntity
-import net.minecraft.block.entity.EnderChestBlockEntity
-import net.minecraft.screen.GenericContainerScreenHandler
-import net.minecraft.screen.ScreenHandlerType
-import net.minecraft.screen.slot.Slot
-
-// ToDo: Make this a Configurable to save container caches. Should use a cached region based storage system.
-@Suppress("unused")
-object ContainerHandler : Loadable {
-    private val containers: List
-        get() = compileContainers + runtimeContainers
-
-    private val compileContainers = ReflectionUtils.getInstances()
-    private val runtimeContainers = mutableSetOf()
-
-    var lastInteractedBlockEntity: BlockEntity? = null
-
-    override fun load() = "Loaded ${compileContainers.size} containers"
-
-    init {
-        listen {
-            lastInteractedBlockEntity = blockEntity(it.blockHitResult.blockPos)
-        }
-
-        listen { event ->
-            if (event.screenHandler !is GenericContainerScreenHandler) return@listen
-
-            val handler = event.screenHandler
-
-            when (val block = lastInteractedBlockEntity) {
-                is EnderChestBlockEntity -> {
-                    if (handler.type != ScreenHandlerType.GENERIC_9X3) return@listen
-
-                    EnderChestContainer.update(handler.containerStacks)
-                }
-
-                is ChestBlockEntity -> {
-                    // ToDo: Handle double chests and single chests
-                    if (handler.type != ScreenHandlerType.GENERIC_9X6) return@listen
-                    val stacks = handler.containerStacks
-
-                    containers
-                        .filterIsInstance()
-                        .find {
-                            it.blockPos == block.pos
-                        }?.update(stacks) ?: runtimeContainers.add(ChestContainer(stacks, block.pos))
-                }
-            }
-            lastInteractedBlockEntity = null
-        }
-    }
-
-    context(_: SafeContext)
-    fun containers() = containers.flatMap { setOf(it) + it.shulkerContainer }.sorted()
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.transfer(destination: MaterialContainer) =
-        with(automatedSafeContext) {
-            findContainerWithMaterial(
-                inventoryConfig.containerSelection
-            )?.transfer(this@transfer, destination)
-                ?: false
-        }
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.transferByTask(destination: MaterialContainer) =
-        with(automatedSafeContext) {
-            findContainerWithMaterial()?.transferByTask(this@transferByTask, destination)
-        }
-
-    context(_: SafeContext)
-    fun findContainer(
-	    block: (MaterialContainer) -> Boolean,
-    ): MaterialContainer? = containers().find(block)
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findContainerWithMaterial(
-        containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection
-    ): MaterialContainer? = findContainersWithMaterial(containerSelection).firstOrNull()
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findContainersWithMaterial(
-	    containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection,
-    ): List =
-        containers()
-            .filter { containerSelection.matches(it) }
-            .filter { it.materialAvailable(this) >= count }
-            .sortedWith(automatedSafeContext.inventoryConfig.providerPriority.materialComparator(this))
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findContainerWithSpace(
-        containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection
-    ): MaterialContainer? = findContainersWithSpace(containerSelection).firstOrNull()
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findContainersWithSpace(
-        containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection
-    ): List =
-        containers()
-            .filter { containerSelection.matches(it) }
-            .filter { it.spaceAvailable(this) >= count }
-            .sortedWith(automatedSafeContext.inventoryConfig.providerPriority.spaceComparator(this))
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findSlotWithMaterial(
-        containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection
-    ) = findSlotsWithMaterial(containerSelection).firstOrNull()
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun StackSelection.findSlotsWithMaterial(
-        containerSelection: ContainerSelection = automatedSafeContext.inventoryConfig.containerSelection
-    ): List =
-        findContainersWithMaterial(containerSelection)
-            .flatMap { filterSlots(it.slots) }
-
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun findDisposable() = containers().find { container ->
-        automatedSafeContext.inventoryConfig.disposables.any { container.materialAvailable(it.asItem().select()) > 0 }
-    }
-
-    class NoContainerFound(selection: StackSelection) : Exception("No container found matching $selection")
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/managers/ActionInfo.kt b/src/main/kotlin/com/lambda/interaction/manager/ActionInfo.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/interaction/managers/ActionInfo.kt
rename to src/main/kotlin/com/lambda/interaction/manager/ActionInfo.kt
index 6d457e8d3..dfaf5548e 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/ActionInfo.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/ActionInfo.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.manager
 
 import com.lambda.interaction.construction.simulation.context.BuildContext
 
diff --git a/src/main/kotlin/com/lambda/interaction/managers/Manager.kt b/src/main/kotlin/com/lambda/interaction/manager/Manager.kt
similarity index 92%
rename from src/main/kotlin/com/lambda/interaction/managers/Manager.kt
rename to src/main/kotlin/com/lambda/interaction/manager/Manager.kt
index 7da159c1f..b70bbdec1 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/Manager.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/Manager.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.manager
 
 import com.lambda.context.Automated
 import com.lambda.context.AutomatedSafeContext
@@ -25,7 +25,7 @@ import com.lambda.event.Event
 import com.lambda.event.events.TickEvent
 import com.lambda.event.events.TickEvent.Companion.ALL_STAGES
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.ManagerUtils.accumulatedManagerPriority
+import com.lambda.interaction.manager.ManagerUtils.accumulatedManagerPriority
 import com.lambda.threading.runSafeAutomated
 import kotlin.reflect.KClass
 
@@ -35,9 +35,7 @@ import kotlin.reflect.KClass
  */
 abstract class Manager(
 	val stagePriority: Int,
-	vararg val blacklistedStages: TickEvent,
-	private val onOpen: (SafeContext.() -> Unit)? = null,
-	private val onClose: (SafeContext.() -> Unit)? = null
+	vararg val blacklistedStages: TickEvent
 ) : Loadable {
 	val openStages: List = ALL_STAGES.filter { it !in blacklistedStages }
 
@@ -86,15 +84,19 @@ abstract class Manager(
                 queuedRequest = null
             }
             acceptingRequests = true
-            onOpen?.invoke(this)
+            onOpen()
         }
 
         listen(instance, { (Int.MIN_VALUE + 1) + stagePriority }) {
-            onClose?.invoke(this)
+            onClose()
             acceptingRequests = false
         }
     }
 
+	protected open fun SafeContext.onOpen() {}
+
+	protected open fun SafeContext.onClose() {}
+
 	/**
 	 * Registers a new request
 	 *
diff --git a/src/main/kotlin/com/lambda/interaction/managers/ManagerUtils.kt b/src/main/kotlin/com/lambda/interaction/manager/ManagerUtils.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/interaction/managers/ManagerUtils.kt
rename to src/main/kotlin/com/lambda/interaction/manager/ManagerUtils.kt
index 9a5f193bd..ae2b3589c 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/ManagerUtils.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/ManagerUtils.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.manager
 
 import com.lambda.util.ReflectionUtils.getInstances
 import net.minecraft.util.math.BlockPos
diff --git a/src/main/kotlin/com/lambda/interaction/managers/PositionBlocking.kt b/src/main/kotlin/com/lambda/interaction/manager/PositionBlocking.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/interaction/managers/PositionBlocking.kt
rename to src/main/kotlin/com/lambda/interaction/manager/PositionBlocking.kt
index 14c3bf240..e10a9d2ba 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/PositionBlocking.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/PositionBlocking.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.manager
 
 import net.minecraft.util.math.BlockPos
 
diff --git a/src/main/kotlin/com/lambda/interaction/managers/Request.kt b/src/main/kotlin/com/lambda/interaction/manager/Request.kt
similarity index 97%
rename from src/main/kotlin/com/lambda/interaction/managers/Request.kt
rename to src/main/kotlin/com/lambda/interaction/manager/Request.kt
index 61fa20be2..9bdcd58c6 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/Request.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/Request.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers
+package com.lambda.interaction.manager
 
 import com.lambda.context.Automated
 import com.lambda.event.events.TickEvent
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakInfo.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakInfo.kt
similarity index 88%
rename from src/main/kotlin/com/lambda/interaction/managers/breaking/BreakInfo.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakInfo.kt
index 9a8367812..f789d0253 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakInfo.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakInfo.kt
@@ -15,19 +15,19 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.breaking
+package com.lambda.interaction.manager.managers.breaking
 
 import com.lambda.config.blocks.BreakConfig
 import com.lambda.config.blocks.BreakConfig.BreakMode
 import com.lambda.context.SafeContext
 import com.lambda.interaction.construction.simulation.context.BreakContext
-import com.lambda.interaction.handlers.breaking.RebreakHandler
-import com.lambda.interaction.managers.ActionInfo
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Primary
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Rebreak
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.RedundantSecondary
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Secondary
-import com.lambda.interaction.managers.breaking.BreakManager.calcBreakDelta
+import com.lambda.interaction.handler.handlers.breaking.RebreakPotential
+import com.lambda.interaction.manager.ActionInfo
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Primary
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Rebreak
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.RedundantSecondary
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Secondary
+import com.lambda.interaction.manager.managers.breaking.BreakManager.calcBreakDelta
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
@@ -51,7 +51,7 @@ data class BreakInfo(
 
 	// Pre Processing
 	var shouldProgress = false
-	var rebreakPotential = RebreakHandler.RebreakPotential.None
+	var rebreakPotential = RebreakPotential.None
 	var swapInfo = SwapInfo.EMPTY
 	var swapStack: ItemStack = ItemStack.EMPTY
 
@@ -163,7 +163,5 @@ data class BreakInfo(
 			}
 		}
 
-	fun getBreakDelay() = if (!bypassedDelay) 6 else breakConfig.breakDelay
-
 	override fun toString() = "$type, ${context.cachedState}, ${context.blockPos}"
 }
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakManager.kt
similarity index 88%
rename from src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakManager.kt
index edc52499e..b1d7726a0 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakManager.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.breaking
+package com.lambda.interaction.manager.managers.breaking
 
 import com.lambda.config.blocks.BreakConfig
 import com.lambda.config.blocks.BreakConfig.BreakConfirmationMode
@@ -34,47 +34,45 @@ import com.lambda.interaction.construction.simulation.context.BreakContext
 import com.lambda.interaction.construction.simulation.result.results.BreakResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler.destroyBlock
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler.pendingActions
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler.setPendingConfigs
-import com.lambda.interaction.handlers.breaking.BrokenBlockHandler.startPending
-import com.lambda.interaction.handlers.breaking.RebreakHandler
-import com.lambda.interaction.handlers.breaking.RebreakHandler.getRebreakPotential
-import com.lambda.interaction.handlers.breaking.RebreakResult
-import com.lambda.interaction.handlers.packet.PacketLimitHandler
-import com.lambda.interaction.handlers.packet.PacketType
-import com.lambda.interaction.managers.Manager
-import com.lambda.interaction.managers.ManagerUtils.isPosBlocked
-import com.lambda.interaction.managers.PositionBlocking
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Primary
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Rebreak
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.RedundantSecondary
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Secondary
-import com.lambda.interaction.managers.breaking.BreakManager.abandonedBreak
-import com.lambda.interaction.managers.breaking.BreakManager.activeInfos
-import com.lambda.interaction.managers.breaking.BreakManager.activeRequest
-import com.lambda.interaction.managers.breaking.BreakManager.breakInfos
-import com.lambda.interaction.managers.breaking.BreakManager.breaks
-import com.lambda.interaction.managers.breaking.BreakManager.canAccept
-import com.lambda.interaction.managers.breaking.BreakManager.checkForCancels
-import com.lambda.interaction.managers.breaking.BreakManager.handlePreProcessing
-import com.lambda.interaction.managers.breaking.BreakManager.hotbarRequest
-import com.lambda.interaction.managers.breaking.BreakManager.initNewBreak
-import com.lambda.interaction.managers.breaking.BreakManager.maxBreaksThisTick
-import com.lambda.interaction.managers.breaking.BreakManager.nullify
-import com.lambda.interaction.managers.breaking.BreakManager.populateFrom
-import com.lambda.interaction.managers.breaking.BreakManager.processNewBreak
-import com.lambda.interaction.managers.breaking.BreakManager.processRequest
-import com.lambda.interaction.managers.breaking.BreakManager.rotationRequest
-import com.lambda.interaction.managers.breaking.BreakManager.simulateAbandoned
-import com.lambda.interaction.managers.breaking.BreakManager.updateBreakProgress
-import com.lambda.interaction.managers.breaking.BreakManager.updatePreProcessing
-import com.lambda.interaction.managers.breaking.SwapInfo.Companion.getSwapInfo
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.interacting.InteractManager
-import com.lambda.interaction.managers.rotating.RotationRequest
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.StackSelection.Companion.select
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.handler.handlers.PacketLimitHandler
+import com.lambda.interaction.handler.handlers.PacketType
+import com.lambda.interaction.handler.handlers.breaking.BrokenBlockHandler
+import com.lambda.interaction.handler.handlers.breaking.BrokenBlockHandler.destroyBlock
+import com.lambda.interaction.handler.handlers.breaking.BrokenBlockHandler.pendingActions
+import com.lambda.interaction.handler.handlers.breaking.BrokenBlockHandler.startPending
+import com.lambda.interaction.handler.handlers.breaking.RebreakHandler
+import com.lambda.interaction.handler.handlers.breaking.RebreakHandler.getRebreakPotential
+import com.lambda.interaction.handler.handlers.breaking.RebreakResult
+import com.lambda.interaction.manager.Manager
+import com.lambda.interaction.manager.ManagerUtils.isPosBlocked
+import com.lambda.interaction.manager.PositionBlocking
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Primary
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Rebreak
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.RedundantSecondary
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Secondary
+import com.lambda.interaction.manager.managers.breaking.BreakManager.abandonedBreak
+import com.lambda.interaction.manager.managers.breaking.BreakManager.activeInfos
+import com.lambda.interaction.manager.managers.breaking.BreakManager.activeRequest
+import com.lambda.interaction.manager.managers.breaking.BreakManager.breakInfos
+import com.lambda.interaction.manager.managers.breaking.BreakManager.breaks
+import com.lambda.interaction.manager.managers.breaking.BreakManager.canAccept
+import com.lambda.interaction.manager.managers.breaking.BreakManager.handlePreProcessing
+import com.lambda.interaction.manager.managers.breaking.BreakManager.hotbarRequest
+import com.lambda.interaction.manager.managers.breaking.BreakManager.initNewBreak
+import com.lambda.interaction.manager.managers.breaking.BreakManager.maxBreaksThisTick
+import com.lambda.interaction.manager.managers.breaking.BreakManager.nullify
+import com.lambda.interaction.manager.managers.breaking.BreakManager.populateFrom
+import com.lambda.interaction.manager.managers.breaking.BreakManager.processNewBreak
+import com.lambda.interaction.manager.managers.breaking.BreakManager.processRequest
+import com.lambda.interaction.manager.managers.breaking.BreakManager.rotationRequest
+import com.lambda.interaction.manager.managers.breaking.BreakManager.updateBreakProgress
+import com.lambda.interaction.manager.managers.breaking.BreakManager.updatePreProcessing
+import com.lambda.interaction.manager.managers.breaking.SwapInfo.Companion.getSwapInfo
+import com.lambda.interaction.manager.managers.hotbar.HotbarRequest
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.interacting.InteractManager
+import com.lambda.interaction.manager.managers.rotating.RotationRequest
 import com.lambda.threading.runGameScheduled
 import com.lambda.threading.runSafe
 import com.lambda.threading.runSafeAutomated
@@ -105,14 +103,7 @@ import kotlin.math.max
  * different requests. Each break will be handled using its own config, and just like the other managers, priority is a first-come, first-served
  * style system.
  */
-object BreakManager : Manager(
-	0,
-	onOpen = {
-		processRequest(activeRequest)
-		simulateAbandoned()
-			 },
-	onClose = { checkForCancels() }
-), PositionBlocking {
+object BreakManager : Manager(0), PositionBlocking {
 	private val breakInfos = arrayOfNulls(2)
 
 	private val activeInfos
@@ -139,7 +130,7 @@ object BreakManager : Manager(
 			.lastOrNull {
 				it.breakConfig.doubleBreak || it.type == Secondary
 			}?.context?.itemSelection
-			?: StackSelection.EVERYTHING.select()
+			?: StackSelection.EVERYTHING
 
 	override val blockedPositions
 		get() = activeInfos.map { it.context.blockPos } + pendingActions.map { it.context.blockPos }
@@ -289,6 +280,15 @@ object BreakManager : Manager(
 		return "Loaded Break Manager"
 	}
 
+	override fun SafeContext.onOpen() {
+		processRequest(activeRequest)
+		simulateAbandoned()
+	}
+
+	override fun SafeContext.onClose() {
+		checkForCancels()
+	}
+
 	/**
 	 * Attempts to accept and process the request, if there is not already an [activeRequest] and the
 	 * [BreakRequest.contexts] collection is not empty. If nowOrNothing is true, the request is cleared
@@ -439,14 +439,13 @@ object BreakManager : Manager(
 				val minKeepTicks = if (first.swapInfo.longSwap || last.swapInfo.longSwap) 1 else 0
 				val serverSwapTicks = max(first.breakConfig.serverSwapTicks, last.breakConfig.serverSwapTicks)
 
-				hotbarRequest = with(last) {
-					HotbarRequest(
-						context.hotbarIndex,
-						request,
-						request.hotbarConfig.keepTicks.coerceAtLeast(minKeepTicks),
-						request.hotbarConfig.swapPause.coerceAtLeast(serverSwapTicks - 1)
-					).submit(false)
-				}
+				hotbarRequest =
+					with(last) {
+						request.hotbarRequest(context.hotbarIndex, nowOrNothing = true) {
+							keepTicks(request.hotbarConfig.keepTicks.coerceAtLeast(minKeepTicks))
+							swapPause(request.hotbarConfig.swapPause.coerceAtLeast(serverSwapTicks - 1))
+						}.submit(false)
+					}
 
 				return
 			}
@@ -469,7 +468,8 @@ object BreakManager : Manager(
 			if (breaksThisTick >= maxBreaksThisTick) return false
 
 			breaks.forEach { ctx ->
-				if (!currentStackSelection.filterStack(player.inventory.getStack(ctx.hotbarIndex))) return@forEach
+				if (breaksThisTick >= maxBreaksThisTick) return false
+				if (!currentStackSelection.matches(player.inventory.getStack(ctx.hotbarIndex))) return@forEach
 
 				initNewBreak(ctx, request) ?: return false
 				breaks.remove(ctx)
@@ -529,7 +529,7 @@ object BreakManager : Manager(
 		}
 
 		primaryBreak = breakInfo
-		setPendingConfigs()
+		BrokenBlockHandler.setPendingConfigs()
 		return primaryBreak
 	}
 
@@ -537,7 +537,7 @@ object BreakManager : Manager(
 	 * Simulates and updates the [abandonedBreak].
 	 */
 	private fun SafeContext.simulateAbandoned() {
-		// Canceled but double breaking so requires break manager to continue the simulation
+		// Cancelled but double breaking so requires break manager to continue the simulation
 		val abandonedInfo = abandonedBreak ?: return
 
 		abandonedInfo.request.runSafeAutomated {
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakRequest.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakRequest.kt
new file mode 100644
index 000000000..a0ecdcf31
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/BreakRequest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.breaking
+
+import com.lambda.context.Automated
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
+import com.lambda.interaction.construction.simulation.context.BreakContext
+import com.lambda.interaction.construction.simulation.context.BuildContext
+import com.lambda.interaction.construction.simulation.result.BuildResult
+import com.lambda.interaction.construction.simulation.result.Dependent
+import com.lambda.interaction.construction.simulation.result.results.BreakResult
+import com.lambda.interaction.construction.simulation.sim
+import com.lambda.interaction.construction.verify.TargetState
+import com.lambda.interaction.manager.Request
+import com.lambda.threading.runSafe
+import com.lambda.util.BlockUtils.blockState
+import com.lambda.util.BlockUtils.isEmpty
+import net.minecraft.entity.ItemEntity
+import net.minecraft.util.math.BlockPos
+
+@DslMarker
+annotation class BreakRequestMarker
+
+@BreakRequestMarker
+@JvmName("breakRequest1")
+fun AutomatedSafeContext.breakRequest(
+	positions: Collection,
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (BreakRequestBuilder.() -> Unit)? = null
+) = positions
+	.associateWith { TargetState.Empty }
+	.sim()
+	.breakRequest(pendingInteractions, nowOrNothing, builder)
+
+@BreakRequestMarker
+@JvmName("breakRequest2")
+context(automated: Automated)
+fun Collection.breakRequest(
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (BreakRequestBuilder.() -> Unit)? = null
+) = asSequence()
+	.breakRequest(pendingInteractions, nowOrNothing, builder)
+
+@BreakRequestMarker
+@JvmName("breakRequest3")
+context(automated: Automated)
+fun Sequence.breakRequest(
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (BreakRequestBuilder.() -> Unit)? = null
+) = map { if (it is Dependent) it.lastDependency else it }
+	.filterIsInstance()
+	.sorted()
+	.map { it.context }
+	.toSet()
+	.takeIf { it.isNotEmpty() }
+	?.let { automated.breakRequest(it, pendingInteractions, nowOrNothing, builder) }
+
+@BreakRequestMarker
+@JvmName("breakRequest4")
+fun Automated.breakRequest(
+	contexts: Collection,
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (BreakRequestBuilder.() -> Unit)? = null
+) = BreakRequestBuilder(contexts, pendingInteractions, nowOrNothing, this)
+	.apply { builder?.invoke(this) }
+	.build()
+
+/**
+ * Contains the information necessary for initializing and continuing breaks within the [BreakManager].
+ *
+ * The callbacks can be used to keep track of the break progress.
+ *
+ * A private constructor is used to force use of the cleaner [BreakRequestMarker] builder. This is
+ * accessed through the [breakRequest] method.
+ *
+ * @param contexts A collection of [BreakContext]'s gathered from the BuildSimulator.
+ * @param pendingInteractions A mutable, concurrent list to store the pending actions.
+ *
+ * @see com.lambda.interaction.construction.simulation.BuildSimulator
+ */
+data class BreakRequest(
+	val contexts: Collection,
+	val pendingInteractions: MutableCollection,
+	override val nowOrNothing: Boolean,
+	private val automated: Automated,
+	val onStart: (SafeContext.(BlockPos) -> Unit)?,
+	val onUpdate: (SafeContext.(BlockPos) -> Unit)?,
+	val onStop: (SafeContext.(BlockPos) -> Unit)?,
+	val onCancel: (SafeContext.(BlockPos) -> Unit)?,
+	val onItemDrop: (SafeContext.(ItemEntity) -> Unit)?,
+	val onReBreakStart: (SafeContext.(BlockPos) -> Unit)?,
+	val onReBreak: (SafeContext.(BlockPos) -> Unit)?
+) : Request(), Automated by automated {
+	override val requestId = ++requestCount
+	override val tickStageMask get() = breakConfig.tickStageMask
+
+	override val done: Boolean
+		get() = runSafe { contexts.all { blockState(it.blockPos).isEmpty } } == true
+
+	@BreakRequestMarker
+	override fun submit(queueIfMismatchedStage: Boolean) =
+		BreakManager.request(this, queueIfMismatchedStage)
+
+	companion object {
+		var requestCount = 0
+			private set
+	}
+}
+
+@BreakRequestMarker
+class BreakRequestBuilder internal constructor(
+	private val contexts: Collection,
+	private val pendingInteractions: MutableCollection,
+	private val nowOrNothing: Boolean,
+	private val automated: Automated
+) {
+	private var onStart: (SafeContext.(BlockPos) -> Unit)? = null
+	private var onUpdate: (SafeContext.(BlockPos) -> Unit)? = null
+	private var onStop: (SafeContext.(BlockPos) -> Unit)? = null
+	private var onCancel: (SafeContext.(BlockPos) -> Unit)? = null
+	private var onItemDrop: (SafeContext.(ItemEntity) -> Unit)? = null
+	private var onReBreakStart: (SafeContext.(BlockPos) -> Unit)? = null
+	private var onReBreak: (SafeContext.(BlockPos) -> Unit)? = null
+
+	fun onStart(callback: SafeContext.(BlockPos) -> Unit) {
+		onStart = callback
+	}
+
+	fun onUpdate(callback: SafeContext.(BlockPos) -> Unit) {
+		onUpdate = callback
+	}
+
+	fun onStop(callback: SafeContext.(BlockPos) -> Unit) {
+		onStop = callback
+	}
+
+	fun onCancel(callback: SafeContext.(BlockPos) -> Unit) {
+		onCancel = callback
+	}
+
+	fun onItemDrop(callback: SafeContext.(ItemEntity) -> Unit) {
+		onItemDrop = callback
+	}
+
+	fun onReBreakStart(callback: SafeContext.(BlockPos) -> Unit) {
+		onReBreakStart = callback
+	}
+
+	fun onReBreak(callback: SafeContext.(BlockPos) -> Unit) {
+		onReBreak = callback
+	}
+
+	internal fun build() =
+		BreakRequest(
+			contexts,
+			pendingInteractions,
+			nowOrNothing,
+			automated,
+			onStart,
+			onUpdate,
+			onStop,
+			onCancel,
+			onItemDrop,
+			onReBreakStart,
+			onReBreak
+		)
+}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/SwapInfo.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/SwapInfo.kt
similarity index 87%
rename from src/main/kotlin/com/lambda/interaction/managers/breaking/SwapInfo.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/breaking/SwapInfo.kt
index c3f0b4202..c0b3a44a3 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/SwapInfo.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/breaking/SwapInfo.kt
@@ -15,16 +15,16 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.breaking
+package com.lambda.interaction.manager.managers.breaking
 
 import com.lambda.config.automation.AutomationConfig.Companion.DEFAULT
 import com.lambda.config.blocks.BreakConfig
 import com.lambda.context.Automated
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.breaking.RebreakHandler
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Primary
-import com.lambda.interaction.managers.breaking.BreakInfo.BreakType.Secondary
-import com.lambda.interaction.managers.breaking.BreakManager.calcBreakDelta
+import com.lambda.interaction.handler.handlers.breaking.RebreakHandler
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Primary
+import com.lambda.interaction.manager.managers.breaking.BreakInfo.BreakType.Secondary
+import com.lambda.interaction.manager.managers.breaking.BreakManager.calcBreakDelta
 import com.lambda.threading.runSafeAutomated
 
 /**
diff --git a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarManager.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarManager.kt
similarity index 89%
rename from src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarManager.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarManager.kt
index ab53fe97c..bd536ccc9 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarManager.kt
@@ -15,19 +15,18 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.hotbar
+package com.lambda.interaction.manager.managers.hotbar
 
 import com.lambda.config.blocks.HotbarConfig.SwapMode
 import com.lambda.context.AutomatedSafeContext
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.Manager
-import com.lambda.interaction.managers.hotbar.HotbarManager.activeRequest
-import com.lambda.interaction.managers.hotbar.HotbarManager.activeSlot
-import com.lambda.interaction.managers.hotbar.HotbarManager.checkResetSwap
-import com.lambda.interaction.managers.hotbar.HotbarManager.setActiveRequest
-import com.lambda.interaction.managers.hotbar.HotbarManager.setActiveSlot
+import com.lambda.interaction.manager.Manager
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager.activeRequest
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager.activeSlot
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager.setActiveRequest
+import com.lambda.interaction.manager.managers.hotbar.HotbarManager.setActiveSlot
 import com.lambda.threading.runSafe
 import net.minecraft.item.ItemStack
 
@@ -39,15 +38,7 @@ import net.minecraft.item.ItemStack
  * set to 0, assuming the request is accepted, the manager will only swap for the duration of the current [tickStage].
  * After which, the manager will end the request and swap back to the player's selected slot.
  */
-object HotbarManager : Manager(
-	1,
-	onOpen = {
-		if (activeRequest != null) {
-			setActiveSlot()
-		}
-			 },
-	onClose = { checkResetSwap() }
-) {
+object HotbarManager : Manager(1) {
 	private var activeRequest: HotbarRequest? = null
 	@JvmStatic var activeSlot: Int = -1
 
@@ -84,6 +75,14 @@ object HotbarManager : Manager(
 		return "Loaded Hotbar Manager"
 	}
 
+	override fun SafeContext.onOpen() {
+		if (activeRequest != null) setActiveSlot()
+	}
+
+	override fun SafeContext.onClose() {
+		checkResetSwap()
+	}
+
 	/**
 	 * If the [activeRequest] is not null, being kept, and the [request]'s slot matches the [activeSlot], the
 	 * [request]'s swapPauseAge is set to the swapped ticks and the request is denied. Otherwise, the active
diff --git a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarRequest.kt
similarity index 65%
rename from src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarRequest.kt
index af9a22ef4..9b889d6fa 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/hotbar/HotbarRequest.kt
@@ -15,10 +15,22 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.hotbar
+package com.lambda.interaction.manager.managers.hotbar
 
 import com.lambda.context.Automated
-import com.lambda.interaction.managers.Request
+import com.lambda.interaction.manager.Request
+
+@DslMarker
+private annotation class HotbarRequestMarker
+
+@HotbarRequestMarker
+fun Automated.hotbarRequest(
+	slot: Int,
+	nowOrNothing: Boolean = false,
+	builder: (HotbarRequestBuilder.() -> Unit)? = null
+) = HotbarRequestBuilder(slot, nowOrNothing, this)
+	.apply { builder?.invoke(this) }
+	.build()
 
 /**
  * A request to change the hotbar slot.
@@ -34,10 +46,10 @@ import com.lambda.interaction.managers.Request
  */
 class HotbarRequest(
 	val slot: Int,
-	automated: Automated,
 	var keepTicks: Int = automated.hotbarConfig.keepTicks,
 	val swapPause: Int = automated.hotbarConfig.swapPause,
-	override val nowOrNothing: Boolean = true
+	override val nowOrNothing: Boolean = false,
+	automated: Automated
 ) : Request(), Automated by automated {
 	override val requestId = ++requestCount
 	override val tickStageMask get() = hotbarConfig.tickStageMask
@@ -48,10 +60,33 @@ class HotbarRequest(
 	override val done: Boolean
 		get() = slot == HotbarManager.activeSlot && swapPauseAge >= swapPause
 
+	@HotbarRequestMarker
 	override fun submit(queueIfMismatchedStage: Boolean) =
 		HotbarManager.request(this, queueIfMismatchedStage)
 
 	companion object {
 		var requestCount = 0
+			private set
 	}
 }
+
+@Suppress("unused")
+@HotbarRequestMarker
+class HotbarRequestBuilder internal constructor(
+	private val slot: Int,
+	private val nowOrNothing: Boolean,
+	private val automated: Automated,
+) {
+	private var keepTicks: Int = automated.hotbarConfig.keepTicks
+	private var swapPause: Int = automated.hotbarConfig.swapPause
+
+	fun keepTicks(keepTicks: Int) {
+		this.keepTicks = keepTicks
+	}
+
+	fun swapPause(swapPause: Int) {
+		this.swapPause = swapPause
+	}
+
+	internal fun build() = HotbarRequest(slot, keepTicks, swapPause, nowOrNothing, automated)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractInfo.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractInfo.kt
similarity index 91%
rename from src/main/kotlin/com/lambda/interaction/managers/interacting/InteractInfo.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractInfo.kt
index b4e813267..a0401782e 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractInfo.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractInfo.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.interacting
+package com.lambda.interaction.manager.managers.interacting
 
 import com.lambda.config.blocks.InteractConfig
 import com.lambda.context.SafeContext
 import com.lambda.interaction.construction.simulation.context.BuildContext
 import com.lambda.interaction.construction.simulation.context.InteractContext
-import com.lambda.interaction.managers.ActionInfo
+import com.lambda.interaction.manager.ActionInfo
 import net.minecraft.util.math.BlockPos
 
 data class InteractInfo(
diff --git a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractManager.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractManager.kt
similarity index 86%
rename from src/main/kotlin/com/lambda/interaction/managers/interacting/InteractManager.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractManager.kt
index b016527b6..61a4669f0 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractManager.kt
@@ -15,10 +15,9 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.interacting
+package com.lambda.interaction.manager.managers.interacting
 
 import com.lambda.config.blocks.InteractConfig
-import com.lambda.config.blocks.InteractConfig.AirPlaceMode
 import com.lambda.context.Automated
 import com.lambda.context.AutomatedSafeContext
 import com.lambda.context.SafeContext
@@ -29,23 +28,22 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
 import com.lambda.interaction.construction.simulation.context.InteractContext
-import com.lambda.interaction.handlers.interacting.InteractedBlockHandler.pendingActions
-import com.lambda.interaction.handlers.interacting.InteractedBlockHandler.setPendingConfigs
-import com.lambda.interaction.handlers.interacting.InteractedBlockHandler.startPending
-import com.lambda.interaction.handlers.packet.PacketLimitHandler
-import com.lambda.interaction.handlers.packet.PacketType
-import com.lambda.interaction.managers.Manager
-import com.lambda.interaction.managers.ManagerUtils.isPosBlocked
-import com.lambda.interaction.managers.PositionBlocking
-import com.lambda.interaction.managers.breaking.BreakManager
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.interacting.InteractManager.activeRequest
-import com.lambda.interaction.managers.interacting.InteractManager.maxInteractionsThisTick
-import com.lambda.interaction.managers.interacting.InteractManager.populateFrom
-import com.lambda.interaction.managers.interacting.InteractManager.potentialInteractions
-import com.lambda.interaction.managers.interacting.InteractManager.processRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.module.modules.world.AutoSign.signWriteDelay
+import com.lambda.interaction.handler.handlers.InteractedBlockHandler
+import com.lambda.interaction.handler.handlers.InteractedBlockHandler.startPending
+import com.lambda.interaction.handler.handlers.PacketLimitHandler
+import com.lambda.interaction.handler.handlers.PacketType
+import com.lambda.interaction.manager.Manager
+import com.lambda.interaction.manager.ManagerUtils.isPosBlocked
+import com.lambda.interaction.manager.PositionBlocking
+import com.lambda.interaction.manager.managers.breaking.BreakManager
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.interacting.InteractManager.activeRequest
+import com.lambda.interaction.manager.managers.interacting.InteractManager.maxInteractionsThisTick
+import com.lambda.interaction.manager.managers.interacting.InteractManager.populateFrom
+import com.lambda.interaction.manager.managers.interacting.InteractManager.potentialInteractions
+import com.lambda.interaction.manager.managers.interacting.InteractManager.processRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
+import com.lambda.module.modules.world.AutoSign
 import com.lambda.threading.runConcurrent
 import com.lambda.threading.runSafeAutomated
 import com.lambda.threading.runSafeGameScheduled
@@ -69,16 +67,12 @@ import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket
 import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket
 import net.minecraft.sound.SoundCategory
 import net.minecraft.util.ActionResult
-import net.minecraft.util.ActionResult.PassToDefaultBlockAction
 import net.minecraft.util.Hand
 import net.minecraft.util.hit.BlockHitResult
 import net.minecraft.util.math.BlockPos
 import net.minecraft.world.GameMode
 
-object InteractManager : Manager(
-	0,
-	onOpen = { activeRequest?.let { it.runSafeAutomated { processRequest(it) } } }
-), PositionBlocking {
+object InteractManager : Manager(0), PositionBlocking {
 	private var activeRequest: InteractRequest? = null
 	private var potentialInteractions = mutableListOf()
 
@@ -90,7 +84,7 @@ object InteractManager : Manager(
 	private val ClientPlayerEntity.validSneak get() = isSneaking == shouldSneak
 
 	override val blockedPositions
-		get() = pendingActions.map { it.context.blockPos }
+		get() = InteractedBlockHandler.pendingActions.map { it.context.blockPos }
 
 	private var cancellingSignScreens = 0
 
@@ -127,8 +121,14 @@ object InteractManager : Manager(
 		return "Loaded Place Manager"
 	}
 
+	override fun SafeContext.onOpen() {
+		activeRequest?.let {
+			it.runSafeAutomated { processRequest(it) }
+		}
+	}
+
 	/**
-	 * Accepts, and processes the request, as long as the current [activeRequest] is null, and the [BreakManager] has not
+	 * Accepts, and processes the request, as long as the current [activeRequest] is null, and the [com.lambda.interaction.manager.managers.breaking.BreakManager] has not
 	 * been active this tick. If nowOrNothing is true, the request is cleared after the first process.
 	 *
 	 * @see processRequest
@@ -162,14 +162,12 @@ object InteractManager : Manager(
 				if (!canInteractThisTick()) break
 				val firstInteraction = potentialInteractions.first()
 				if (player.inventory.selectedSlot != firstInteraction.hotbarIndex) {
-					val hotbarRequest = HotbarRequest(
-						firstInteraction.hotbarIndex,
-						this
-					).submit(queueIfMismatchedStage = false)
+					val hotbarRequest = hotbarRequest(firstInteraction.hotbarIndex, nowOrNothing = true)
+						.submit(false)
 					if (!hotbarRequest.done) break
 				}
 				var interactResult: InteractResult? = null
-				if (interactConfig.airPlace == AirPlaceMode.Grim) {
+				if (interactConfig.airPlace == InteractConfig.AirPlaceMode.Grim) {
 					val inventoryRequest = inventoryRequest {
 						swapHands()
 						action { interactResult = performInteractions(request) }
@@ -197,7 +195,7 @@ object InteractManager : Manager(
 			if (!player.validSneak) return InteractResult.CompleteFailure
 			if (tickStage !in interactConfig.tickStageMask) return InteractResult.CompleteFailure
 
-			val hand = if (interactConfig.airPlace == AirPlaceMode.Grim) Hand.OFF_HAND else Hand.MAIN_HAND
+			val hand = if (interactConfig.airPlace == InteractConfig.AirPlaceMode.Grim) Hand.OFF_HAND else Hand.MAIN_HAND
 			val actionResult =
 				if (ctx.preProcessingInfo.placing) placeBlock(ctx, request, hand)
 				else interaction.interactBlock(player, if (ctx.preProcessingInfo.item != null) hand else Hand.MAIN_HAND, ctx.hitResult)
@@ -217,7 +215,7 @@ object InteractManager : Manager(
 				if (ctx.preProcessingInfo.placing && expectedState.block is AbstractSignBlock) {
 					cancellingSignScreens++
 					runConcurrent {
-						delay(signWriteDelay)
+						delay(AutoSign.signWriteDelay)
 						runSafeGameScheduled {
 							connection.sendPacket {
 								UpdateSignC2SPacket(ctx.blockPos, true, "", "", "", "")
@@ -246,7 +244,7 @@ object InteractManager : Manager(
 	 * @see isPosBlocked
 	 */
 	private fun Automated.populateFrom(request: InteractRequest) {
-		setPendingConfigs()
+		InteractedBlockHandler.setPendingConfigs()
 		potentialInteractions = request.contexts
 			.distinctBy { it.blockPos }
 			.filter { !isPosBlocked(it.blockPos) }
@@ -290,7 +288,7 @@ object InteractManager : Manager(
 			val actionResult = blockState.onUseWithItem(player.getStackInHand(hand), world, player, hand, hitResult)
 			if (actionResult.isAccepted) return actionResult
 
-			if (actionResult is PassToDefaultBlockAction && hand == Hand.MAIN_HAND) {
+			if (actionResult is ActionResult.PassToDefaultBlockAction && hand == Hand.MAIN_HAND) {
 				val actionResult2 = blockState.onUse(world, player, hitResult)
 				if (actionResult2.isAccepted) return actionResult2
 			}
@@ -414,4 +412,4 @@ object InteractManager : Manager(
 		WrongSlot,
 		CompleteFailure
 	}
-}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractRequest.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractRequest.kt
new file mode 100644
index 000000000..8f7f1289b
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/interacting/InteractRequest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.interacting
+
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.construction.simulation.context.BuildContext
+import com.lambda.interaction.construction.simulation.context.InteractContext
+import com.lambda.interaction.construction.simulation.result.BuildResult
+import com.lambda.interaction.construction.simulation.result.Dependent
+import com.lambda.interaction.construction.simulation.result.results.InteractResult
+import com.lambda.interaction.manager.Request
+import com.lambda.threading.runSafe
+import com.lambda.util.BlockUtils.blockState
+import com.lambda.util.BlockUtils.matches
+import net.minecraft.util.math.BlockPos
+
+@DslMarker
+annotation class InteractRequestMarker
+
+@InteractRequestMarker
+@JvmName("interactRequest1")
+context(automated: Automated)
+fun Collection.interactRequest(
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (PlaceRequestBuilder.() -> Unit)? = null
+) = asSequence()
+	.interactRequest(pendingInteractions, nowOrNothing, builder)
+
+@InteractRequestMarker
+@JvmName("interactRequest2")
+context(automated: Automated)
+fun Sequence.interactRequest(
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (PlaceRequestBuilder.() -> Unit)? = null
+) = map { if (it is Dependent) it.lastDependency else it }
+	.filterIsInstance()
+	.sorted()
+	.map { it.context }
+	.toSet()
+	.takeIf { it.isNotEmpty() }
+	?.let { automated.interactRequest(it, pendingInteractions, nowOrNothing, builder) }
+
+@InteractRequestMarker
+@JvmName("interactRequest3")
+fun Automated.interactRequest(
+	contexts: Collection,
+	pendingInteractions: MutableCollection,
+	nowOrNothing: Boolean = false,
+	builder: (PlaceRequestBuilder.() -> Unit)? = null
+) = PlaceRequestBuilder(
+	contexts,
+	pendingInteractions,
+	nowOrNothing,
+	this
+).apply { builder?.invoke(this) }.build()
+
+data class InteractRequest(
+	val contexts: Collection,
+	val pendingInteractions: MutableCollection,
+	override val nowOrNothing: Boolean = false,
+	private val automated: Automated,
+	val onPlace: (SafeContext.(BlockPos) -> Unit)?
+) : Request(), Automated by automated {
+	override val requestId = ++requestCount
+	override val tickStageMask get() = interactConfig.tickStageMask
+
+	override val done: Boolean
+		get() = runSafe {
+			contexts.all { it.expectedState.matches(blockState(it.blockPos)) }
+		} == true
+
+	@InteractRequestMarker
+	override fun submit(queueIfMismatchedStage: Boolean) =
+		InteractManager.request(this, queueIfMismatchedStage)
+
+	companion object {
+		var requestCount = 0
+			private set
+	}
+}
+
+@InteractRequestMarker
+class PlaceRequestBuilder internal constructor(
+	private val contexts: Collection,
+	private val pendingInteractions: MutableCollection,
+	private val nowOrNothing: Boolean,
+	private val automated: Automated
+) {
+	private var onPlace: (SafeContext.(BlockPos) -> Unit)? = null
+
+	fun onPlace(callback: SafeContext.(BlockPos) -> Unit) {
+		onPlace = callback
+	}
+
+	internal fun build() =
+		InteractRequest(
+			contexts,
+			pendingInteractions,
+			nowOrNothing,
+			automated,
+			onPlace
+		)
+}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryAction.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryAction.kt
similarity index 95%
rename from src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryAction.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryAction.kt
index 33f4559a6..ebf22474f 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryAction.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryAction.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.inventory
+package com.lambda.interaction.manager.managers.inventory
 
 import com.lambda.context.SafeContext
 
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryLedger.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryLedger.kt
new file mode 100644
index 000000000..2eaf6e4e1
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryLedger.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.inventory
+
+import kotlin.time.Clock
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Instant
+
+class InventoryLedger(
+    private val equal: (a: T, b: T) -> Boolean,
+    var maxAgeMs: Long = 1500L,
+    private val maxDepthPerSlot: Int = 64,
+    private val clock: () -> Instant = { Clock.System.now() }
+) {
+    data class Pending(val before: T, val after: T, val recordedAt: Instant)
+
+    enum class SingleDecision {
+        Drop,
+        Apply
+    }
+
+    companion object {
+        const val CURSOR_SLOT_ID = -1
+    }
+
+    private val queues = mutableMapOf>>>()
+
+    @Synchronized
+    fun record(syncId: Int, slotId: Int, before: T, after: T) {
+        if (equal(before, after)) return
+        cleanup()
+        val queue = queues.getOrPut(syncId) { mutableMapOf() }.getOrPut(slotId) { ArrayDeque() }
+        queue.addLast(Pending(before, after, clock()))
+        while (queue.size > maxDepthPerSlot) queue.removeFirst()
+    }
+
+    @Synchronized
+    fun decideSingle(syncId: Int, slotId: Int, incoming: T): SingleDecision {
+        cleanup()
+        val queue = queues[syncId]?.get(slotId) ?: return SingleDecision.Apply
+        val match = queue.indexOfFirst { equal(it.after, incoming) }
+        return if (match < 0) {
+            removeQueue(syncId, slotId)
+            SingleDecision.Apply
+        } else {
+            repeat(match + 1) { queue.removeFirst() }
+            if (queue.isEmpty()) removeQueue(syncId, slotId)
+            SingleDecision.Drop
+        }
+    }
+
+    @Synchronized
+    fun decideCursor(syncId: Int, incoming: T): SingleDecision {
+        cleanup()
+        val queue = queues[syncId]?.get(CURSOR_SLOT_ID) ?: return SingleDecision.Apply
+        if (queue.any { equal(it.after, incoming) }) {
+            consumeFirstAfterMatch(queue, incoming)
+            if (queue.isEmpty()) removeQueue(syncId, CURSOR_SLOT_ID)
+            return SingleDecision.Drop
+        }
+        if (queue.any { equal(it.before, incoming) }) return SingleDecision.Drop
+        removeQueue(syncId, CURSOR_SLOT_ID)
+        return SingleDecision.Apply
+    }
+
+    @Synchronized
+    fun decideFull(syncId: Int, incoming: List, client: List): List {
+        if (incoming.size != client.size) return List(incoming.size) { true }
+        cleanup()
+        return incoming.indices.map { index ->
+            val queue = queues[syncId]?.get(index)
+            if (queue.isNullOrEmpty()) {
+                true
+            } else if (equal(incoming[index], client[index])) {
+                consumeFirstAfterMatch(queue, incoming[index])
+                if (queue.isEmpty()) removeQueue(syncId, index)
+                true
+            } else {
+                val match = queue.indexOfFirst { equal(it.after, incoming[index]) }
+                when {
+                    match >= 0 -> {
+                        consumeFirstAfterMatch(queue, incoming[index])
+                        if (queue.isEmpty()) removeQueue(syncId, index)
+                        false
+                    }
+                    queue.any { equal(it.before, incoming[index]) } -> false
+                    else -> {
+                        removeQueue(syncId, index)
+                        true
+                    }
+                }
+            }
+        }
+    }
+
+    @Synchronized
+    fun clear(syncId: Int) {
+        queues.remove(syncId)
+    }
+
+    @Synchronized
+    fun pendingCount(syncId: Int, slotId: Int): Int = queues[syncId]?.get(slotId)?.size ?: 0
+
+    @Synchronized
+    fun totalPending(): Int = queues.values.sumOf { perSync -> perSync.values.sumOf { it.size } }
+
+    private fun consumeFirstAfterMatch(queue: ArrayDeque>, value: T) {
+        val match = queue.indexOfFirst { equal(it.after, value) }
+        if (match >= 0) repeat(match + 1) { queue.removeFirst() }
+    }
+
+    private fun removeQueue(syncId: Int, slotId: Int) {
+        val perSync = queues[syncId] ?: return
+        perSync.remove(slotId)
+        if (perSync.isEmpty()) queues.remove(syncId)
+    }
+
+    private fun cleanup() {
+        if (maxAgeMs <= 0) {
+            queues.clear()
+            return
+        }
+        val cutoff = clock().minus(maxAgeMs.milliseconds)
+        val emptySyncs = mutableListOf()
+        queues.forEach { (syncId, perSync) ->
+            val emptySlots = mutableListOf()
+            perSync.forEach { (slotId, queue) ->
+                while (queue.isNotEmpty() && queue.first().recordedAt < cutoff) queue.removeFirst()
+                if (queue.isEmpty()) emptySlots += slotId
+            }
+            emptySlots.forEach { perSync.remove(it) }
+            if (perSync.isEmpty()) emptySyncs += syncId
+        }
+        emptySyncs.forEach { queues.remove(it) }
+    }
+}
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryManager.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryManager.kt
new file mode 100644
index 000000000..e9bea9b45
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryManager.kt
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.inventory
+
+import com.lambda.Lambda.LOG
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
+import com.lambda.event.events.InventoryEvent
+import com.lambda.event.events.PacketEvent
+import com.lambda.event.events.PlayerEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.handler.handlers.PacketLimitHandler.canSendPackets
+import com.lambda.interaction.handler.handlers.PacketLimitHandler.sentPackets
+import com.lambda.interaction.handler.handlers.PacketType
+import com.lambda.interaction.manager.Manager
+import com.lambda.interaction.manager.managers.inventory.InventoryManager.actions
+import com.lambda.interaction.manager.managers.inventory.InventoryManager.activeRequest
+import com.lambda.interaction.manager.managers.inventory.InventoryManager.filter
+import com.lambda.interaction.manager.managers.inventory.InventoryManager.indexInventoryChanges
+import com.lambda.interaction.manager.managers.inventory.InventoryManager.onInventoryUpdate
+import com.lambda.module.modules.client.Client
+import com.lambda.module.modules.client.Client.verboseDebug
+import com.lambda.threading.runSafe
+import com.lambda.threading.runSafeAutomated
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation
+import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen
+import net.minecraft.item.ItemStack
+import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
+import net.minecraft.network.packet.s2c.play.InventoryS2CPacket
+import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket
+import net.minecraft.network.packet.s2c.play.SetCursorItemS2CPacket
+import net.minecraft.screen.PlayerScreenHandler
+import net.minecraft.screen.ScreenHandler
+import kotlin.time.Instant
+
+/**
+ * Manager designed to handle inventory actions. One of the key features being the inventory change detection to
+ * avoid accepting old information from the server in cases where ping is high. This helps to prevent desync.
+ *
+ * Every local prediction (clicks through this manager, plus anything else the client predicts such as block
+ * placement, which is picked up by the tick diff in [indexInventoryChanges]) is recorded in the [filter] as an
+ * ordered per-slot queue. Server answers that match a recording are echoes of our own actions and are dropped;
+ * anything else is new information and is applied. See [InventorySyncFilter] for the exact rules.
+ */
+object InventoryManager : Manager(1) {
+	private var activeRequest: InventoryRequest? = null
+	private var actions = mutableListOf()
+
+	private val filter =
+		InventorySyncFilter(
+			{ a, b -> ItemStack.areEqual(a, b) },
+			{ it.copy() },
+			Client.desyncTimeout * 50L
+		)
+
+	private var actionsThisTick = 0
+
+	override fun load(): String {
+		super.load()
+
+        listen({ Int.MIN_VALUE }) {
+            if (Client.avoidInventoryDesync) indexInventoryChanges()
+            actionsThisTick = 0
+            activeRequest = null
+            actions = mutableListOf()
+        }
+
+		listen {
+			if (Client.avoidInventoryDesync) indexInventoryChanges()
+		}
+
+		listen { event ->
+			if (event.packet is CloseHandledScreenC2SPacket) {
+				onSetScreenHandler(player.playerScreenHandler)
+			}
+		}
+
+		listen {
+			onSetScreenHandler(player.currentScreenHandler)
+		}
+
+		return "Loaded Inventory Manager"
+	}
+
+	/**
+	 * Attempts to accept the request and perform the actions. If, for example, the tick stage isn't valid, or
+	 * not all the actions can be performed and [InventoryRequest.settleForLess] is set to false, the request is rejected.
+	 * All checks aside from tick stage are ignored if the request has [InventoryRequest.mustPerform] set to true.
+	 * This is typically used in dangerous situations where typical rules are worth breaking. For example, if the player
+	 * needs to equip a totem of undying.
+	 */
+	override fun AutomatedSafeContext.handleRequest(request: InventoryRequest) {
+		if (activeRequest != null) {
+			request.failureReason = InvRequestFailureReason.Preoccupied
+			return
+		}
+
+		val playerActionCount = request.actions.count { it is InventoryAction.Player }
+		val inventoryActionCount = request.actions.count { it is InventoryAction.Inventory }
+		val canPerformAllPlayerActions = canSendPackets(playerActionCount, PacketType.PlayerAction)
+		val canPerformAllInventoryActions = canSendPackets(inventoryActionCount, PacketType.Inventory)
+		if ((!canPerformAllPlayerActions || !canPerformAllInventoryActions) &&
+			!request.settleForLess &&
+			!request.mustPerform
+		) {
+			request.failureReason = InvRequestFailureReason.PacketLimit
+			return
+		}
+
+		if (request.fresh) populateFrom(request)
+
+		processActiveRequest()
+		if (request.nowOrNothing) {
+			activeRequest = null
+			actions = mutableListOf()
+		}
+	}
+
+	override fun SafeContext.onOpen() {
+		processActiveRequest()
+	}
+
+	private fun populateFrom(request: InventoryRequest) {
+		activeRequest = request
+		actions = request.actions.toMutableList()
+		filter.maxAgeMs = Client.desyncTimeout * 50L
+	}
+
+	/**
+	 * Attempts to perform as many actions as possible from the [actions] collection. If
+	 * [actions] is empty, the request is set to done, and the onComplete callback is invoked.
+	 * The [activeRequest] is then set to null.
+	 */
+	private fun SafeContext.processActiveRequest() {
+		val active = activeRequest ?: return
+		active.runSafeAutomated {
+			if (tickStage !in active.inventoryConfig.tickStageMask && active.nowOrNothing) return
+			val iterator = actions.iterator()
+			while (iterator.hasNext()) {
+				val action = iterator.next()
+				if (action is InventoryAction.Player && !canSendPackets(1, PacketType.PlayerAction)) break
+				else if (action is InventoryAction.Inventory && !canSendPackets(1, PacketType.Inventory)) break
+				action.action(this)
+				if (action is InventoryAction.Player) sentPackets(1, PacketType.PlayerAction)
+				else if (action is InventoryAction.Inventory) sentPackets(1, PacketType.Inventory)
+				if (Client.avoidInventoryDesync) indexInventoryChanges()
+				actionsThisTick++
+				iterator.remove()
+			}
+
+			if (actions.isEmpty()) {
+				active.done = true
+				active.onComplete?.invoke(this)
+				activeRequest = null
+			} else {
+				active.failureReason = InvRequestFailureReason.PacketLimit
+			}
+
+			if (actionsThisTick > 0) activeThisTick = true
+		}
+	}
+
+	/**
+	 * Diffs the live screen against the last snapshot and records every changed slot in the [filter]
+	 * in the order it happened. This runs after every performed action and at the end of every tick,
+	 * so predictions made outside this manager (placing blocks, eating, picking items up) are recorded
+	 * too, as long as their server echo arrives later than the next recording point.
+	 *
+	 * The cursor is tracked the same way: every click sends a mismatched revision (-1), so the server
+	 * answers with a full [InventoryS2CPacket] carrying the cursor, and with rapid clicks that cursor
+	 * is routinely stale by the time it arrives. Merging it like a slot keeps a resurrected cursor
+	 * from breaking the transfer tasks that read it every tick.
+	 *
+	 * Note the click path sends a mismatched revision (-1), so the server answers a click with a full
+	 * [InventoryS2CPacket]; that packet is merged per slot in [onInventoryUpdate], which is why each
+	 * change is recorded exactly once here.
+	 */
+	private fun SafeContext.indexInventoryChanges() {
+		val current = player.currentScreenHandler
+		val recorded = filter.observeLocal(
+			current.syncId,
+			current.slots.associate { it.id to it.stack },
+			current.cursorStack
+		)
+		if (verboseDebug && recorded > 0) LOG.info("[desync] recorded $recorded change(s) on sync=${current.syncId}")
+	}
+
+	private fun snapshotHandler(handler: ScreenHandler) {
+		filter.snapshot(
+			handler.syncId,
+			handler.slots.associate { it.id to it.stack },
+			handler.cursorStack
+		)
+	}
+
+	/**
+	 * A modified version of the minecraft onScreenHandlerSlotUpdate method
+	 *
+	 * @see net.minecraft.client.network.ClientPlayNetworkHandler.onScreenHandlerSlotUpdate
+	 */
+	@JvmStatic
+	fun onSlotUpdate(packet: ScreenHandlerSlotUpdateS2CPacket, original: Operation) {
+		runSafe {
+			val itemStack = packet.stack
+			mc.tutorialManager.onSlotUpdate(itemStack)
+
+			val packetScreenHandler =
+				when (packet.syncId) {
+					0 -> player.playerScreenHandler
+					player.currentScreenHandler.syncId -> player.currentScreenHandler
+					else -> {
+						if (verboseDebug) LOG.info("[desync] single ignored: packet sync=${packet.syncId} slot=${packet.slot} current=${player.currentScreenHandler.syncId} stack=${packet.stack}")
+						original.call(packet)
+						return
+					}
+				}
+			val current = packetScreenHandler.slots.getOrNull(packet.slot)?.stack
+			if (current == null) {
+				original.call(packet)
+				return
+			}
+			if (!mc.isOnThread || !Client.avoidInventoryDesync) {
+				original.call(packet)
+				snapshotHandler(packetScreenHandler)
+				return
+			}
+
+			val bl = (mc.currentScreen as? CreativeInventoryScreen)?.let {
+				!it.isInventoryTabSelected
+			} ?: false
+
+			val apply = filter.onSingle(packet.syncId, packet.slot, itemStack, current)
+
+			if (packet.syncId == 0) {
+				if (PlayerScreenHandler.isInHotbar(packet.slot) && !itemStack.isEmpty) {
+					val itemStack2 = player.playerScreenHandler.getSlot(packet.slot).stack
+					if (itemStack2.isEmpty || itemStack2.count < itemStack.count) {
+						itemStack.bobbingAnimationTime = 5
+					}
+				}
+
+				if (!apply) player.playerScreenHandler.revision = packet.revision
+				else player.playerScreenHandler.setStackInSlot(packet.slot, packet.revision, itemStack)
+			} else if (packet.syncId == player.currentScreenHandler.syncId && (packet.syncId != 0 || !bl)) {
+				if (!apply) player.currentScreenHandler.revision = packet.revision
+				else player.currentScreenHandler.setStackInSlot(packet.slot, packet.revision, itemStack)
+			}
+
+			if (mc.currentScreen is CreativeInventoryScreen) {
+				player.playerScreenHandler.setReceivedStack(packet.slot, itemStack)
+				player.playerScreenHandler.sendContentUpdates()
+			}
+			return
+		}
+		original.call(packet)
+	}
+
+	/**
+	 * A modified version of the minecraft onSetCursorItem method. Cursor-only updates bypass
+	 * the full packets, so without this every one of them is applied blindly — including ones
+	 * the server generated before our latest click (see [InventorySyncFilter.onCursor]).
+	 *
+	 * @see net.minecraft.client.network.ClientPlayNetworkHandler.onSetCursorItem
+	 */
+	@JvmStatic
+	fun onCursorUpdate(packet: SetCursorItemS2CPacket, original: Operation) {
+		runSafe {
+			val stack = packet.contents()
+			mc.tutorialManager.onSlotUpdate(stack)
+			if (!mc.isOnThread || !Client.avoidInventoryDesync) {
+				original.call(packet)
+				snapshotHandler(player.currentScreenHandler)
+				return
+			}
+			val handler = player.currentScreenHandler
+			val drop = !filter.onCursor(handler.syncId, stack, handler.cursorStack)
+			if (verboseDebug) {
+				LOG.info("[desync] cursor single sync=${handler.syncId} incoming=$stack client=${handler.cursorStack} apply=${!drop} pendings=${filter.pendingCount(handler.syncId, InventoryLedger.CURSOR_SLOT_ID)}")
+			}
+			if (drop) return
+			original.call(packet)
+			return
+		}
+		original.call(packet)
+	}
+
+	/**
+	 * A modified version of the minecraft onInventory method
+	 *
+	 * @see net.minecraft.client.network.ClientPlayNetworkHandler.onInventory
+	 */
+	@JvmStatic
+	fun onInventoryUpdate(packet: InventoryS2CPacket, original: Operation) {
+		runSafe {
+			val packetScreenHandler =
+				when (packet.syncId) {
+					0 -> player.playerScreenHandler
+					player.currentScreenHandler.syncId -> player.currentScreenHandler
+					else -> {
+						if (verboseDebug) LOG.info("[desync] full ignored: packet sync=${packet.syncId} current=${player.currentScreenHandler.syncId}")
+						original.call(packet)
+						return
+					}
+				}
+			if (!mc.isOnThread || !Client.avoidInventoryDesync) {
+				original.call(packet)
+				snapshotHandler(packetScreenHandler)
+				return
+			}
+			val clientStacks = packetScreenHandler.slots.map { it.stack }
+			if (clientStacks.size != packet.contents.size) {
+				original.call(packet)
+				snapshotHandler(packetScreenHandler)
+				return
+			}
+			val result =
+				filter.onFull(
+					packet.syncId,
+					packet.contents,
+					clientStacks,
+					packet.cursorStack(),
+					packetScreenHandler.cursorStack
+				)
+			val merged =
+				packet.contents.mapIndexed { index, incomingStack ->
+					if (result.apply[index]) incomingStack else clientStacks[index]
+				}
+			val mergedCursor =
+				if (result.applyCursor) packet.cursorStack()
+				else packetScreenHandler.cursorStack
+			if (verboseDebug && !ItemStack.areEqual(packet.cursorStack(), packetScreenHandler.cursorStack)) {
+				LOG.info("[desync] full sync=${packet.syncId} cursor incoming=${packet.cursorStack()} client=${packetScreenHandler.cursorStack} apply=${result.applyCursor} pendings=${filter.pendingCount(packet.syncId, InventoryLedger.CURSOR_SLOT_ID)}")
+			}
+			packetScreenHandler.updateSlotStacks(packet.revision(), merged, mergedCursor)
+			return
+		}
+		original.call(packet)
+	}
+
+	/**
+	 * When a slot last changed locally, for breaking ties between identical stacks.
+	 * Pure filter read, safe anywhere. Null means unknown: keep the old order.
+	 */
+	fun lastSlotChange(syncId: Int, slotId: Int): Instant? = filter.lastChange(syncId, slotId)
+
+	/**
+	 * Anchors tracking to a screen's live state: drops pendings that cannot belong to a fresh
+	 * screen id, and photographs the current truth. Called on opens, closes and respawns.
+	 */
+	@JvmStatic
+	fun onSetScreenHandler(screenHandler: ScreenHandler) {
+		if (screenHandler.syncId != 0) filter.clear(screenHandler.syncId)
+		filter.snapshot(
+			screenHandler.syncId,
+			screenHandler.slots.associate { it.id to it.stack },
+			screenHandler.cursorStack
+		)
+	}
+}
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryRequest.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryRequest.kt
new file mode 100644
index 000000000..89970063f
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventoryRequest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.inventory
+
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.interaction.manager.Request
+import com.lambda.util.PacketUtils.sendPacket
+import com.lambda.util.player.SlotUtils.clickSlot
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
+import net.minecraft.item.ItemStack
+import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket
+import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket
+import net.minecraft.screen.slot.SlotActionType
+import net.minecraft.screen.sync.ItemStackHash
+import net.minecraft.util.Hand
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Direction
+
+@DslMarker
+annotation class InvRequestDsl
+
+@InvRequestDsl
+fun Automated.inventoryRequest(
+	settleForLess: Boolean = false,
+	mustPerform: Boolean = false,
+	builder: InvRequestBuilder.() -> Unit
+) = InvRequestBuilder(settleForLess, mustPerform).apply(builder).build()
+
+/**
+ * A private constructor is used to enforce use of the [InvRequestDsl] builder.
+ *
+ * @property actions A list of inventory actions to be performed in the request.
+ * @property settleForLess A flag indicating whether to settle for partial completion of the request.
+ * @property mustPerform A flag indicating whether the request must be performed regardless of conditions as long as the tick stage is valid.
+ */
+class InventoryRequest(
+	val actions: List,
+	val settleForLess: Boolean,
+	val mustPerform: Boolean,
+	automated: Automated,
+	override val nowOrNothing: Boolean = false,
+	val onComplete: (SafeContext.() -> Unit)?
+) : Request(), Automated by automated {
+	override val requestId = ++requestCount
+	override val tickStageMask get() = inventoryConfig.tickStageMask
+	var failureReason: InvRequestFailureReason = InvRequestFailureReason.None
+	override var done = false
+
+	@InvRequestDsl
+	override fun submit(queueIfMismatchedStage: Boolean): InventoryRequest =
+		InventoryManager.request(this, queueIfMismatchedStage)
+
+	companion object {
+		var requestCount = 0
+			private set
+	}
+}
+
+@Suppress("unused")
+@InvRequestDsl
+class InvRequestBuilder internal constructor(
+	private val settleForLess: Boolean,
+	private val mustPerform: Boolean
+) {
+	private val actions = mutableListOf()
+	private var onComplete: (SafeContext.() -> Unit)? = null
+
+	fun click(slotId: Int, button: Int, actionType: SlotActionType) {
+		InventoryAction.Inventory { clickSlot(slotId, button, actionType) }.addToActions()
+	}
+
+	fun resyncInventory() {
+		InventoryAction.Inventory {
+			val sh = player.currentScreenHandler
+			val action = if (sh.cursorStack.isEmpty) SlotActionType.PICKUP_ALL else SlotActionType.CLONE
+			connection.sendPacket {
+				ClickSlotC2SPacket(
+					sh.syncId,
+					-1, 0, 0,
+					action,
+					Int2ObjectOpenHashMap(),
+					ItemStackHash.fromItemStack(sh.cursorStack, connection.componentHasher)
+				)
+			}
+		}.addToActions()
+	}
+
+	fun pickFromInventory(slotId: Int) {
+		InventoryAction.Inventory {
+			clickSlot(slotId, player.inventory.selectedSlot, SlotActionType.SWAP)
+		}.addToActions()
+	}
+
+	fun dropItemInHand(entireStack: Boolean = true) {
+		InventoryAction.Inventory { player.dropSelectedItem(entireStack) }.addToActions()
+	}
+
+	fun swapHands() {
+		InventoryAction.Player {
+			val offhandStack = player.getStackInHand(Hand.OFF_HAND)
+			player.setStackInHand(Hand.OFF_HAND, player.getStackInHand(Hand.MAIN_HAND))
+			player.setStackInHand(Hand.MAIN_HAND, offhandStack)
+			connection.sendPacket {
+				PlayerActionC2SPacket(
+					PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND,
+					BlockPos.ORIGIN,
+					Direction.DOWN
+				)
+			}
+		}.addToActions()
+	}
+
+	fun clickCreativeStack(stack: ItemStack, slotId: Int) {
+		InventoryAction.Inventory { interaction.clickCreativeStack(stack, slotId) }.addToActions()
+	}
+
+	fun pickup(slotId: Int, button: Int = 0) = click(slotId, button, SlotActionType.PICKUP)
+
+	fun quickMove(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_MOVE)
+
+	fun swapWithHotbar(slotId: Int, hotbarSlot: Int) = click(slotId, hotbarSlot, SlotActionType.SWAP)
+
+	fun clone(slotId: Int) = click(slotId, 2, SlotActionType.CLONE)
+
+	fun throwStack(slotId: Int) = click(slotId, 1, SlotActionType.THROW)
+
+	fun throwSingle(slotId: Int) = click(slotId, 0, SlotActionType.THROW)
+
+	fun quickCraftStart(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_CRAFT)
+
+	fun quickCraftDrag(slotId: Int) = click(slotId, 1, SlotActionType.QUICK_CRAFT)
+
+	fun quickCraftEnd(slotId: Int) = click(slotId, 2, SlotActionType.QUICK_CRAFT)
+
+	fun pickupAll(slotId: Int) = click(slotId, 0, SlotActionType.PICKUP_ALL)
+
+	fun moveSlot(fromSlotId: Int, toSlotId: Int, button: Int = 0) {
+		pickup(fromSlotId, button)
+		pickup(toSlotId, button)
+	}
+
+	fun splitStack(slotId: Int, targetSlotId: Int) {
+		pickup(slotId, 1)
+		pickup(targetSlotId, 0)
+	}
+
+	fun mergeStacks(sourceSlotId: Int, targetSlotId: Int) {
+		pickup(sourceSlotId, 0)
+		pickup(targetSlotId, 0)
+	}
+
+	fun action(action: SafeContext.() -> Unit) {
+		InventoryAction.Other(action).addToActions()
+	}
+
+	fun onComplete(callback: SafeContext.() -> Unit) {
+		onComplete = callback
+	}
+
+	private fun InventoryAction.addToActions() {
+		actions.add(this)
+	}
+
+	context(automated: Automated)
+	internal fun build() = InventoryRequest(actions, settleForLess, mustPerform, automated, onComplete = onComplete)
+}
+
+enum class InvRequestFailureReason {
+	None,
+	Preoccupied,
+	PacketLimit
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventorySyncFilter.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventorySyncFilter.kt
new file mode 100644
index 000000000..eb3758646
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/inventory/InventorySyncFilter.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.interaction.manager.managers.inventory
+
+import com.lambda.interaction.manager.managers.inventory.InventoryLedger.SingleDecision.Drop
+import kotlin.time.Clock
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Instant
+
+class InventorySyncFilter(
+    private val equal: (a: T, b: T) -> Boolean,
+    private val copy: (T) -> T,
+    maxAgeMs: Long = 1500L,
+    private val clock: () -> Instant = { Clock.System.now() }
+) {
+    private val ledger = InventoryLedger(equal, maxAgeMs, clock = clock)
+    private val slotSnapshots = mutableMapOf>()
+    private val cursorSnapshots = mutableMapOf()
+
+    private val changeTimes = mutableMapOf, Instant>()
+
+    var maxAgeMs: Long
+        get() = ledger.maxAgeMs
+        set(value) {
+            ledger.maxAgeMs = value
+        }
+
+    data class FullResult(val apply: List, val applyCursor: Boolean)
+
+    @Synchronized
+    fun snapshot(syncId: Int, slots: Map, cursor: T) {
+        slotSnapshots[syncId] = slots.mapValuesTo(HashMap()) { copy(it.value) }
+        cursorSnapshots[syncId] = copy(cursor)
+    }
+
+    @Synchronized
+    fun observeLocal(syncId: Int, slots: Map, cursor: T): Int {
+        val snap = slotSnapshots[syncId]
+        if (snap == null) {
+            slotSnapshots[syncId] = slots.mapValuesTo(HashMap()) { copy(it.value) }
+            cursorSnapshots[syncId] = copy(cursor)
+            return 0
+        }
+        var recorded = 0
+        slots.forEach { (slotId, live) ->
+            val old = snap[slotId]
+            if (old == null) snap[slotId] = copy(live)
+            else if (!equal(old, live)) {
+                ledger.record(syncId, slotId, old, copy(live))
+                snap[slotId] = copy(live)
+                changeTimes[syncId to slotId] = clock()
+                recorded++
+            }
+        }
+        snap.keys.retainAll(slots.keys)
+        val oldCursor = cursorSnapshots[syncId]
+        if (oldCursor == null) cursorSnapshots[syncId] = copy(cursor)
+        else if (!equal(oldCursor, cursor)) {
+            ledger.record(syncId, InventoryLedger.CURSOR_SLOT_ID, oldCursor, copy(cursor))
+            cursorSnapshots[syncId] = copy(cursor)
+            recorded++
+        }
+        return recorded
+    }
+
+    @Synchronized
+    fun onSingle(syncId: Int, slotId: Int, incoming: T, current: T): Boolean {
+        val drop = ledger.decideSingle(syncId, slotId, incoming) == Drop
+        slotSnapshots[syncId]?.let { snap ->
+            if (snap.containsKey(slotId)) snap[slotId] = copy(if (drop) current else incoming)
+        }
+        return !drop
+    }
+
+    /**
+     * Decides a cursor-only server update ([SetCursorItemS2CPacket]). These bypass the full
+     * packets: the server emits them from content updates on non-click triggers (pickups, tick
+     * syncs), carrying cursor truth that lags our predictions by a round trip. Without this,
+     * every such packet is applied blindly and resurrects whatever we just placed.
+     * Same contract as [onSingle], tracked under [InventoryLedger.CURSOR_SLOT_ID].
+     */
+    @Synchronized
+    fun onCursor(syncId: Int, incoming: T, current: T): Boolean {
+        val drop = ledger.decideCursor(syncId, incoming) == Drop
+        if (cursorSnapshots.containsKey(syncId)) cursorSnapshots[syncId] = copy(if (drop) current else incoming)
+        return !drop
+    }
+
+    @Synchronized
+    fun onFull(
+        syncId: Int,
+        incoming: List,
+        client: List,
+        incomingCursor: T,
+        clientCursor: T,
+    ): FullResult {
+        val apply = ledger.decideFull(syncId, incoming, client)
+        val applyCursor = ledger.decideCursor(syncId, incomingCursor) != Drop
+        val merged = incoming.indices.map { copy(if (apply[it]) incoming[it] else client[it]) }
+        val snap = HashMap(merged.size)
+        merged.forEachIndexed { index, stack -> snap[index] = stack }
+        slotSnapshots[syncId] = snap
+        cursorSnapshots[syncId] = copy(if (applyCursor) incomingCursor else clientCursor)
+        return FullResult(apply, applyCursor)
+    }
+
+    @Synchronized
+    fun clear(syncId: Int) {
+        ledger.clear(syncId)
+        changeTimes.keys.removeIf { (sync, _) -> sync == syncId }
+    }
+
+    /**
+     * When [slotId] last changed locally, or null if never (or no longer ago than
+     * [maxAgeMs]). A fresh screen id must never inherit another screen's stamps.
+     */
+    @Synchronized
+    fun lastChange(syncId: Int, slotId: Int): Instant? {
+        val key = syncId to slotId
+        val at = changeTimes[key] ?: return null
+        if (clock() - at > maxAgeMs.milliseconds) {
+            changeTimes.remove(key)
+            return null
+        }
+        return at
+    }
+
+    @Synchronized
+    fun pendingCount(syncId: Int, slotId: Int): Int = ledger.pendingCount(syncId, slotId)
+
+    @Synchronized
+    fun totalPending(): Int = ledger.totalPending()
+}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/Rotation.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/Rotation.kt
similarity index 99%
rename from src/main/kotlin/com/lambda/interaction/managers/rotating/Rotation.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/rotating/Rotation.kt
index 06600b9d1..9efeb74ce 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/rotating/Rotation.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/Rotation.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.rotating
+package com.lambda.interaction.manager.managers.rotating
 
 import com.lambda.threading.runSafe
 import com.lambda.util.math.MathUtils.toDegree
diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationManager.kt
similarity index 95%
rename from src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationManager.kt
index d719df9e2..4d40683d3 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationManager.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.rotating
+package com.lambda.interaction.manager.managers.rotating
 
 import com.lambda.Lambda.mc
 import com.lambda.context.AutomatedSafeContext
@@ -25,12 +25,12 @@ import com.lambda.event.events.RenderEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.events.TickEvent.Companion.ALL_STAGES
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.Manager
-import com.lambda.interaction.managers.rotating.Rotation.Companion.slerpPitch
-import com.lambda.interaction.managers.rotating.Rotation.Companion.slerpYaw
-import com.lambda.interaction.managers.rotating.RotationManager.activeRotation
-import com.lambda.interaction.managers.rotating.RotationManager.serverRotation
-import com.lambda.interaction.managers.rotating.RotationManager.updateActiveRotation
+import com.lambda.interaction.manager.Manager
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.slerpPitch
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.slerpYaw
+import com.lambda.interaction.manager.managers.rotating.RotationManager.activeRotation
+import com.lambda.interaction.manager.managers.rotating.RotationManager.serverRotation
+import com.lambda.interaction.manager.managers.rotating.RotationManager.updateActiveRotation
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.rotation
 import com.lambda.util.math.Vec2d
diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationMode.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationMode.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/interaction/managers/rotating/RotationMode.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationMode.kt
index 5d0cae02e..0ce450f52 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationMode.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationMode.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.rotating
+package com.lambda.interaction.manager.managers.rotating
 
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationRequest.kt
similarity index 60%
rename from src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt
rename to src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationRequest.kt
index 94187dd9f..8b5f62b08 100644
--- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt
+++ b/src/main/kotlin/com/lambda/interaction/manager/managers/rotating/RotationRequest.kt
@@ -15,16 +15,18 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.interaction.managers.rotating
+package com.lambda.interaction.manager.managers.rotating
 
 import com.lambda.context.Automated
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.Request
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.requestCount
-import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.wrap
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.manager.Request
+import com.lambda.interaction.manager.managers.rotating.IRotationRequest.Full
+import com.lambda.interaction.manager.managers.rotating.IRotationRequest.Pitch
+import com.lambda.interaction.manager.managers.rotating.IRotationRequest.Yaw
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.dist
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.wrap
 import com.lambda.threading.runSafe
 import com.lambda.util.collections.UpdatableLazy
 import com.lambda.util.collections.updatableLazy
@@ -32,7 +34,11 @@ import kotlin.math.abs
 import kotlin.math.hypot
 
 @DslMarker
-annotation class RotationRequestDsl
+annotation class RotationRequestMarker
+
+@RotationRequestMarker
+fun Automated.rotationRequest(builder: RotationRequestBuilder.() -> Unit) =
+	RotationRequestBuilder().apply(builder).build()
 
 abstract class RotationRequest(automated: Automated) : Request(), Automated by automated {
 	override val requestId = requestCount++
@@ -41,9 +47,14 @@ abstract class RotationRequest(automated: Automated) : Request(), Automated by a
 
 	abstract infix fun dist(rotation: Rotation): Double
 
-	@RotationRequestDsl
+	@RotationRequestMarker
 	override fun submit(queueIfMismatchedStage: Boolean) =
 		RotationManager.request(this, queueIfMismatchedStage)
+
+	companion object {
+		var requestCount = 0
+			private set
+	}
 }
 
 interface IRotationRequest : Automated {
@@ -141,73 +152,53 @@ interface IRotationRequest : Automated {
 	interface YawRot : IRotationRequest { val yaw: UpdatableLazy }
 	interface PitchRot : IRotationRequest { val pitch: UpdatableLazy }
 	interface FullRot : YawRot, PitchRot { val rotation: UpdatableLazy }
+}
 
-	class RotationRequestBuilder {
-		var pitchBuilder: (SafeContext.() -> Double)? = null
-		var yawBuilder: (SafeContext.() -> Double)? = null
-		var rotationBuilder: (SafeContext.() -> Rotation)? = null
-
-		@JvmName("yawBuilder1")
-		@RotationRequestDsl
-		fun yaw(builder: SafeContext.() -> Double) { yawBuilder = builder }
+@RotationRequestMarker
+class RotationRequestBuilder internal constructor() {
+	private var pitchBuilder: (SafeContext.() -> Double)? = null
+	private var yawBuilder: (SafeContext.() -> Double)? = null
+	private var rotationBuilder: (SafeContext.() -> Rotation)? = null
 
-		@JvmName("yawBuilder2")
-		@RotationRequestDsl
-		fun yaw(builder: SafeContext.() -> Float) { yawBuilder = { builder().toDouble() } }
+	@JvmName("yawBuilder1")
+	fun yaw(builder: SafeContext.() -> Double) { yawBuilder = builder }
 
-		@RotationRequestDsl
-		fun yaw(yaw: Double) { yawBuilder = { yaw } }
+	@JvmName("yawBuilder2")
+	fun yaw(builder: SafeContext.() -> Float) { yawBuilder = { builder().toDouble() } }
 
-		@RotationRequestDsl
-		fun yaw(yaw: Float) { yawBuilder = { yaw.toDouble() } }
+	fun yaw(yaw: Double) { yawBuilder = { yaw } }
 
-		@JvmName("pitchBuilder1")
-		@RotationRequestDsl
-		fun pitch(builder: SafeContext.() -> Double) { pitchBuilder = builder }
+	fun yaw(yaw: Float) { yawBuilder = { yaw.toDouble() } }
 
-		@JvmName("pitchBuilder2")
-		@RotationRequestDsl
-		fun pitch(builder: SafeContext.() -> Float) { pitchBuilder = { builder().toDouble() } }
+	@JvmName("pitchBuilder1")
+	fun pitch(builder: SafeContext.() -> Double) { pitchBuilder = builder }
 
-		@RotationRequestDsl
-		fun pitch(pitch: Double) { pitchBuilder = { pitch } }
+	@JvmName("pitchBuilder2")
+	fun pitch(builder: SafeContext.() -> Float) { pitchBuilder = { builder().toDouble() } }
 
-		@RotationRequestDsl
-		fun pitch(pitch: Float) { pitchBuilder = { pitch.toDouble() } }
+	fun pitch(pitch: Double) { pitchBuilder = { pitch } }
 
-		@RotationRequestDsl
-		fun rotation(builder: SafeContext.() -> Rotation) { rotationBuilder = builder }
+	fun pitch(pitch: Float) { pitchBuilder = { pitch.toDouble() } }
 
-		@RotationRequestDsl
-		fun rotation(yaw: Double, pitch: Double) { rotationBuilder = { Rotation(yaw, pitch) } }
+	fun rotation(builder: SafeContext.() -> Rotation) { rotationBuilder = builder }
 
-		@RotationRequestDsl
-		fun rotation(yaw: Float, pitch: Float) { rotationBuilder = { Rotation(yaw, pitch) } }
+	fun rotation(yaw: Double, pitch: Double) { rotationBuilder = { Rotation(yaw, pitch) } }
 
-		@RotationRequestDsl
-		fun rotation(rotation: Rotation) { rotationBuilder = { rotation } }
-	}
+	fun rotation(yaw: Float, pitch: Float) { rotationBuilder = { Rotation(yaw, pitch) } }
 
-	companion object {
-		var requestCount = 0
+	fun rotation(rotation: Rotation) { rotationBuilder = { rotation } }
 
-		@RotationRequestDsl
-		fun Automated.rotationRequest(builder: RotationRequestBuilder.() -> Unit) =
-			RotationRequestBuilder().apply(builder).build()
-
-		@RotationRequestDsl
-		context(automated: Automated)
-		private fun RotationRequestBuilder.build(): RotationRequest {
-			val yawBuilder = yawBuilder
-			val pitchBuilder = pitchBuilder
-			val rotationBuilder = rotationBuilder
-			return when {
-				rotationBuilder != null -> Full(automated, rotationBuilder)
-				yawBuilder != null && pitchBuilder != null -> Full(automated) { Rotation(yawBuilder(), pitchBuilder()) }
-				yawBuilder != null -> Yaw(automated, yawBuilder)
-				pitchBuilder != null -> Pitch(automated, pitchBuilder)
-				else -> throw IllegalArgumentException("Must specify at least one rotation value to build a rotation request")
-			}
+	context(automated: Automated)
+	internal fun build(): RotationRequest {
+		val yawBuilder = yawBuilder
+		val pitchBuilder = pitchBuilder
+		val rotationBuilder = rotationBuilder
+		return when {
+			rotationBuilder != null -> Full(automated, rotationBuilder)
+			yawBuilder != null && pitchBuilder != null -> Full(automated) { Rotation(yawBuilder(), pitchBuilder()) }
+			yawBuilder != null -> Yaw(automated, yawBuilder)
+			pitchBuilder != null -> Pitch(automated, pitchBuilder)
+			else -> throw IllegalArgumentException("Must specify at least one rotation value to build a rotation request")
 		}
 	}
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakRequest.kt b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakRequest.kt
deleted file mode 100644
index 3330cf195..000000000
--- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakRequest.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.managers.breaking
-
-import com.lambda.context.Automated
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.interaction.construction.simulation.context.BreakContext
-import com.lambda.interaction.construction.simulation.context.BuildContext
-import com.lambda.interaction.construction.simulation.result.BuildResult
-import com.lambda.interaction.construction.simulation.result.Dependent
-import com.lambda.interaction.construction.simulation.result.results.BreakResult
-import com.lambda.interaction.construction.simulation.sim
-import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.Request
-import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
-import com.lambda.threading.runSafe
-import com.lambda.util.BlockUtils.blockState
-import com.lambda.util.BlockUtils.isEmpty
-import net.minecraft.entity.ItemEntity
-import net.minecraft.util.math.BlockPos
-
-/**
- * Contains the information necessary for initializing and continuing breaks within the [BreakManager].
- *
- * The callbacks can be used to keep track of the break progress.
- *
- * A private constructor is used to force use of the cleaner [BreakRequestDsl] builder. This is
- * accessed through the [breakRequest] method.
- *
- * @param contexts A collection of [BreakContext]'s gathered from the BuildSimulator.
- * @param pendingInteractions A mutable, concurrent list to store the pending actions.
- *
- * @see com.lambda.interaction.construction.simulation.BuildSimulator
- */
-data class BreakRequest private constructor(
-	val contexts: Collection,
-	val pendingInteractions: MutableCollection,
-	private val automated: Automated,
-	override val nowOrNothing: Boolean = false
-) : Request(), Automated by automated {
-	override val requestId = ++requestCount
-	override val tickStageMask get() = breakConfig.tickStageMask
-
-	var onStart: (SafeContext.(BlockPos) -> Unit)? = null
-	var onUpdate: (SafeContext.(BlockPos) -> Unit)? = null
-	var onStop: (SafeContext.(BlockPos) -> Unit)? = null
-	var onCancel: (SafeContext.(BlockPos) -> Unit)? = null
-	var onItemDrop: (SafeContext.(ItemEntity) -> Unit)? = null
-	var onReBreakStart: (SafeContext.(BlockPos) -> Unit)? = null
-	var onReBreak: (SafeContext.(BlockPos) -> Unit)? = null
-
-	override val done: Boolean
-		get() = runSafe { contexts.all { blockState(it.blockPos).isEmpty } } == true
-
-	@DslMarker
-	annotation class BreakRequestDsl
-
-	@BreakRequestDsl
-	override fun submit(queueIfMismatchedStage: Boolean) =
-		BreakManager.request(this, queueIfMismatchedStage)
-
-	@BreakRequestDsl
-	class BreakRequestBuilder(
-		contexts: Collection,
-		pendingInteractions: MutableCollection,
-		nowOrNothing: Boolean,
-		automated: Automated
-	) {
-		val request = BreakRequest(contexts, pendingInteractions, automated, nowOrNothing)
-
-		fun onStart(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onStart = callback
-		}
-
-		fun onUpdate(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onUpdate = callback
-		}
-
-		fun onStop(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onStop = callback
-		}
-
-		fun onCancel(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onCancel = callback
-		}
-
-		fun onItemDrop(callback: SafeContext.(ItemEntity) -> Unit) {
-			request.onItemDrop = callback
-		}
-
-		fun onReBreakStart(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onReBreakStart = callback
-		}
-
-		fun onReBreak(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onReBreak = callback
-		}
-	}
-
-	companion object {
-		var requestCount = 0
-
-		@BreakRequestDsl
-		@JvmName("breakRequest1")
-		fun AutomatedSafeContext.breakRequest(
-			positions: Collection,
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (BreakRequestBuilder.() -> Unit)? = null
-		) = positions
-			.associateWith { TargetState.Empty }
-			.sim()
-			.breakRequest(pendingInteractions, nowOrNothing, builder)
-
-		@BreakRequestDsl
-		@JvmName("breakRequest2")
-		context(automated: Automated)
-		fun Collection.breakRequest(
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (BreakRequestBuilder.() -> Unit)? = null
-		) = asSequence()
-			.breakRequest(pendingInteractions, nowOrNothing, builder)
-
-		@BreakRequestDsl
-		@JvmName("breakRequest3")
-		context(automated: Automated)
-		fun Sequence.breakRequest(
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (BreakRequestBuilder.() -> Unit)? = null
-		) = map { if (it is Dependent) it.lastDependency else it }
-			.filterIsInstance()
-			.sorted()
-			.map { it.context }
-			.toSet()
-			.takeIf { it.isNotEmpty() }
-			?.let { automated.breakRequest(it, pendingInteractions, nowOrNothing, builder) }
-
-		@BreakRequestDsl
-		@JvmName("breakRequest4")
-		fun Automated.breakRequest(
-			contexts: Collection,
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (BreakRequestBuilder.() -> Unit)? = null
-		) = BreakRequestBuilder(
-			contexts, pendingInteractions, nowOrNothing, this
-		).apply { builder?.invoke(this) }.build()
-
-		@BreakRequestDsl
-		private fun BreakRequestBuilder.build(): BreakRequest = request
-	}
-}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractRequest.kt b/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractRequest.kt
deleted file mode 100644
index 80d6a859f..000000000
--- a/src/main/kotlin/com/lambda/interaction/managers/interacting/InteractRequest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.managers.interacting
-
-import com.lambda.context.Automated
-import com.lambda.context.SafeContext
-import com.lambda.interaction.construction.simulation.context.BuildContext
-import com.lambda.interaction.construction.simulation.context.InteractContext
-import com.lambda.interaction.construction.simulation.result.BuildResult
-import com.lambda.interaction.construction.simulation.result.Dependent
-import com.lambda.interaction.construction.simulation.result.results.InteractResult
-import com.lambda.interaction.managers.Request
-import com.lambda.threading.runSafe
-import com.lambda.util.BlockUtils.blockState
-import com.lambda.util.BlockUtils.matches
-import net.minecraft.util.math.BlockPos
-
-data class InteractRequest private constructor(
-	val contexts: Collection,
-	val pendingInteractions: MutableCollection,
-	private val automated: Automated,
-	override val nowOrNothing: Boolean = false,
-) : Request(), Automated by automated {
-	override val requestId = ++requestCount
-	override val tickStageMask get() = interactConfig.tickStageMask
-
-	var onPlace: (SafeContext.(BlockPos) -> Unit)? = null
-
-	override val done: Boolean
-		get() = runSafe {
-			contexts.all { it.expectedState.matches(blockState(it.blockPos)) }
-		} == true
-
-	@DslMarker
-	annotation class PlaceRequestDsl
-
-	@PlaceRequestDsl
-	override fun submit(queueIfMismatchedStage: Boolean) =
-		InteractManager.request(this, queueIfMismatchedStage)
-
-	@PlaceRequestDsl
-	class PlaceRequestBuilder(
-		contexts: Collection,
-		pendingInteractions: MutableCollection,
-		nowOrNothing: Boolean,
-		automated: Automated
-	) {
-		val request = InteractRequest(contexts, pendingInteractions, automated, nowOrNothing)
-
-		fun onPlace(callback: SafeContext.(BlockPos) -> Unit) {
-			request.onPlace = callback
-		}
-	}
-
-	companion object {
-		var requestCount = 0
-
-		@PlaceRequestDsl
-		@JvmName("interactRequest1")
-		context(automated: Automated)
-		fun Collection.interactRequest(
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (PlaceRequestBuilder.() -> Unit)? = null
-		) = asSequence()
-			.interactRequest(pendingInteractions, nowOrNothing, builder)
-
-		@PlaceRequestDsl
-		@JvmName("interactRequest2")
-		context(automated: Automated)
-		fun Sequence.interactRequest(
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (PlaceRequestBuilder.() -> Unit)? = null
-		) = map { if (it is Dependent) it.lastDependency else it }
-			.filterIsInstance()
-			.sorted()
-			.map { it.context }
-			.toSet()
-			.takeIf { it.isNotEmpty() }
-			?.let { automated.interactRequest(it, pendingInteractions, nowOrNothing, builder) }
-
-		@PlaceRequestDsl
-		@JvmName("interactRequest3")
-		fun Automated.interactRequest(
-			contexts: Collection,
-			pendingInteractions: MutableCollection,
-			nowOrNothing: Boolean = false,
-			builder: (PlaceRequestBuilder.() -> Unit)? = null
-		) = PlaceRequestBuilder(contexts, pendingInteractions, nowOrNothing, this).apply { builder?.invoke(this) }.build()
-
-		@PlaceRequestDsl
-		fun PlaceRequestBuilder.build() = request
-	}
-}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt
deleted file mode 100644
index f89841caf..000000000
--- a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.managers.inventory
-
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.event.events.PacketEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.packet.PacketLimitHandler.canSendPackets
-import com.lambda.interaction.handlers.packet.PacketLimitHandler.sentPackets
-import com.lambda.interaction.handlers.packet.PacketType
-import com.lambda.interaction.managers.Manager
-import com.lambda.interaction.managers.inventory.InventoryManager.actions
-import com.lambda.interaction.managers.inventory.InventoryManager.activeRequest
-import com.lambda.interaction.managers.inventory.InventoryManager.alteredSlots
-import com.lambda.interaction.managers.inventory.InventoryManager.processActiveRequest
-import com.lambda.module.modules.client.Client
-import com.lambda.threading.runSafe
-import com.lambda.threading.runSafeAutomated
-import com.lambda.util.collections.LimitedDecayQueue
-import com.lambda.util.item.ItemStackUtils.equal
-import com.llamalad7.mixinextras.injector.wrapoperation.Operation
-import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen
-import net.minecraft.item.ItemStack
-import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
-import net.minecraft.network.packet.s2c.play.InventoryS2CPacket
-import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket
-import net.minecraft.screen.PlayerScreenHandler
-import net.minecraft.screen.ScreenHandler
-import net.minecraft.screen.slot.Slot
-
-/**
- * Manager designed to handle inventory actions. One of the key features being the inventory change detection to
- * avoid accepting old information from the server in cases where ping is high. This helps to prevent desync.
- */
-object InventoryManager : Manager(
-	1,
-	onOpen = { processActiveRequest() }
-) {
-	private var activeRequest: InventoryRequest? = null
-	private var actions = mutableListOf()
-
-	private var slots = listOf()
-	private var alteredSlots = LimitedDecayQueue(Int.MAX_VALUE, Client.desyncTimeout * 50L)
-	private var alteredPlayerSlots = LimitedDecayQueue(Int.MAX_VALUE, Client.desyncTimeout * 50L)
-
-	var screenHandler: ScreenHandler? = null
-		set(value) {
-			if (value != null) {
-				alteredSlots.clear()
-				slots = getStacks(value.slots)
-			}
-			field = value
-		}
-
-	private var actionsThisTick = 0
-
-	override fun load(): String {
-		super.load()
-
-        listen({ Int.MIN_VALUE }) {
-            if (Client.avoidInventoryDesync) indexInventoryChanges()
-            actionsThisTick = 0
-            activeRequest = null
-            actions = mutableListOf()
-        }
-
-		listen { event ->
-			if (event.packet is CloseHandledScreenC2SPacket) {
-				screenHandler = player.playerScreenHandler
-			}
-		}
-
-		return "Loaded Inventory Manager"
-	}
-
-	/**
-	 * Attempts to accept the request and perform the actions. If, for example, the tick stage isn't valid, or
-	 * not all the actions can be performed and [InventoryRequest.settleForLess] is set to false, the request is rejected.
-	 * All checks aside from tick stage are ignored if the request has [InventoryRequest.mustPerform] set to true.
-	 * This is typically used in dangerous situations where typical rules are worth breaking. For example, if the player
-	 * needs to equip a totem of undying.
-	 */
-	override fun AutomatedSafeContext.handleRequest(request: InventoryRequest) {
-		if (activeRequest != null) return
-
-		val playerActionCount = request.actions.count { it is InventoryAction.Player }
-		val inventoryActionCount = request.actions.count { it is InventoryAction.Inventory }
-		val canPerformAllPlayerActions = canSendPackets(playerActionCount, PacketType.PlayerAction)
-		val canPerformAllInventoryActions = canSendPackets(inventoryActionCount, PacketType.Inventory)
-		if ((!canPerformAllPlayerActions || !canPerformAllInventoryActions) &&
-			!request.settleForLess &&
-			!request.mustPerform) return
-
-		if (request.fresh) populateFrom(request)
-
-
-		processActiveRequest()
-		if (request.nowOrNothing) {
-			activeRequest = null
-			actions = mutableListOf()
-		}
-	}
-
-	private fun populateFrom(request: InventoryRequest) {
-		activeRequest = request
-		actions = request.actions.toMutableList()
-		alteredSlots.setDecayTime(Client.desyncTimeout * 50L)
-		alteredPlayerSlots.setDecayTime(Client.desyncTimeout * 50L)
-	}
-
-	/**
-	 * Attempts to perform as many actions as possible from the [actions] collection. If
-	 * [actions] is empty, the request is set to done, and the onComplete callback is invoked.
-	 * The [activeRequest] is then set to null.
-	 */
-	private fun SafeContext.processActiveRequest() {
-		val active = activeRequest ?: return
-		active.runSafeAutomated {
-			if (tickStage !in active.inventoryConfig.tickStageMask && active.nowOrNothing) return
-			val iterator = actions.iterator()
-			while (iterator.hasNext()) {
-				val action = iterator.next()
-				if (action is InventoryAction.Player && !canSendPackets(1, PacketType.PlayerAction)) break
-				else if (action is InventoryAction.Inventory && !canSendPackets(1, PacketType.Inventory)) break
-				action.action(this)
-				if (action is InventoryAction.Player) sentPackets(1, PacketType.PlayerAction)
-				else if (action is InventoryAction.Inventory) sentPackets(1, PacketType.Inventory)
-				if (Client.avoidInventoryDesync) indexInventoryChanges()
-				actionsThisTick++
-				iterator.remove()
-			}
-
-			if (actions.isEmpty()) {
-				active.done = true
-				active.onComplete?.invoke(this)
-				activeRequest = null
-			}
-
-			if (actionsThisTick > 0) activeThisTick = true
-		}
-	}
-
-	/**
-	 * Detects changes in item stacks between now and the last time slots were cached and
-	 * adds them all to the [alteredSlots] collection where they can be compared to
-	 * incoming [InventoryS2CPacket] and [ScreenHandlerSlotUpdateS2CPacket] packets to decide whether to
-	 * block certain updates.
-	 */
-	context(safeContext: SafeContext)
-	fun indexInventoryChanges() {
-		with(safeContext) {
-			if (player.currentScreenHandler.syncId != screenHandler?.syncId) return
-			val changes = screenHandler?.slots
-				?.filter { !it.stack.equal(slots[it.id]) }
-				?.map { InventoryChange(it.id, slots[it.id], it.stack.copy()) }
-				?: emptyList()
-			if (player.currentScreenHandler.syncId == 0) alteredPlayerSlots.addAll(changes)
-			else alteredSlots.addAll(changes)
-			slots = getStacks(player.currentScreenHandler.slots)
-		}
-	}
-
-	private fun getStacks(slots: Collection) = slots.map { it.stack.copy() }
-
-	/**
-	 * A modified version of the minecraft onInventory method
-	 *
-	 * @see net.minecraft.client.network.ClientPlayNetworkHandler.onInventory
-	 */
-	@JvmStatic
-	fun onInventoryUpdate(packet: InventoryS2CPacket, original: Operation){
-		runSafe {
-			if (!mc.isOnThread || !Client.avoidInventoryDesync) {
-				original.call(packet)
-				return
-			}
-			val packetScreenHandler =
-				when (packet.syncId) {
-					0 -> player.playerScreenHandler
-					player.currentScreenHandler.syncId -> player.currentScreenHandler
-					else -> return@runSafe
-				}
-			val alteredContents = mutableListOf()
-			val alteredSlots = if (packet.syncId == 0) alteredPlayerSlots else alteredSlots
-			packet.contents.forEachIndexed { index, incomingStack ->
-				val matches = alteredSlots.removeIf { cached ->
-					incomingStack.equal(cached.after)
-				}
-				if (matches) alteredContents.add(packetScreenHandler.slots[index].stack)
-				else alteredContents.add(incomingStack)
-			}
-			packetScreenHandler.updateSlotStacks(packet.revision(), alteredContents, packet.cursorStack())
-			return
-		}
-		original.call(packet)
-	}
-
-	/**
-	 * A modified version of the minecraft onScreenHandlerSlotUpdate method
-	 *
-	 * @see net.minecraft.client.network.ClientPlayNetworkHandler.onScreenHandlerSlotUpdate
-	 */
-	@JvmStatic
-	fun onSlotUpdate(packet: ScreenHandlerSlotUpdateS2CPacket, original: Operation) {
-		runSafe {
-			if (!mc.isOnThread || !Client.avoidInventoryDesync) {
-				original.call(packet)
-				return
-			}
-			val itemStack = packet.stack
-			mc.tutorialManager.onSlotUpdate(itemStack)
-
-			val bl = (mc.currentScreen as? CreativeInventoryScreen)?.let {
-				!it.isInventoryTabSelected
-			} ?: false
-
-			val alteredSlots = if (packet.syncId == 0) alteredPlayerSlots else alteredSlots
-			val matches = alteredSlots.removeIf {
-				it.slotId == packet.slot && it.after.equal(itemStack)
-			}
-
-			if (packet.syncId == 0) {
-				if (PlayerScreenHandler.isInHotbar(packet.slot) && !itemStack.isEmpty) {
-					val itemStack2 = player.playerScreenHandler.getSlot(packet.slot).stack
-					if (itemStack2.isEmpty || itemStack2.count < itemStack.count) {
-						itemStack.bobbingAnimationTime = 5
-					}
-				}
-
-				if (matches) player.playerScreenHandler.revision = packet.revision
-				else player.playerScreenHandler.setStackInSlot(packet.slot, packet.revision, itemStack)
-			} else if (packet.syncId == player.currentScreenHandler.syncId && (packet.syncId != 0 || !bl)) {
-				if (matches) player.currentScreenHandler.revision = packet.revision
-				else player.currentScreenHandler.setStackInSlot(packet.slot, packet.revision, itemStack)
-			}
-
-			if (mc.currentScreen is CreativeInventoryScreen) {
-				player.playerScreenHandler.setReceivedStack(packet.slot, itemStack)
-				player.playerScreenHandler.sendContentUpdates()
-			}
-			return
-		}
-		original.call(packet)
-	}
-
-	@JvmStatic
-	fun onSetScreenHandler(screenHandler: ScreenHandler) {
-		this.screenHandler = screenHandler
-	}
-
-	private data class InventoryChange(
-		val slotId: Int,
-		val before: ItemStack,
-		val after: ItemStack
-	)
-}
diff --git a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryRequest.kt b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryRequest.kt
deleted file mode 100644
index 277eb2faf..000000000
--- a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryRequest.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.managers.inventory
-
-import com.lambda.context.Automated
-import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.Request
-import com.lambda.util.PacketUtils.sendPacket
-import com.lambda.util.player.SlotUtils.clickSlot
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
-import net.minecraft.item.ItemStack
-import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket
-import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket
-import net.minecraft.screen.slot.SlotActionType
-import net.minecraft.screen.sync.ItemStackHash
-import net.minecraft.util.Hand
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Direction
-
-/**
- * A private constructor is used to enforce use of the [InvRequestDsl] builder.
- *
- * @property actions A list of inventory actions to be performed in the request.
- * @property settleForLess A flag indicating whether to settle for partial completion of the request.
- * @property mustPerform A flag indicating whether the request must be performed regardless of conditions as long as the tick stage is valid.
- */
-class InventoryRequest private constructor(
-	val actions: List,
-	val settleForLess: Boolean,
-	val mustPerform: Boolean,
-	automated: Automated,
-	override val nowOrNothing: Boolean = false,
-	val onComplete: (SafeContext.() -> Unit)?
-) : Request(), Automated by automated {
-	override val requestId = ++requestCount
-	override val tickStageMask get() = inventoryConfig.tickStageMask
-	override var done = false
-
-	@InvRequestDsl
-	override fun submit(queueIfMismatchedStage: Boolean) =
-		InventoryManager.request(this, queueIfMismatchedStage)
-
-	@DslMarker
-	private annotation class InvRequestDsl
-
-	@Suppress("unused")
-	@InvRequestDsl
-	class InvRequestBuilder(val settleForLess: Boolean, val mustPerform: Boolean) {
-		val actions = mutableListOf()
-		var onComplete: (SafeContext.() -> Unit)? = null
-
-		fun click(slotId: Int, button: Int, actionType: SlotActionType) {
-			InventoryAction.Inventory { clickSlot(slotId, button, actionType) }.addToActions()
-		}
-
-		fun resyncInventory() {
-			InventoryAction.Inventory {
-				connection.sendPacket {
-					val sh = player.currentScreenHandler
-					ClickSlotC2SPacket(
-						sh.syncId,
-						-1, -1, 0,
-						SlotActionType.CLONE,
-						Int2ObjectOpenHashMap(),
-						ItemStackHash.fromItemStack(sh.cursorStack, connection.componentHasher)
-					)
-				}
-			}.addToActions()
-		}
-
-		fun pickFromInventory(slotId: Int) {
-			InventoryAction.Inventory {
-				clickSlot(slotId, player.inventory.selectedSlot, SlotActionType.SWAP)
-			}.addToActions()
-		}
-
-		fun dropItemInHand(entireStack: Boolean = true) {
-			InventoryAction.Inventory { player.dropSelectedItem(entireStack) }.addToActions()
-		}
-
-		fun swapHands() {
-			InventoryAction.Player {
-				val offhandStack = player.getStackInHand(Hand.OFF_HAND)
-				player.setStackInHand(Hand.OFF_HAND, player.getStackInHand(Hand.MAIN_HAND))
-				player.setStackInHand(Hand.MAIN_HAND, offhandStack)
-				connection.sendPacket {
-					PlayerActionC2SPacket(
-						PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND,
-						BlockPos.ORIGIN,
-						Direction.DOWN
-					)
-				}
-			}.addToActions()
-		}
-
-		fun clickCreativeStack(stack: ItemStack, slotId: Int) {
-			InventoryAction.Inventory { interaction.clickCreativeStack(stack, slotId) }.addToActions()
-		}
-
-		fun pickup(slotId: Int, button: Int = 0) = click(slotId, button, SlotActionType.PICKUP)
-
-		// Quick move action (Shift-click)
-		fun quickMove(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_MOVE)
-
-		fun swap(slotId: Int, hotbarSlot: Int) = click(slotId, hotbarSlot, SlotActionType.SWAP)
-
-		// Clone action (Creative mode)
-		fun clone(slotId: Int) = click(slotId, 2, SlotActionType.CLONE)
-
-		// Throw stack or single item
-		fun throwStack(slotId: Int) = click(slotId, 1, SlotActionType.THROW)
-
-		fun throwSingle(slotId: Int) = click(slotId, 0, SlotActionType.THROW)
-
-		// Quick craft action
-		fun quickCraftStart(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_CRAFT)
-
-		fun quickCraftDrag(slotId: Int) = click(slotId, 1, SlotActionType.QUICK_CRAFT)
-
-		fun quickCraftEnd(slotId: Int) = click(slotId, 2, SlotActionType.QUICK_CRAFT)
-
-		// Pickup all items (double-click)
-		fun pickupAll(slotId: Int) = click(slotId, 0, SlotActionType.PICKUP_ALL)
-
-		// Helper function: Move items from one slot to another
-		fun moveSlot(fromSlotId: Int, toSlotId: Int, button: Int = 0) {
-			pickup(fromSlotId, button)
-			pickup(toSlotId, button)
-		}
-
-		// Helper function: Split a stack into two
-		fun splitStack(slotId: Int, targetSlotId: Int) {
-			pickup(slotId, 1) // Pickup half the stack
-			pickup(targetSlotId, 0) // Place it in the target slot
-		}
-
-		// Helper function: Merge stacks
-		fun mergeStacks(sourceSlotId: Int, targetSlotId: Int) {
-			pickup(sourceSlotId, 0)
-			pickup(targetSlotId, 0)
-		}
-
-		fun action(action: SafeContext.() -> Unit) {
-			InventoryAction.Other(action).addToActions()
-		}
-
-		fun onComplete(callback: SafeContext.() -> Unit) {
-			onComplete = callback
-		}
-
-		private fun InventoryAction.addToActions() {
-			actions.add(this)
-		}
-	}
-
-	companion object {
-		var requestCount = 0
-
-		@InvRequestDsl
-		fun Automated.inventoryRequest(settleForLess: Boolean = false, mustPerform: Boolean = false, builder: InvRequestBuilder.() -> Unit) =
-			InvRequestBuilder(settleForLess, mustPerform).apply(builder).build()
-
-		@InvRequestDsl
-		context(automated: Automated)
-		private fun InvRequestBuilder.build() = InventoryRequest(actions, settleForLess, mustPerform, automated, onComplete = onComplete)
-	}
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/ContainerSelection.kt b/src/main/kotlin/com/lambda/interaction/material/ContainerSelection.kt
deleted file mode 100644
index fca5a4d20..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/ContainerSelection.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material
-
-import com.lambda.context.SafeContext
-import com.lambda.interaction.material.container.MaterialContainer
-
-/**
- * ContainerSelection is a class that holds a predicate for matching MaterialContainers.
- * It can be combined using "and", "or", etc.
- */
-class ContainerSelection {
-    private var selector: (MaterialContainer) -> Boolean = { true }
-
-    /**
-     * Tests whether the provided container matches this selection.
-     */
-    @ContainerSelectionDsl
-    fun matches(container: MaterialContainer): Boolean = selector(container)
-
-    /**
-     * Returns a function that matches containers having at least one stack
-     * which matches the given StackSelection.
-     */
-    @ContainerSelectionDsl
-    context(_: SafeContext)
-    fun matches(stackSelection: StackSelection): (MaterialContainer) -> Boolean =
-        { container -> container.matchingSlots(stackSelection).isNotEmpty() }
-
-    /**
-     * Returns a function that checks whether a given MaterialContainer matches the criteria
-     * defined in the provided ContainerSelection.
-     */
-    @ContainerSelectionDsl
-    fun matches(containerSelection: ContainerSelection): (MaterialContainer) -> Boolean =
-        { container -> containerSelection.matches(container) }
-
-    /**
-     * Returns a function that matches containers whose rank is any of the types provided.
-     */
-    @ContainerSelectionDsl
-    fun ofAnyType(vararg types: MaterialContainer.Rank): (MaterialContainer) -> Boolean =
-        { container -> types.contains(container.rank) }
-
-    /**
-     * Returns a function that matches containers whose rank is not any of the types provided.
-     */
-    @ContainerSelectionDsl
-    fun noneOfType(vararg types: MaterialContainer.Rank): (MaterialContainer) -> Boolean =
-        { container -> !types.contains(container.rank) }
-
-    /**
-     * Returns a function that combines two container predicates using logical AND.
-     */
-    @ContainerSelectionDsl
-    infix fun ((MaterialContainer) -> Boolean).and(other: (MaterialContainer) -> Boolean): (MaterialContainer) -> Boolean =
-        { container -> this(container) && other(container) }
-
-    /**
-     * Returns a function that combines two container predicates using logical OR.
-     */
-    @ContainerSelectionDsl
-    infix fun ((MaterialContainer) -> Boolean).or(other: (MaterialContainer) -> Boolean): (MaterialContainer) -> Boolean =
-        { container -> this(container) || other(container) }
-
-    /**
-     * Returns a function that negates the current selection predicate.
-     */
-    @ContainerSelectionDsl
-    fun ((MaterialContainer) -> Boolean).negate(): (MaterialContainer) -> Boolean =
-        { container -> !this(container) }
-
-    companion object {
-        @DslMarker
-        annotation class ContainerSelectionDsl
-
-        @ContainerSelectionDsl
-        fun selectContainer(
-            block: ContainerSelection.() -> (MaterialContainer) -> Boolean
-        ): ContainerSelection =
-            ContainerSelection().apply { selector = block() }
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt b/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt
deleted file mode 100644
index 96a8b96ad..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material
-
-import com.lambda.interaction.material.StackSelection.Companion.StackSelectionDsl
-import com.lambda.util.EnchantmentUtils.getEnchantment
-import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
-import com.lambda.util.item.ItemUtils
-import net.minecraft.block.Block
-import net.minecraft.block.BlockState
-import net.minecraft.component.ComponentType
-import net.minecraft.component.DataComponentTypes
-import net.minecraft.enchantment.Enchantment
-import net.minecraft.item.Item
-import net.minecraft.item.ItemStack
-import net.minecraft.item.consume.UseAction
-import net.minecraft.registry.RegistryKey
-import net.minecraft.registry.tag.TagKey
-import net.minecraft.screen.slot.Slot
-import java.util.*
-import kotlin.reflect.KClass
-
-/**
- * [StackSelection] is a class that holds a predicate for matching [ItemStack]s.
- */
-@Suppress("unused")
-@StackSelectionDsl
-class StackSelection {
-    var selector: (ItemStack) -> Boolean = EVERYTHING
-    var comparator: Comparator = NO_COMPARE
-    var count: Int = DEFAULT_AMOUNT
-    var inShulkerBox: Boolean = false
-
-    var item: Item? = null
-    var itemClass: KClass? = null
-    private var damage: Int? = null
-    var itemStack: ItemStack? = null
-    val stackSize: Int
-        get() = optimalStack?.maxCount ?: 64
-
-    val optimalStack: ItemStack?
-        get() = itemStack ?: item?.let { ItemStack(it, count) }
-
-    /**
-     * Filters the given [stacks], sorts them with the [comparator] and returns the first value
-     */
-    fun bestItemMatch(stacks: List): ItemStack? = filterStacks(stacks).firstOrNull()
-
-    fun matches(stack: ItemStack): Boolean = filterStack(stack)
-
-    fun filterStack(stack: ItemStack) =
-        if (inShulkerBox) stack.shulkerBoxContents.any { selector(it) }
-        else selector(stack)
-
-    fun filterSlot(slot: Slot): Boolean = filterStack(slot.stack)
-
-    fun filterStacks(stacks: List): List =
-        stacks.filter(::filterStack).sortedWith(comparator)
-
-    fun filterSlots(slots: List): List =
-        slots.filter(::filterSlot).sortedWith { slot, slot2 -> comparator.compare(slot.stack, slot2.stack) }
-
-    fun > sortBy(selector: (ItemStack) -> R?): StackSelection = apply {
-        comparator = compareBy(selector)
-    }
-
-    fun > sortByDescending(selector: (ItemStack) -> R?): StackSelection = apply {
-        comparator = compareByDescending(selector)
-    }
-
-    fun > thenBy(selector: (ItemStack) -> R?): StackSelection = apply {
-        check(comparator != NO_COMPARE) { "No comparator specified" }
-        comparator = comparator.thenBy(selector)
-    }
-
-    fun > thenByDescending(selector: (ItemStack) -> R?): StackSelection = apply {
-        check(comparator != NO_COMPARE) { "No comparator specified" }
-        comparator = comparator.thenByDescending(selector)
-    }
-
-    fun sortWith(custom: Comparator): StackSelection = apply {
-        comparator = custom
-    }
-
-    fun reversed(): StackSelection = apply {
-        comparator = comparator.reversed()
-    }
-
-    /**
-     * returns a function that finds a shulker box to push matching items into.
-     */
-    val findShulkerToPush = { stack: ItemStack ->
-        stack.shulkerBoxContents.let { inventory ->
-            if (inventory.all { selector(it) || it.isEmpty }) {
-                val storableItems = inventory.sumOf {
-//                    if (it.isEmpty) item.itemStackLimit else it.maxStackSize - it.count
-                    if (it.isEmpty) item?.maxCount ?: 0 else 0
-                }
-
-                if (storableItems > 0) storableItems else null
-            } else {
-                null
-            }
-        }
-    }
-
-    /**
-     * returns a function that finds a shulker box to pull matching items from.
-     */
-    val findShulkerToPull = { slot: Slot ->
-        slot.stack.shulkerBoxContents.let { inventory ->
-            val usableItems = inventory.sumOf { if (selector(it)) it.count else 0 }
-
-            if (usableItems > 0) slot to usableItems else null
-        }
-    }
-
-    fun any() = EVERYTHING
-    fun none() = NOTHING
-
-    /**
-     * [isItem] returns a predicate that matches a specific [Item].
-     * @param item The [Item] to be matched.
-     * @return A predicate that matches the [Item].
-     */
-    fun isItem(item: Item): (ItemStack) -> Boolean {
-        this.item = item
-        return { it.item == item }
-    }
-
-    /**
-     * Returns a predicate that matches if the `ItemStack`'s item is one of the specified items in the collection.
-     *
-     * @param items The collection of `Item` instances to match against.
-     * @return A predicate that checks if the `ItemStack`'s item is contained in the provided collection.
-     */
-    fun isOneOfItems(items: Collection): (ItemStack) -> Boolean = { it.item in items }
-
-    fun isNoneOfItems(items: Collection): (ItemStack) -> Boolean = isOneOfItems(items).not()
-
-    /**
-     * Returns a predicate that checks if a given `ItemStack` exists within the provided collection of `ItemStack`s.
-     *
-     * @param stacks A collection of `ItemStack` instances to be checked against.
-     * @return A predicate that evaluates to `true` if the given `ItemStack` is within the specified collection, otherwise `false`.
-     */
-    fun isOneOfStacks(stacks: Collection): (ItemStack) -> Boolean = stacks::contains
-
-    fun isEfficientForBreaking(blockState: BlockState): (ItemStack) -> Boolean = { itemStack ->
-        val hasEfficientTool = efficientToolCache.getOrPut(blockState) {
-            ItemUtils.tools.any { it.getMiningSpeed(it.defaultStack, blockState) > 1f }
-        }
-        if (hasEfficientTool) itemStack.item.getMiningSpeed(itemStack, blockState) > 1f
-        else true
-    }
-
-    fun isSuitableForBreaking(blockState: BlockState): (ItemStack) -> Boolean = { !blockState.isToolRequired || it.isSuitableFor(blockState) }
-
-    fun hasTag(tag: TagKey): (ItemStack) -> Boolean = { it.isIn(tag) }
-
-    fun hasUseAction(action: UseAction): (ItemStack) -> Boolean = { it.useAction == action }
-
-    fun isTool(): (ItemStack) -> Boolean = hasComponent(DataComponentTypes.TOOL)
-
-    fun isFood(): (ItemStack) -> Boolean = hasComponent(DataComponentTypes.FOOD)
-
-    fun hasComponent(type: ComponentType<*>): (ItemStack) -> Boolean = { it.components.contains(type) }
-
-    /**
-     * [isItem] returns a predicate that matches a specific [Item] instance.
-     * @param T The instance of [Item] to be matched.
-     * @return A predicate that matches the [Item].
-     */
-    inline fun  isItem(): (ItemStack) -> Boolean {
-        itemClass = T::class
-        return { it.item is T }
-    }
-
-    /**
-     * [isBlock] returns a predicate that matches a specific [Block].
-     * @param block The [Block] to be matched.
-     * @return A predicate that matches the [Block].
-     */
-    fun isBlock(block: Block): (ItemStack) -> Boolean {
-        item = block.asItem()
-        return { it.item == block.asItem() }
-    }
-
-    /**
-     * [isItemStack] returns a predicate that matches a specific [ItemStack].
-     * @param stack The [ItemStack] to be matched.
-     * @return A predicate that matches the [ItemStack].
-     */
-    fun isItemStack(stack: ItemStack): (ItemStack) -> Boolean {
-        this.itemStack = stack
-        return { ItemStack.areEqual(it, stack) }
-    }
-
-    /**
-     * [hasDamage] returns a predicate that matches a specific damage value.
-     * @param damage The damage value to be matched.
-     * @return A predicate that matches the damage value.
-     */
-    fun hasDamage(damage: Int): (ItemStack) -> Boolean {
-        this.damage = damage
-        return { it.damage == damage }
-    }
-
-    /**
-     * [hasEnchantment] returns a predicate that matches a specific [Enchantment] and level.
-     * @param enchantment The [Enchantment] to be matched.
-     * @param level The level to be matched (if -1 will look for any level above 0).
-     * @return A predicate that matches the [Enchantment] and `level`.
-     */
-    fun hasEnchantment(enchantment: RegistryKey, level: Int = -1): (ItemStack) -> Boolean = {
-        if (level < 0) {
-            it.getEnchantment(enchantment) > 0
-        } else {
-            it.getEnchantment(enchantment) == level
-        }
-    }
-
-    /**
-     * Returns the negation of the original predicate.
-     * @return A new predicate that matches if the original predicate does not match.
-     */
-    fun ((ItemStack) -> Boolean).not(): (ItemStack) -> Boolean = { !this(it) }
-
-    /**
-     * Combines two predicates using the logical AND operator.
-     * @param otherPredicate The second predicate.
-     * @return A new predicate that matches if both inputs predicate match.
-     */
-    infix fun ((ItemStack) -> Boolean).and(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean = { this(it) && otherPredicate(it) }
-
-    /**
-     * Combines two predicates using the logical OR operator.
-     * @param otherPredicate The second predicate.
-     * @return A new predicate that matches if either input predicate matches.
-     */
-    infix fun ((ItemStack) -> Boolean).or(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean = { this(it) || otherPredicate(it) }
-
-    fun ((ItemStack) -> Boolean).andIf(predicate: Boolean, otherPredicate: () -> (ItemStack) -> Boolean): (ItemStack) -> Boolean =
-        if (predicate) { { this(it) && otherPredicate()(it) } } else this
-
-    override fun toString() = buildString {
-        append("selection of ${count}x ")
-        item?.let { append(it.name.string) }
-        itemClass?.let { append(it.simpleName) }
-        itemStack?.let { append(it.name.string) }
-        damage?.let { append(" with damage $it") }
-        when (selector) {
-            EVERYTHING -> append(" everything")
-            NOTHING -> append(" nothing")
-            else -> append(" custom predicate")
-        }
-        if (inShulkerBox) append(" in shulker box")
-        if (comparator != NO_COMPARE) append(" sorted by custom comparator")
-    }
-
-    companion object {
-        @DslMarker
-        annotation class StackSelectionDsl
-
-        const val DEFAULT_AMOUNT = 1
-
-        val FULL_SHULKERS: (ItemStack) -> Boolean = { stack -> stack.shulkerBoxContents.none { it.isEmpty } }
-        val EMPTY_SHULKERS: (ItemStack) -> Boolean = { stack -> stack.shulkerBoxContents.all { it.isEmpty } }
-        val EVERYTHING: (ItemStack) -> Boolean = { true }
-        val NOTHING: (ItemStack) -> Boolean = { false }
-        val NO_COMPARE: Comparator = Comparator { _, _ -> 0 }
-
-        val efficientToolCache: MutableMap = Collections.synchronizedMap(mutableMapOf())
-
-        @StackSelectionDsl
-        fun Item.select() = selectStack { isItem(this@select) }
-
-        @StackSelectionDsl
-        fun ItemStack.select() = selectStack { isItemStack(this@select) }
-
-        @StackSelectionDsl
-        @JvmName("selectStacks")
-        fun Collection.select() = selectStack { isOneOfStacks(this@select) }
-
-        @StackSelectionDsl
-        @JvmName("selectItems")
-        fun Collection.select() = selectStack { isOneOfItems(this@select) }
-
-        @StackSelectionDsl
-        fun ((ItemStack) -> Boolean).select() = selectStack { this@select }
-
-        @StackSelectionDsl
-        fun selectStack(
-            count: Int = DEFAULT_AMOUNT,
-            inShulkerBox: Boolean = false,
-            sorter: Comparator = NO_COMPARE,
-            block: StackSelection.() -> (ItemStack) -> Boolean = { EVERYTHING },
-        ) = StackSelection().apply {
-            selector = block()
-            comparator = sorter
-            this.count = count
-            this.inShulkerBox = inShulkerBox
-        }
-    }
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt
deleted file mode 100644
index 895bdcbdb..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container
-
-import com.lambda.context.Automated
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.event.EventFlow.post
-import com.lambda.event.events.ContainerEvent
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.containers.ShulkerBoxContainer
-import com.lambda.task.Task
-import com.lambda.task.tasks.ContainerTransferTask
-import com.lambda.util.Nameable
-import com.lambda.util.item.ItemStackUtils.count
-import com.lambda.util.item.ItemStackUtils.empty
-import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
-import com.lambda.util.item.ItemStackUtils.spaceLeft
-import com.lambda.util.item.ItemUtils
-import com.lambda.util.item.ItemUtils.toItemCount
-import com.lambda.util.text.TextBuilder
-import com.lambda.util.text.TextDsl
-import com.lambda.util.text.buildText
-import com.lambda.util.text.highlighted
-import com.lambda.util.text.literal
-import com.lambda.util.text.text
-import net.minecraft.component.DataComponentTypes
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.slot.Slot
-import net.minecraft.text.Text
-
-// ToDo: Make jsonable to persistently store them
-abstract class MaterialContainer(
-    val rank: Rank
-) : Nameable, Comparable {
-    context(_: SafeContext)
-    abstract val slots: List
-    abstract var stacks: List
-    open val swapMethodPriority = 0
-    abstract val description: Text
-
-    context(automated: Automated)
-    open val replaceSorter get() = compareByDescending {
-        it.stack.isEmpty
-    }.thenByDescending {
-        it.stack.item in automated.inventoryConfig.disposables
-    }.thenByDescending {
-        !it.stack.item.components.contains(DataComponentTypes.TOOL)
-    }.thenByDescending {
-        !it.stack.item.components.contains(DataComponentTypes.FOOD)
-    }.thenByDescending {
-        !it.stack.item.components.contains(DataComponentTypes.CONSUMABLE)
-    }.thenByDescending {
-        it.stack.isStackable
-    }
-
-    @TextDsl
-    context(_: SafeContext)
-    fun TextBuilder.stock(selection: StackSelection) {
-        literal("\n")
-        literal("Contains ")
-        val available = materialAvailable(selection)
-        highlighted(if (available == Int.MAX_VALUE) "∞" else available.toItemCount())
-        literal(" of ")
-        highlighted("${selection.optimalStack?.name?.string}")
-        literal("\n")
-        literal("Could store ")
-        val left = spaceAvailable(selection)
-        highlighted(if (left == Int.MAX_VALUE) "∞" else left.toItemCount())
-        literal(" of ")
-        highlighted("${selection.optimalStack?.name?.string}")
-    }
-
-    context(_: SafeContext)
-    fun description(selection: StackSelection) =
-        buildText {
-            text(description)
-            stock(selection)
-        }
-
-    override val name: String
-        get() = buildText { text(description) }.string
-
-    context(_: SafeContext)
-    val shulkerContainer
-        get() =
-            slots.filter {
-                it.stack.item in ItemUtils.shulkerBoxes
-            }.map { slot ->
-                ShulkerBoxContainer(
-                    slot.stack.shulkerBoxContents,
-                    containedIn = this@MaterialContainer,
-                    shulkerSlot = slot
-                )
-            }.toSet()
-
-    fun update(slots: List) {
-        this.stacks = slots
-    }
-
-    class FailureTask(override val name: String) : Task() {
-        override fun SafeContext.onStart() {
-            failure(name)
-        }
-    }
-
-    context(_: SafeContext)
-    open fun InventoryRequest.InvRequestBuilder.transfer(fromHere: Slot, toSlot: Slot) {
-        if (fromHere.stack.isEmpty) pickupAndPlace(toSlot.id, fromHere.id)
-        else {
-            pickupAndPlace(fromHere.id, toSlot.id)
-            if (!toSlot.stack.isEmpty) pickup(fromHere.id)
-        }
-    }
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun transfer(stackSelection: StackSelection, destination: MaterialContainer): Boolean =
-        with(automatedSafeContext) {
-            val fromSlot = getSlot(stackSelection) ?: return false
-            val toSlot = destination.getReplaceableSlot() ?: return false
-            val transferEvent = ContainerEvent.Transfer(fromSlot, toSlot, this@MaterialContainer, destination)
-            if (transferEvent.post().isCanceled()) return false
-            return inventoryRequest {
-                if (swapMethodPriority > destination.swapMethodPriority) transfer(fromSlot, toSlot)
-                else with(destination) { transfer(toSlot, fromSlot) }
-            }.submit().done
-        }
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    fun transferByTask(stackSelection: StackSelection, destination: MaterialContainer, failIfNoMaterial: Boolean = false) =
-        ContainerTransferTask(this, destination, stackSelection, automatedSafeContext, failIfNoMaterial)
-
-    protected fun InventoryRequest.InvRequestBuilder.pickupAndPlace(fromId: Int, toId: Int) {
-        pickup(fromId)
-        pickup(toId)
-    }
-
-    context(_: SafeContext)
-    open fun matchingStacks(selection: StackSelection) =
-        selection.filterStacks(stacks)
-
-    context(_: SafeContext)
-    open fun matchingSlots(selection: StackSelection) =
-        selection.filterSlots(slots)
-
-    context(_: SafeContext)
-    open fun materialAvailable(selection: StackSelection) =
-        matchingStacks(selection).count
-
-    context(_: SafeContext)
-    open fun spaceAvailable(selection: StackSelection) =
-        matchingStacks(selection).spaceLeft + stacks.empty * selection.stackSize
-
-    context(_: AutomatedSafeContext)
-    open fun getReplaceableSlot() = slots.sortedWith(replaceSorter).firstOrNull()
-
-    context(_: SafeContext)
-    open fun getSlot(stackSelection: StackSelection): Slot? =
-        stackSelection.filterSlots(slots).firstOrNull()
-
-    enum class Rank {
-        MainHand,
-        OffHand,
-        Hotbar,
-        Inventory,
-        Creative,
-        ShulkerBox,
-        EnderChest,
-        Chest,
-        Stash
-    }
-
-    override fun compareTo(other: MaterialContainer) =
-        compareBy {
-            it.rank
-        }.compare(this, other)
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt
deleted file mode 100644
index bc54e958f..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container.containers
-
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.ContainerHandler
-import com.lambda.interaction.material.container.ExternalContainer
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.task.Task
-import com.lambda.task.tasks.OpenContainerTask
-import com.lambda.task.wrappers.TaskSupplier
-import com.lambda.task.wrappers.thenAction
-import com.lambda.util.extension.containerSlots
-import com.lambda.util.text.buildText
-import com.lambda.util.text.highlighted
-import com.lambda.util.text.literal
-import net.minecraft.block.entity.ChestBlockEntity
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.slot.Slot
-import net.minecraft.util.math.BlockPos
-
-data class ChestContainer(
-    override var stacks: List,
-    val blockPos: BlockPos,
-    val containedInStash: StashContainer? = null
-) : MaterialContainer(Rank.Chest), ExternalContainer {
-    context(safeContext: SafeContext)
-    override val slots
-        get(): List =
-            if (ContainerHandler.lastInteractedBlockEntity is ChestBlockEntity)
-                safeContext.player.currentScreenHandler.containerSlots
-            else emptyList()
-
-    override val description =
-        buildText {
-            literal("Chest at ")
-            highlighted(blockPos.toShortString())
-            containedInStash?.let { stash ->
-                literal(" (contained in ")
-                highlighted(stash.name)
-                literal(")")
-            }
-        }
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    override fun accessThen(exitAfter: Boolean, taskSupplier: TaskSupplier): Task<*> =
-        OpenContainerTask(blockPos, automatedSafeContext).thenAction {
-            taskSupplier.invoke(automatedSafeContext, Unit).thenAction {
-                if (exitAfter) automatedSafeContext.player.closeHandledScreen()
-            }
-        }
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt
deleted file mode 100644
index 5845263a1..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container.containers
-
-import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.util.text.buildText
-import com.lambda.util.text.literal
-import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen
-import net.minecraft.entity.player.PlayerEntity
-import net.minecraft.inventory.SingleStackInventory
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.PlayerScreenHandler
-import net.minecraft.screen.slot.Slot
-
-data object CreativeContainer : MaterialContainer(Rank.Creative) {
-	context(_: SafeContext)
-	override val slots: List
-		get() = emptyList()
-	override var stacks = emptyList()
-
-	override val swapMethodPriority = 11
-
-	override val description = buildText { literal("Creative") }
-
-	context(safeContext : SafeContext)
-	override fun InventoryRequest.InvRequestBuilder.transfer(fromHere: Slot, toSlot: Slot) {
-		clickCreativeStack(fromHere.stack, toSlot.id)
-		safeContext.player.currentScreenHandler.slots[toSlot.id].stack = fromHere.stack
-	}
-
-	context(safeContext: SafeContext)
-	override fun getSlot(stackSelection: StackSelection) =
-		stackSelection.optimalStack?.let { stack ->
-			Slot(
-				object : SingleStackInventory {
-					override fun getStack() = stack
-					override fun setStack(stack: ItemStack?) {}
-					override fun markDirty() {}
-					override fun canPlayerUse(player: PlayerEntity?) = false
-				},
-				0, 0, 0
-			)
-		}
-
-	context(safeContext: SafeContext)
-	override fun materialAvailable(selection: StackSelection): Int =
-		if (safeContext.player.isCreative && correctScreenHandler && selection.optimalStack != null) Int.MAX_VALUE else -1
-
-	context(safeContext: SafeContext)
-	override fun spaceAvailable(selection: StackSelection): Int =
-		if (safeContext.player.isCreative && correctScreenHandler && selection.optimalStack != null) Int.MAX_VALUE else -1
-
-	context(safeContext: SafeContext)
-	private val correctScreenHandler
-		get() = safeContext.player.currentScreenHandler is PlayerScreenHandler || safeContext.player.currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt
deleted file mode 100644
index cad16b9f5..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container.containers
-
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.ContainerHandler
-import com.lambda.interaction.handlers.ContainerHandler.findSlotsWithMaterial
-import com.lambda.interaction.material.StackSelection.Companion.select
-import com.lambda.interaction.material.container.ExternalContainer
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock
-import com.lambda.task.tasks.OpenContainerTask
-import com.lambda.task.tasks.PlaceContainerTask
-import com.lambda.task.wrappers.TaskSupplier
-import com.lambda.task.wrappers.thenAction
-import com.lambda.task.wrappers.thenOrNull
-import com.lambda.util.extension.containerSlots
-import com.lambda.util.text.buildText
-import com.lambda.util.text.literal
-import net.minecraft.block.entity.EnderChestBlockEntity
-import net.minecraft.item.ItemStack
-import net.minecraft.item.Items
-
-object EnderChestContainer : MaterialContainer(Rank.EnderChest), ExternalContainer {
-	context(safeContext: SafeContext)
-	override val slots
-		get() =
-			if (ContainerHandler.lastInteractedBlockEntity is EnderChestBlockEntity)
-				safeContext.player.currentScreenHandler.containerSlots
-			else emptyList()
-	override var stacks = emptyList()
-
-	override val description = buildText { literal("Ender Chest") }
-
-	context(automatedSafeContext: AutomatedSafeContext)
-	override fun accessThen(exitAfter: Boolean, taskSupplier: TaskSupplier) =
-		Items.ENDER_CHEST
-			.select()
-			.findSlotsWithMaterial()
-			.firstOrNull()?.let { slot ->
-				PlaceContainerTask(slot, automatedSafeContext).thenAction { pos ->
-					OpenContainerTask(pos, automatedSafeContext).thenAction {
-						taskSupplier.invoke(automatedSafeContext, Unit).thenOrNull {
-							if (exitAfter) automatedSafeContext.breakAndCollectBlock(pos, lifeMaintenance = false)
-							else null
-						}
-					}
-				}
-			}
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt
deleted file mode 100644
index 88d936f34..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container.containers
-
-import com.lambda.Lambda.mc
-import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.util.player.SlotUtils.mainHandSlots
-import com.lambda.util.text.buildText
-import com.lambda.util.text.literal
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.slot.Slot
-
-object MainHandContainer : MaterialContainer(Rank.MainHand) {
-    context(safeContext: SafeContext)
-    override val slots: List
-        get() = safeContext.player.mainHandSlots
-    override var stacks: List
-        get() = mc.player?.mainHandStack?.let { listOf(it) } ?: emptyList()
-        set(_) {}
-
-    override val swapMethodPriority = 10
-
-    override val description = buildText { literal("MainHand") }
-
-    context(safeContext: SafeContext)
-    override fun InventoryRequest.InvRequestBuilder.transfer(fromHere: Slot, toSlot: Slot) {
-        swap(toSlot.id, safeContext.player.inventory.selectedSlot)
-    }
-}
diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt
deleted file mode 100644
index 6bc95a8ad..000000000
--- a/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.interaction.material.container.containers
-
-import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.ContainerHandler
-import com.lambda.interaction.material.container.ExternalContainer
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock
-import com.lambda.task.tasks.OpenContainerTask
-import com.lambda.task.tasks.PlaceContainerTask
-import com.lambda.task.wrappers.TaskSupplier
-import com.lambda.task.wrappers.thenAction
-import com.lambda.task.wrappers.thenOrNull
-import com.lambda.threading.runSafe
-import com.lambda.util.extension.containerSlots
-import com.lambda.util.text.buildText
-import com.lambda.util.text.highlighted
-import com.lambda.util.text.literal
-import net.minecraft.block.entity.ShulkerBoxBlockEntity
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.slot.Slot
-import net.minecraft.util.math.BlockPos
-
-data class ShulkerBoxContainer(
-    override var stacks: List,
-    val containedIn: MaterialContainer,
-    val shulkerSlot: Slot,
-) : MaterialContainer(Rank.ShulkerBox), ExternalContainer {
-    context(safeContext: SafeContext)
-    override val slots
-        get(): List =
-            if (ContainerHandler.lastInteractedBlockEntity is ShulkerBoxBlockEntity)
-                safeContext.player.currentScreenHandler.containerSlots
-            else emptyList()
-
-    override val description =
-        buildText {
-            highlighted(shulkerSlot.stack.name.string)
-            literal(" in ")
-            highlighted(containedIn.name)
-            literal(" in slot ")
-            highlighted("${runSafe { slotInContainer }}")
-        }
-
-    context(_: SafeContext)
-    private val slotInContainer: Int get() = containedIn.slots.indexOf(shulkerSlot)
-
-    private var placePos = BlockPos.ORIGIN
-
-    context(automatedSafeContext: AutomatedSafeContext)
-    override fun accessThen(exitAfter: Boolean, taskSupplier: TaskSupplier) =
-        PlaceContainerTask(shulkerSlot, automatedSafeContext).thenAction { pos ->
-            placePos = pos
-            OpenContainerTask(pos, automatedSafeContext).thenAction {
-                taskSupplier.invoke(automatedSafeContext, Unit).thenOrNull {
-                    if (exitAfter) automatedSafeContext.breakAndCollectBlock(placePos)
-                    else null
-                }
-            }
-        }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/HudModule.kt b/src/main/kotlin/com/lambda/module/HudModule.kt
index 9b3d571e4..5a4372b02 100644
--- a/src/main/kotlin/com/lambda/module/HudModule.kt
+++ b/src/main/kotlin/com/lambda/module/HudModule.kt
@@ -19,7 +19,6 @@ package com.lambda.module
 
 import com.lambda.config.settings.complex.Bind
 import com.lambda.gui.Layout
-import com.lambda.module.tag.ModuleTag
 import java.awt.Color
 
 abstract class HudModule(
diff --git a/src/main/kotlin/com/lambda/module/Module.kt b/src/main/kotlin/com/lambda/module/Module.kt
index 0f106d0db..a5ab332b1 100644
--- a/src/main/kotlin/com/lambda/module/Module.kt
+++ b/src/main/kotlin/com/lambda/module/Module.kt
@@ -23,11 +23,9 @@ import com.lambda.config.ConfigCategory
 import com.lambda.config.automation.IMutableAutomationConfig
 import com.lambda.config.automation.MutableAutomationConfig
 import com.lambda.config.categories.ModuleCategory
-import com.lambda.config.entries.Setting.Companion.onValueChange
-import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
+import com.lambda.config.entries.onValueChange
+import com.lambda.config.entries.onValueChangeUnsafe
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onRelease
 import com.lambda.context.SafeContext
 import com.lambda.event.EventFlow.post
 import com.lambda.event.EventFlow.updateListenerSorting
@@ -41,7 +39,6 @@ import com.lambda.event.listener.SafeListener
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener
 import com.lambda.module.modules.client.Client
-import com.lambda.module.tag.ModuleTag
 import com.lambda.sound.LambdaSound
 import com.lambda.sound.SoundHandler.play
 import com.lambda.util.KeyCode
diff --git a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt b/src/main/kotlin/com/lambda/module/ModuleTag.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
rename to src/main/kotlin/com/lambda/module/ModuleTag.kt
index caab346cc..e01c0d70b 100644
--- a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
+++ b/src/main/kotlin/com/lambda/module/ModuleTag.kt
@@ -15,12 +15,12 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.tag
+package com.lambda.module
 
 import com.lambda.util.Nameable
 
 /**
- * The [ModuleTag] class represents a tag, that can be associated in any cardinality with a [Module].
+ * The [ModuleTag] class represents a tag, that can be associated in any cardinality with a [java.lang.Module].
  *
  * Tags are used to categorize and organize modules, making them easier to find.
  * They can be custom created as per the user's needs.
@@ -61,4 +61,4 @@ data class ModuleTag(override val name: String) : Nameable {
 
         fun isTagShown(tag: ModuleTag) = shownTags.contains(tag)
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
index 9f2199c93..cb55a5b9b 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt
@@ -22,9 +22,9 @@ import com.lambda.config.ConfigBlock
 import com.lambda.config.Group
 import com.lambda.event.events.ChatEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.ChatUtils.addresses
 import com.lambda.util.ChatUtils.colors
 import com.lambda.util.ChatUtils.discord
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt b/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
index 9fafeecbb..facc693f2 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
@@ -20,13 +20,13 @@ package com.lambda.module.modules.chat
 import com.lambda.config.blocks.FormatterConfig
 import com.lambda.config.blocks.FormatterSettings
 import com.lambda.config.editTypedSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.hide
 import com.lambda.config.withEdits
 import com.lambda.event.events.ChatEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.FormattingUtils.format
 import com.lambda.util.text.buildText
 import com.lambda.util.text.literal
@@ -47,7 +47,11 @@ object ChatTimestamp : Module(
 ) {
 	private var color: Formatting by setting("Color", Formatting.GRAY)
 		.onValueChange { from, to -> if (to.colorIndex !in 0..15) color = from }
-	private val javaColor: Color get() = Color(color.colorValue!! and 16777215)
+	private val javaColor: Color
+		get() {
+			val rgb = color.colorValue ?: return Color.GRAY
+			return Color(rgb and 16777215)
+		}
 
 	val formatter by configBlock(FormatterSettings(this))
 		.withEdits {
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/CustomChat.kt b/src/main/kotlin/com/lambda/module/modules/chat/CustomChat.kt
index 2c68b2abd..4cff328bd 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/CustomChat.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/CustomChat.kt
@@ -21,9 +21,9 @@ import com.google.common.collect.Comparators.min
 import com.lambda.command.CommandRegistry.prefix
 import com.lambda.event.events.ChatEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.NamedEnum
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/FancyChat.kt b/src/main/kotlin/com/lambda/module/modules/chat/FancyChat.kt
index b25e12a60..074b96cd8 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/FancyChat.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/FancyChat.kt
@@ -20,9 +20,9 @@ package com.lambda.module.modules.chat
 import com.lambda.command.CommandRegistry.prefix
 import com.lambda.event.events.ChatEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.ChatUtils.toBlue
 import com.lambda.util.ChatUtils.toGreen
 import com.lambda.util.ChatUtils.toLeet
diff --git a/src/main/kotlin/com/lambda/module/modules/chat/FriendHighlight.kt b/src/main/kotlin/com/lambda/module/modules/chat/FriendHighlight.kt
index 72d48f3b2..32a078dce 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/FriendHighlight.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/FriendHighlight.kt
@@ -17,12 +17,12 @@
 
 package com.lambda.module.modules.chat
 
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.event.events.ChatEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.sound.SoundHandler.playSound
 import com.lambda.util.CommunicationUtils.logError
 import com.lambda.util.text.MessageType
@@ -41,7 +41,11 @@ object FriendHighlight : Module(
 ) {
 	private var color: Formatting by setting("Color", Formatting.GREEN)
 		.onValueChange { from, to -> if (to.colorIndex !in 0..15) color = from }
-	private val javaColor: Color get() = Color(color.colorValue!! and 16777215)
+	private val javaColor: Color
+		get() {
+			val rgb = color.colorValue ?: return Color.GREEN
+			return Color(rgb and 16777215)
+		}
 
 	private val bold by setting("Bold", true)
 	private val italic by setting("Italic", false)
diff --git a/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt
index 9d62142d3..4d3dd975e 100644
--- a/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt
+++ b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt
@@ -26,7 +26,7 @@ import com.lambda.gui.dsl.ImGuiBuilder.popupModal
 import com.lambda.imgui.ImGui
 import com.lambda.imgui.flag.ImGuiWindowFlags
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runIO
 import com.lambda.util.CommunicationUtils.debug
 import com.lambda.util.CommunicationUtils.logError
diff --git a/src/main/kotlin/com/lambda/module/modules/client/Capes.kt b/src/main/kotlin/com/lambda/module/modules/client/Capes.kt
index e1e42039f..bb42498e2 100644
--- a/src/main/kotlin/com/lambda/module/modules/client/Capes.kt
+++ b/src/main/kotlin/com/lambda/module/modules/client/Capes.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.client
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object Capes : Module(
     name = "Capes",
diff --git a/src/main/kotlin/com/lambda/module/modules/client/Client.kt b/src/main/kotlin/com/lambda/module/modules/client/Client.kt
index 180314afd..56449d62e 100644
--- a/src/main/kotlin/com/lambda/module/modules/client/Client.kt
+++ b/src/main/kotlin/com/lambda/module/modules/client/Client.kt
@@ -22,7 +22,7 @@ import com.lambda.config.Tab
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.interaction.construction.simulation.result.Drawable
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import java.awt.Color
 
 object Client : Module(
@@ -47,6 +47,7 @@ object Client : Module(
 
 	@Tab(DEBUG_TAB) val ignoreItemDropWarnings by setting("Ignore Drop Warnings", false, "Hides the item drop warnings from the break manager")
 	@Tab(DEBUG_TAB) val verboseDebug by setting("Verbose Debug", false, "Prints more, and more detailed, debug logs")
+	@Tab(DEBUG_TAB) val showInvisibleTasks by setting("Show Invisible Tasks", false, "Shows utility tasks such as chaining wrappers in the Task Flow HUD")
 
 	@Volatile
 	var drawables = listOf()
diff --git a/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/src/main/kotlin/com/lambda/module/modules/client/Discord.kt
index 7858e254f..2679a1052 100644
--- a/src/main/kotlin/com/lambda/module/modules/client/Discord.kt
+++ b/src/main/kotlin/com/lambda/module/modules/client/Discord.kt
@@ -24,7 +24,7 @@ import com.lambda.event.EventFlow
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listenOnce
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.network.NetworkHandler.updateToken
 import com.lambda.network.api.v1.endpoints.linkDiscord
 import com.lambda.threading.runConcurrent
diff --git a/src/main/kotlin/com/lambda/module/modules/client/ModuleNotifier.kt b/src/main/kotlin/com/lambda/module/modules/client/ModuleNotifier.kt
index 95425ca6b..40a791379 100644
--- a/src/main/kotlin/com/lambda/module/modules/client/ModuleNotifier.kt
+++ b/src/main/kotlin/com/lambda/module/modules/client/ModuleNotifier.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.client
 import com.lambda.event.events.ModuleEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.log
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoArmor.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoArmor.kt
index fde140314..b27eea905 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/AutoArmor.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoArmor.kt
@@ -17,21 +17,26 @@
 
 package com.lambda.module.modules.combat
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
+import com.lambda.config.hideBlockExcept
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.container.containers.ArmorContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackAndSlot
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.EnchantmentUtils.getEnchantment
 import com.lambda.util.item.ItemUtils.armorSlot
-import com.lambda.util.player.SlotUtils.armorSlots
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
 import net.minecraft.component.DataComponentTypes
 import net.minecraft.enchantment.Enchantment
 import net.minecraft.enchantment.Enchantments
@@ -62,50 +67,52 @@ object AutoArmor : Module(
 
 	var overriddenElytraPriority: Boolean? = null
 
-	val SORTER = compareByDescending {
-		if (it.stack.isDamageable && 1 - (it.stack.damage.toFloat() / it.stack.maxDamage) < minDurabilityPercentage.toFloat() / 100)
-			-Double.MAX_VALUE
-		else 0.0
-	}.thenByDescending {
-		if (overriddenElytraPriority ?: elytraPriority) {
-			if (it.stack.item == Items.ELYTRA) 1.0
+	val SORTER =
+		compareByDescending> {
+			if (it.stack.isDamageable && 1 - (it.stack.damage.toFloat() / it.stack.maxDamage) < minDurabilityPercentage.toFloat() / 100)
+				-Double.MAX_VALUE
 			else 0.0
-		} else 0.0
-	}.thenByDescending {
-		it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
-			?.modifiers
-			?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR }
-			?.modifier?.value
-			?: 0.0
-	}.thenByDescending {
-		it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
-			?.modifiers
-			?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR_TOUGHNESS }
-			?.modifier?.value
-			?: 0.0
-	}.thenByDescending {
-		val stack = it.stack
-		when {
-			stack.isIn(ItemTags.FOOT_ARMOR) -> stack.getEnchantment(feetProtection.enchant)
-			stack.isIn(ItemTags.LEG_ARMOR) -> stack.getEnchantment(legProtection.enchant)
-			stack.isIn(ItemTags.CHEST_ARMOR) -> stack.getEnchantment(chestProtection.enchant)
-			else -> stack.getEnchantment(headProtection.enchant)
-		}
-	}.thenByDescending { slot ->
-		Protection.entries.fold(0) { acc, protection ->
-			acc + slot.stack.getEnchantment(protection.enchant)
+		}.thenByDescending {
+			if (overriddenElytraPriority ?: elytraPriority) {
+				if (it.stack.item == Items.ELYTRA) 1.0
+				else 0.0
+			} else 0.0
+		}.thenByDescending {
+			it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
+				?.modifiers
+				?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR }
+				?.modifier?.value
+				?: 0.0
+		}.thenByDescending {
+			it.stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, null)
+				?.modifiers
+				?.find { modifier -> modifier.attribute == EntityAttributes.ARMOR_TOUGHNESS }
+				?.modifier?.value
+				?: 0.0
+		}.thenByDescending {
+			val stack = it.stack
+			when {
+				stack.isIn(ItemTags.FOOT_ARMOR) -> stack.getEnchantment(feetProtection.enchant)
+				stack.isIn(ItemTags.LEG_ARMOR) -> stack.getEnchantment(legProtection.enchant)
+				stack.isIn(ItemTags.CHEST_ARMOR) -> stack.getEnchantment(chestProtection.enchant)
+				else -> stack.getEnchantment(headProtection.enchant)
+			}
+		}.thenByDescending { stackAndSlot ->
+			Protection.entries.fold(0) { acc, protection ->
+				acc + stackAndSlot.stack.getEnchantment(protection.enchant)
+			}
+		}.thenByDescending { stackAndSlot ->
+			stackAndSlot.stack.getEnchantment(Enchantments.UNBREAKING) +
+					stackAndSlot.stack.getEnchantment(Enchantments.MENDING)
+		}.thenByDescending { stackAndSlot ->
+			stackAndSlot.slot?.index in 0..8
 		}
-	}.thenByDescending { slot ->
-		slot.stack.getEnchantment(Enchantments.UNBREAKING) +
-				slot.stack.getEnchantment(Enchantments.MENDING)
-	}.thenByDescending { slot ->
-		slot.index in 0..8
-	}
 
 	init {
 		setDefaultAutomationConfig()
 			.withEdits {
-				hideAllExcept(::inventoryConfig)
+				hideAllExcept(::buildConfig, ::inventoryConfig)
+				hideBlockExcept(::buildConfig, buildConfig::inventoryLimit)
 			}
 
 		listen {
@@ -119,20 +126,32 @@ object AutoArmor : Module(
 
 	fun SafeContext.tick() {
 		tickedThisTick = true
-		val armorSlots = player.armorSlots
-
-		val swappable = player.hotbarAndInventorySlots
-			.filter { it.stack.isEquipable && (!ignoreBinding || it.stack.getEnchantment(Enchantments.BINDING_CURSE) <= 0) }
-			.sortedWith(SORTER)
-			.distinctBy { it.stack.armorSlot }
+		val armorSlots = findSlots(containerSelection = ArmorContainer.select())
+			?.toList()
+			?: return
+
+		val swappable =
+			findSlots(
+				stackSelection(StackSelection.UNBOUNDED) {
+					predicate { stack, _ -> stack.isEquipable(armorSlots) }
+					if (ignoreBinding) withoutEnchantment(Enchantments.BINDING_CURSE)
+					sortedWith(SORTER)
+				},
+				ContainerSelection.HOTBAR_AND_INVENTORY
+			)?.distinctBy { it.stack.armorSlot }
+				?: return
 
 		val swaps = mutableListOf>()
-		armorSlots.forEach { equipped ->
-			val new = swappable.find { new ->
-				equipped.canInsert(new.stack) && SORTER.compare(equipped, new) > 0
-			} ?: return@forEach
-
-			swaps.add(Pair(new, equipped))
+		armorSlots
+			.map { StackAndSlot(it.stack, it) }
+			.forEach { equipped ->
+			val new =
+				swappable.find { new ->
+					equipped.slot.canInsert(new.stack) &&
+							SORTER.compare(equipped, StackAndSlot(new.stack, new)) > 0
+				} ?: return@forEach
+
+			swaps.add(Pair(new, equipped.slot))
 		}
 
 		if (swaps.isEmpty()) return
@@ -144,9 +163,8 @@ object AutoArmor : Module(
 		}.submit()
 	}
 
-	context(safeContext: SafeContext)
-	private val ItemStack.isEquipable
-		get() = safeContext.player.armorSlots.any { it.canInsert(this) }
+	context(_: SafeContext)
+	private fun ItemStack.isEquipable(armorSlots: List) = armorSlots.any { it.canInsert(this) }
 
 	private enum class Protection(val enchant: RegistryKey) {
 		Protection(Enchantments.PROTECTION),
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
index da32a1807..723314ec8 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt
@@ -17,17 +17,20 @@
 
 package com.lambda.module.modules.combat
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.FriendHandler
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.StackSelection.Companion.select
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.moveStack
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
+import com.lambda.threading.runSafeAutomated
 import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal
 import com.lambda.util.combat.DamageUtils.isFallDeadly
 import com.lambda.util.extension.fullHealth
@@ -57,27 +60,21 @@ object AutoTotem : Module(
 
     init {
 		setDefaultAutomationConfig()
-			.withEdits {
-				hideAllExcept(::inventoryConfig)
-			}
+			.withEdits { hideAllExcept(::inventoryConfig) }
 
         listen {
             if (!always && Reason.entries.none { it.check(this) }) return@listen
 
-            if ((!ignoreWhenHolding || !player.isHolding(Items.TOTEM_OF_UNDYING)) && player.offHandStack.item != Items.TOTEM_OF_UNDYING) {
-                Items.TOTEM_OF_UNDYING.select()
-	                .filterSlots(player.currentScreenHandler.slots)
-	                .takeIf { it.isNotEmpty() }
-	                ?.let { totems ->
-		                val cursor = player.currentScreenHandler.cursorStack
-		                val targetSlot = player.currentScreenHandler.slots
-			                .findLast { !cursor.isEmpty && it.canInsert(cursor) }
-
-						inventoryRequest {
-							targetSlot?.let { pickup(it.id, 0) }
-							swap(totems.first().id, 40)
-						}.submit()
-	                }
+            val swapWhileHolding = !ignoreWhenHolding || player.mainHandStack.item != Items.TOTEM_OF_UNDYING
+            if (swapWhileHolding && player.offHandStack.item != Items.TOTEM_OF_UNDYING) {
+                runSafeAutomated {
+                    Items.TOTEM_OF_UNDYING
+                        .select()
+                        .moveStack(
+                            ContainerSelection.HOTBAR_AND_INVENTORY,
+                            OffhandContainer.select()
+                        )
+                }
             }
         }
     }
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/Criticals.kt b/src/main/kotlin/com/lambda/module/modules/combat/Criticals.kt
index a4c848b0d..1755d1335 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/Criticals.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/Criticals.kt
@@ -20,10 +20,10 @@ package com.lambda.module.modules.combat
 import com.lambda.context.SafeContext
 import com.lambda.event.events.PlayerEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.PacketUtils.sendPacket
 import com.lambda.util.extension.rotation
 import com.lambda.util.math.component1
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/FakePlayer.kt b/src/main/kotlin/com/lambda/module/modules/combat/FakePlayer.kt
index 177e06ad3..987e50619 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/FakePlayer.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/FakePlayer.kt
@@ -24,7 +24,7 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.SafeListener.Companion.listenConcurrently
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.network.mojang.getProfile
 import com.lambda.threading.onShutdown
 import com.lambda.util.Timer
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt b/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
index 878d4470e..d14f696fd 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.combat
 
 import com.lambda.config.Tab
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.TargetingSettings
 import com.lambda.config.editSetting
 import com.lambda.config.hide
@@ -28,18 +28,20 @@ import com.lambda.context.SafeContext
 import com.lambda.event.events.InventoryEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.NamedEnum
 import com.lambda.util.item.ItemStackUtils.attackDamage
 import com.lambda.util.item.ItemStackUtils.attackSpeed
 import com.lambda.util.math.random
 import com.lambda.util.player.RotationUtils.lookAtEntity
-import com.lambda.util.player.SlotUtils.hotbarStacks
 import net.minecraft.entity.Entity
 import net.minecraft.item.ItemStack
 import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket
@@ -98,9 +100,7 @@ object KillAura : Module(
                         ::maxBuildDependencies, ::blockReach
                     )
                 }
-                hotbarConfig.apply {
-                    ::tickStageMask.editSetting { defaultValue(mutableSetOf(TickEvent.Pre)) }
-                }
+                hotbarConfig::tickStageMask.editSetting { defaultValue(mutableSetOf(TickEvent.Pre)) }
             }
 
         listen { cooldownFromSwap = true }
@@ -119,14 +119,21 @@ object KillAura : Module(
                 }
 
                 if (swap) {
-                    val selection = selectStack().sortByDescending {
-                        damageMode.block(this, it)
-                    }
-
-                    selection.bestItemMatch(player.hotbarStacks)?.let { bestStack ->
-                        val slotId = player.hotbarStacks.indexOf(bestStack)
-                        if (!HotbarRequest(slotId, this@KillAura, nowOrNothing = false).submit().done) return@listen
-                    }
+                    val selection =
+                        stackSelection {
+                            sortedWith {
+                                compareByDescending {
+                                    damageMode.block(this@listen, it.stack)
+                                }
+                            }
+                        }
+
+                    val slot =
+                        findSlot(
+                            selection,
+                            HotbarContainer.select()
+                        ) ?: return@listen
+                    if (!hotbarRequest(slot.index).submit().done) return@listen
                 }
 
                 if (!rotated) return@listen
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/PlayerTrap.kt b/src/main/kotlin/com/lambda/module/modules/combat/PlayerTrap.kt
index 104fb4a25..bdac416c0 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/PlayerTrap.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/PlayerTrap.kt
@@ -17,24 +17,26 @@
 
 package com.lambda.module.modules.combat
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editTypedSettings
 import com.lambda.config.hideBlock
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.interaction.construction.blueprint.TickingBlueprint.Companion.tickingBlueprint
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.FriendHandler.isFriend
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.start
+import com.lambda.task.tasks.build
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.extension.shrinkByEpsilon
 import com.lambda.util.item.ItemUtils.block
-import com.lambda.util.math.flooredBlockPos
-import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
+import com.lambda.util.math.blockPos
 import com.lambda.util.world.entitySearch
 import net.minecraft.block.Blocks
 import net.minecraft.client.network.OtherClientPlayerEntity
@@ -69,23 +71,40 @@ object PlayerTrap : Module(
 
 		onEnable {
 			task = tickingBlueprint {
-				val block = player.hotbarAndInventoryStacks.firstOrNull {
-					it.item is BlockItem && blocks.contains(it.item.block)
-				}?.item?.block ?: return@tickingBlueprint emptyMap()
-				val targetPlayer = if (self) player
-				else entitySearch(
-					buildConfig.blockReach,
-					player.eyePos.flooredBlockPos
-				).firstOrNull { friends || !isFriend(it.gameProfile) } ?: return@tickingBlueprint emptyMap()
+				val selection =
+					stackSelection {
+						isItem()
+						predicate { stack, _ ->
+							blocks.contains(stack.item.block)
+						}
+					}
+
+				val block = findStack(selection, ContainerSelection.HOTBAR_AND_INVENTORY, sorted = false)
+					?.item?.block
+					?: return@tickingBlueprint emptyMap()
+
+				val targetPlayer =
+					if (self) player
+					else entitySearch(
+						buildConfig.blockReach,
+						player.eyePos.blockPos
+					).firstOrNull { friends || !isFriend(it.gameProfile) }
+						?: return@tickingBlueprint emptyMap()
+
 				getTrapPositions(targetPlayer).associateWith { TargetState.Block(block) }
-			}.build(finishOnDone = false).run()
+			}.build(finishOnDone = false)
+				.start()
+		}
+
+		onDisable {
+			task?.cancel()
+			task = null
 		}
-		onDisable { task?.cancel(); task = null }
 	}
 
 	fun SafeContext.getTrapPositions(player: PlayerEntity): Set {
-		val min = player.boundingBox.shrinkByEpsilon().minPos.flooredBlockPos.add(-1, -1, -1)
-		val max = player.boundingBox.shrinkByEpsilon().maxPos.flooredBlockPos.add(1, 1, 1)
+		val min = player.boundingBox.shrinkByEpsilon().minPos.blockPos.add(-1, -1, -1)
+		val max = player.boundingBox.shrinkByEpsilon().maxPos.blockPos.add(1, 1, 1)
 
 		return buildSet {
 			(min.x + 1..
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/Surround.kt b/src/main/kotlin/com/lambda/module/modules/combat/Surround.kt
index 52b790afc..04e79ec4f 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/Surround.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/Surround.kt
@@ -17,20 +17,22 @@
 
 package com.lambda.module.modules.combat
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editTypedSettings
 import com.lambda.config.hideBlock
 import com.lambda.config.withEdits
 import com.lambda.interaction.construction.blueprint.TickingBlueprint.Companion.tickingBlueprint
 import com.lambda.interaction.construction.verify.TargetState
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.PlayerTrap.getTrapPositions
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.start
+import com.lambda.task.tasks.build
 import com.lambda.util.item.ItemUtils.block
-import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
 import net.minecraft.block.Blocks
 import net.minecraft.item.BlockItem
 
@@ -59,13 +61,22 @@ object Surround : Module(
 
 		onEnable {
 			task = tickingBlueprint {
-				val block = player.hotbarAndInventoryStacks.firstOrNull {
-					it.item is BlockItem && blocks.contains(it.item.block)
-				}?.item?.block ?: return@tickingBlueprint emptyMap()
+				val selection =
+					stackSelection {
+						predicate { stack, _ ->
+							stack.item is BlockItem && blocks.contains(stack.item.block)
+						}
+					}
+
+				val block = findStack(selection, ContainerSelection.HOTBAR_AND_INVENTORY, sorted = false)
+					?.item?.block
+					?: return@tickingBlueprint emptyMap()
+
 				getTrapPositions(player)
 					.filter { it.y <= player.blockPos.y }
 					.associateWith { TargetState.Block(block) }
-			}.build(finishOnDone = false).run()
+			}.build(finishOnDone = false)
+				.start()
 		}
 		onDisable { task?.cancel(); task = null }
 	}
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/autodisconnect/AutoDisconnect.kt b/src/main/kotlin/com/lambda/module/modules/combat/autodisconnect/AutoDisconnect.kt
index 75f9e5f19..f6562419b 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/autodisconnect/AutoDisconnect.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/autodisconnect/AutoDisconnect.kt
@@ -26,10 +26,10 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.events.WorldEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.gui.components.ClickGuiLayout
-import com.lambda.interaction.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.network.AutoReconnect
-import com.lambda.module.tag.ModuleTag
 import com.lambda.sound.SoundHandler.playSound
 import com.lambda.util.CommunicationUtils
 import com.lambda.util.CommunicationUtils.info
@@ -42,8 +42,8 @@ import com.lambda.util.combat.CombatUtils.crystalDamage
 import com.lambda.util.combat.DamageUtils.isFallDeadly
 import com.lambda.util.extension.fullHealth
 import com.lambda.util.extension.tickDeltaF
-import com.lambda.util.item.ItemStackUtils.bundleContents
-import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
+import com.lambda.util.item.ItemStackUtils.bundleStacks
+import com.lambda.util.item.ItemStackUtils.shulkerBoxStacks
 import com.lambda.util.player.MovementUtils.moveDelta
 import com.lambda.util.player.PlayerUtils.isIn2b2tQueue
 import com.lambda.util.player.SlotUtils.allStacks
@@ -705,7 +705,7 @@ object AutoDisconnect : Module(
     private fun itemRow(stack: ItemStack, label: String? = null, selected: Boolean = false): DetailSection {
         val header = itemHeader(stack, label, selected)
         val enchantments = stack.forEachEnchantment { entry, level -> Enchantment.getName(entry, level) }
-        val contents = (stack.shulkerBoxContents + stack.bundleContents).filter { !it.isEmpty }
+        val contents = (stack.shulkerBoxStacks + stack.bundleStacks).filter { !it.isEmpty }
 
         if (enchantments.isEmpty() && contents.isEmpty()) {
             return DetailSection.TextSection(header)
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt
index 21cdf2182..817d57925 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.combat.crystalaura
 
 import com.lambda.config.Tab
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.TargetingSettings
 import com.lambda.config.blocks.WorldLineSettings
 import com.lambda.config.forEachSetting
@@ -31,9 +31,9 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.crystalaura.CrystalAuraExploding.explodeInternal
 import com.lambda.module.modules.combat.crystalaura.CrystalAuraPlacing.placeInternal
-import com.lambda.module.tag.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.threading.runSafeGameScheduled
 import com.lambda.util.BlockUtils.blockState
@@ -44,8 +44,8 @@ import com.lambda.util.combat.CombatUtils.crystalDamage
 import com.lambda.util.extension.fullHealth
 import com.lambda.util.math.MathUtils.ceilToInt
 import com.lambda.util.math.MathUtils.roundToStep
+import com.lambda.util.math.blockPos
 import com.lambda.util.math.distSq
-import com.lambda.util.math.flooredBlockPos
 import com.lambda.util.math.getHitVec
 import com.lambda.util.math.minus
 import com.lambda.util.math.plus
@@ -87,7 +87,7 @@ object CrystalAura : Module(
     @Tab(PLACEMENT_TAB) private val placeRange by setting("Place Range", 4.6, 1.0..7.0, 0.1, "Range to place crystals", " blocks")
     @Tab(PLACEMENT_TAB) val placeDelay by setting("Place Delay", 50L, 0L..1000L, 1L, "Delay between placement attempts", " ms")
     @Tab(PLACEMENT_TAB) val swap by setting("Swap", true, "Swaps to crystals")
-    @Tab(PLACEMENT_TAB) val swapHand by setting("Swap Hand", Hand.MAIN_HAND, "Which hand to swap the crystal to") { swap }
+    @Tab(PLACEMENT_TAB) val swapHand: Hand by setting("Swap Hand", Hand.MAIN_HAND, "Which hand to swap the crystal to") { swap }
     @Tab(PLACEMENT_TAB) val priorityMode by setting("Crystal Priority", Priority.Damage)
     @Tab(PLACEMENT_TAB) private val minDamageAdvantage by setting("Min Damage Advantage", 4.0, 1.0..10.0, 0.5) { priorityMode == Priority.Advantage }
     @Tab(PLACEMENT_TAB) private val minTargetDamage by setting("Min Target Damage", 8.0, 0.0..20.0, 0.5, "Minimum target damage to use crystals")
@@ -508,7 +508,7 @@ object CrystalAura : Module(
     }
 
     private val EndCrystalEntity.baseBlockPos get() =
-        (pos - Vec3d(0.0, 0.5, 0.0)).flooredBlockPos
+        (pos - Vec3d(0.0, 0.5, 0.0)).blockPos
 
     val BlockPos.crystalPosition get() =
         this.getHitVec(Direction.UP)
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt
index e51b411da..3a85e4ca2 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt
@@ -19,14 +19,15 @@ package com.lambda.module.modules.combat.crystalaura
 
 import com.lambda.Lambda
 import com.lambda.context.Automated
-import com.lambda.interaction.handlers.ContainerHandler.transfer
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.containers.HotbarContainer
-import com.lambda.interaction.material.container.containers.OffHandContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.moveStack
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.modules.combat.crystalaura.CrystalAura.ActionType
 import com.lambda.module.modules.combat.crystalaura.CrystalAura.crystalPosition
 import com.lambda.module.modules.combat.crystalaura.CrystalAura.explodeDelay
@@ -65,7 +66,6 @@ import net.minecraft.util.math.Direction
 import net.minecraft.util.math.Vec3d
 import kotlin.time.Duration.Companion.milliseconds
 
-
 /**
  * Represents the damage information resulting from placing an end crystal on a given [blockPos]
  * and causing an explosion that targets current target entity.
@@ -122,7 +122,7 @@ class Opportunity(
 			return@runSafe
 		}
 		var crystalHand: Hand? = null
-		val selection = StackSelection.selectStack { isItem(Items.END_CRYSTAL) }
+		val selection = stackSelection { isItem(Items.END_CRYSTAL) }
 		if ((swapHand == Hand.MAIN_HAND && player.mainHandStack.item != Items.END_CRYSTAL) ||
 			(swapHand == Hand.OFF_HAND && player.offHandStack.item != Items.END_CRYSTAL)
 		) automated.runSafeAutomated {
@@ -137,19 +137,19 @@ class Opportunity(
 
 			val swapTo = when (swapHand) {
 				Hand.MAIN_HAND -> HotbarContainer
-				Hand.OFF_HAND -> OffHandContainer
-			}
+				Hand.OFF_HAND -> OffhandContainer
+			}.select()
 
 			if (itemToUse == null || swapHand == Hand.MAIN_HAND) {
 				if (swapHand == Hand.MAIN_HAND) {
-					val itemStack = selection.bestItemMatch(player.hotbarStacks)
+					val itemStack = selection.bestMatch(player.hotbarStacks)
 					if (itemStack == null) { // retrieve to hotbar
-						if (!selection.transfer(swapTo)) return@runSafe
+						if (!selection.moveStack(toSelection = swapTo)) return@runSafe
 					}
 					val s = player.hotbarStacks.indexOf(itemStack)
-					if ((!HotbarRequest(s, automated, nowOrNothing = false).submit().done) && itemToUse == null) return@runSafe
+					if ((!hotbarRequest(s, nowOrNothing = false).submit().done) && itemToUse == null) return@runSafe
 				} else { // retrieve to offhand
-					if (!selection.transfer(swapTo)) return@runSafe
+					if (!selection.moveStack(toSelection = swapTo)) return@runSafe
 				}
 			}
 		}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/BaritoneTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/BaritoneTest.kt
index 0edf97ccc..82e684486 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/BaritoneTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/BaritoneTest.kt
@@ -20,9 +20,9 @@ package com.lambda.module.modules.debug
 import baritone.api.pathing.goals.GoalXZ
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object BaritoneTest : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
index 8dd42d214..71db39448 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.debug
 
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.world.blockSearch
 import net.minecraft.block.Blocks
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt
index fc9ceb73a..364e2bc90 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt
@@ -19,11 +19,11 @@ package com.lambda.module.modules.debug
 
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.material.StackSelection.Companion.select
+import com.lambda.interaction.container.selection.select
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
-import com.lambda.task.tasks.AcquireMaterialTask.Companion.acquire
+import com.lambda.module.ModuleTag
+import com.lambda.task.start
+import com.lambda.task.tasks.acquireStack
 import net.minecraft.item.Items
 
 @Suppress("unused")
@@ -38,9 +38,8 @@ object ContainerTest : Module(
         }
 
         onEnable {
-            acquire {
-                Items.OBSIDIAN.select()
-            }.run()
+            acquireStack { Items.OBSIDIAN.select() }
+                .start()
         }
     }
 }
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/ContainerTransferTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/ContainerTransferTest.kt
new file mode 100644
index 000000000..d0bebae75
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/debug/ContainerTransferTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.module.modules.debug
+
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.event.events.ButtonEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.PlacedContainer
+import com.lambda.interaction.container.containers.external.DoubleChestContainer
+import com.lambda.interaction.container.selection.containerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findContainers
+import com.lambda.interaction.handler.handlers.ContainerSearchScope
+import com.lambda.module.Module
+import com.lambda.module.ModuleTag
+import com.lambda.task.start
+import com.lambda.task.tasks.transfer
+import com.lambda.threading.runSafeAutomated
+import com.lambda.util.BlockUtils.blockState
+import com.lambda.util.CommunicationUtils.info
+import com.lambda.util.math.distSq
+import net.minecraft.block.ChestBlock
+import net.minecraft.item.Items
+import net.minecraft.util.hit.BlockHitResult
+import net.minecraft.util.math.BlockPos
+import org.lwjgl.glfw.GLFW
+
+@Suppress("unused")
+object ContainerTransferTest : Module(
+    name = "ContainerTransferTest",
+    description = "Test moving stack selections over 1 stack from one external container to another.",
+    tag = ModuleTag.DEBUG,
+) {
+    private val amountToMove by setting("Amount", 128, 1..1000)
+
+    private var sourcePos: BlockPos? = null
+    private var destPos: BlockPos? = null
+
+    init {
+        onEnable {
+            sourcePos = null
+            destPos = null
+            info("Middle-click source chest, then middle-click destination chest.")
+        }
+
+        listen { event ->
+            if (event.button != GLFW.GLFW_MOUSE_BUTTON_MIDDLE || event.action != 1) return@listen
+            
+            val hitResult = mc.crosshairTarget as? BlockHitResult ?: return@listen
+            val pos = hitResult.blockPos
+            
+            val block = blockState(pos).block
+            if (block !is ChestBlock) return@listen
+            
+            if (sourcePos == null) {
+                sourcePos = pos
+                info("Source container set to $pos. Now middle-click destination.")
+                event.cancel()
+            } else if (destPos == null && pos != sourcePos) {
+                destPos = pos
+                info("Destination container set to $pos. Starting transfer task...")
+                event.cancel()
+                
+                runSafeAutomated {
+                    val from = sourcePos
+                    if (from != null) startTransferTask(from, pos)
+                }
+                
+                disable()
+            }
+        }
+    }
+
+    private fun AutomatedSafeContext.startTransferTask(fromPos: BlockPos, toPos: BlockPos) {
+        info("--- DEBUG INFO ---")
+        val allChests = findContainers(containerSelection(ContainerSearchScope.All) { ofAnyType(ContainerType.Chest) }).toList()
+        info("Total known Chest containers in scope All: ${allChests.size}")
+        
+        val matchedFrom = allChests.filter { 
+            val placed = it as? PlacedContainer ?: return@filter false
+            placed.pos distSq fromPos <= 4.0 ||
+                (placed is DoubleChestContainer && (placed.leftPos distSq fromPos <= 4.0 || placed.rightPos distSq fromPos <= 4.0))
+        }
+        info("Chests matching fromPos distance: ${matchedFrom.size}")
+        
+        if (matchedFrom.isNotEmpty()) {
+            val fromContainer = matchedFrom.first()
+            val totalItems = fromContainer.stacks.sumOf { it.count }
+            info("First matched source container has $totalItems total items.")
+            if (totalItems < amountToMove) {
+                info("WARNING: Source container has fewer items ($totalItems) than required ($amountToMove). The task will fail!")
+            }
+        } else {
+            info("WARNING: No chest found near fromPos. Did you open it at least once to save it?")
+        }
+
+        val matchedTo = allChests.filter { 
+            val placed = it as? PlacedContainer ?: return@filter false
+            placed.pos distSq toPos <= 4.0 ||
+                (placed is DoubleChestContainer && (placed.leftPos distSq toPos <= 4.0 || placed.rightPos distSq toPos <= 4.0))
+        }
+        info("Chests matching toPos distance: ${matchedTo.size}")
+        if (matchedTo.isNotEmpty()) {
+            val toContainer = matchedTo.first()
+            info("First matched destination container has ${toContainer.stacks.size} slots (${toContainer.stacks.count { it.isEmpty }} empty).")
+        } else {
+            info("WARNING: No chest found near toPos. Did you open it at least once to save it?")
+        }
+        info("------------------")
+
+        val fromSelection =
+            containerSelection(ContainerSearchScope.All) {
+                ofAnyType(ContainerType.Chest)
+                predicate { container ->
+                    val placed = container as? PlacedContainer ?: return@predicate false
+                    placed.pos distSq fromPos <= 4.0 ||
+                        (placed is DoubleChestContainer && (placed.leftPos distSq fromPos <= 4.0 || placed.rightPos distSq fromPos <= 4.0))
+                }
+            }
+        val toSelection =
+            containerSelection(ContainerSearchScope.All) {
+                ofAnyType(ContainerType.Chest)
+                predicate { container ->
+                    val placed = container as? PlacedContainer ?: return@predicate false
+                    placed.pos distSq toPos <= 4.0 ||
+                        (placed is DoubleChestContainer && (placed.leftPos distSq toPos <= 4.0 || placed.rightPos distSq toPos <= 4.0))
+                }
+            }
+
+        val stackSelection = Items.OBSIDIAN.select(amountToMove)
+
+        stackSelection.transfer(fromSelection, toSelection)
+            .onSuccess { info("Transfer task completed!") }
+            .onFailure { info("Transfer task failed: $it") }
+            .start()
+    }
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/DebugRendererModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/DebugRendererModule.kt
index 3e417f071..e4abf5b44 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/DebugRendererModule.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/DebugRendererModule.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.debug
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object DebugRendererModule: Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/FallTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/FallTest.kt
index d397b76a5..9ec65d9e8 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/FallTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/FallTest.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.debug
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.combat.DamageUtils.fallDamage
 import com.lambda.util.combat.DamageUtils.isFallDeadly
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt b/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt
index 47ec92134..85af88c45 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt
@@ -22,7 +22,7 @@ import com.lambda.event.events.InventoryEvent
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.DynamicReflectionSerializer.dynamicString
 import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/PropertyPrinter.kt b/src/main/kotlin/com/lambda/module/modules/debug/PropertyPrinter.kt
index 6b5a18da1..77a3dabd2 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/PropertyPrinter.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/PropertyPrinter.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.debug
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.FolderRegistry
 import com.lambda.util.extension.resolveFile
 import net.minecraft.block.Block
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
index 1a0dc8dc7..5d5b177f5 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt
@@ -21,7 +21,7 @@ import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRend
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.graphics.util.DynamicAABB.Companion.dynamicBox
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.tickDelta
 import com.lambda.util.math.setAlpha
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
index 0e8692900..23b48c4a1 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
@@ -32,7 +32,7 @@ import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRend
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.graphics.outline.OutlineStyle
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.prevPos
 import com.lambda.util.extension.tickDelta
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
index 6e8a3e4a0..7b32db1e1 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
@@ -19,9 +19,9 @@ package com.lambda.module.modules.debug
 
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.player.RotationUtils.lookAt
 import net.minecraft.util.hit.HitResult
 
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt
index 2facef200..f3430fc74 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt
@@ -26,7 +26,7 @@ import com.lambda.config.blocks.ScreenTextSettings
 import com.lambda.config.blocks.WorldLineSettings
 import com.lambda.config.blocks.WorldTextSettings
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object SettingsTestModule : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt b/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
index 092419a65..36cdc7702 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
@@ -17,16 +17,17 @@
 
 package com.lambda.module.modules.debug
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.PlayerEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.hotbar.HotbarRequest
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 
+@Suppress("unused")
 object SilentSwap : Module(
     name = "SilentSwap",
     description = "SilentSwap",
@@ -39,7 +40,7 @@ object SilentSwap : Module(
             }
 
         listen {
-            if (!HotbarRequest(0, this@SilentSwap).submit().done) {
+            if (!hotbarRequest(0, nowOrNothing = true).submit().done) {
                 it.cancel()
                 return@listen
             }
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/StateInfo.kt b/src/main/kotlin/com/lambda/module/modules/debug/StateInfo.kt
index 1cf5a0a47..97d40e693 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/StateInfo.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/StateInfo.kt
@@ -17,9 +17,8 @@
 
 package com.lambda.module.modules.debug
 
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.KeyCode
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/TimerTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/TimerTest.kt
index 25073151a..10b6abe2c 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/TimerTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/TimerTest.kt
@@ -21,7 +21,7 @@ import com.lambda.event.events.ClientEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.SafeListener.Companion.listenConcurrently
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/hud/AccountName.kt b/src/main/kotlin/com/lambda/module/modules/hud/AccountName.kt
similarity index 93%
rename from src/main/kotlin/com/lambda/module/hud/AccountName.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/AccountName.kt
index 7b81bdaa3..859694b81 100644
--- a/src/main/kotlin/com/lambda/module/hud/AccountName.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/AccountName.kt
@@ -15,11 +15,11 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/hud/Baritone.kt b/src/main/kotlin/com/lambda/module/modules/hud/Baritone.kt
similarity index 91%
rename from src/main/kotlin/com/lambda/module/hud/Baritone.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Baritone.kt
index 601602b71..7d3ce6e16 100644
--- a/src/main/kotlin/com/lambda/module/hud/Baritone.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Baritone.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.interaction.construction.simulation.BuildGoal
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object Baritone : HudModule(
diff --git a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt b/src/main/kotlin/com/lambda/module/modules/hud/Coordinates.kt
similarity index 97%
rename from src/main/kotlin/com/lambda/module/hud/Coordinates.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Coordinates.kt
index a09dae66f..e3c169e51 100644
--- a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Coordinates.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.config.Tab
 import com.lambda.config.blocks.FormatterSettings
@@ -23,7 +23,7 @@ import com.lambda.config.editSetting
 import com.lambda.config.withEdits
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.FormattingUtils.format
 import com.lambda.util.extension.dimensionName
diff --git a/src/main/kotlin/com/lambda/module/hud/Fps.kt b/src/main/kotlin/com/lambda/module/modules/hud/Fps.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/module/hud/Fps.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Fps.kt
index 917238a02..87f425840 100644
--- a/src/main/kotlin/com/lambda/module/hud/Fps.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Fps.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.event.events.RenderEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.collections.LimitedDecayQueue
 import kotlin.time.Duration.Companion.seconds
 
diff --git a/src/main/kotlin/com/lambda/module/hud/ModuleList.kt b/src/main/kotlin/com/lambda/module/modules/hud/ModuleList.kt
similarity index 95%
rename from src/main/kotlin/com/lambda/module/hud/ModuleList.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/ModuleList.kt
index cd438fbc2..ef45c76b3 100644
--- a/src/main/kotlin/com/lambda/module/hud/ModuleList.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/ModuleList.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.imgui.flag.ImGuiCol
 import com.lambda.module.HudModule
 import com.lambda.module.ModuleRegistry
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import java.awt.Color
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/hud/Rotation.kt b/src/main/kotlin/com/lambda/module/modules/hud/Rotation.kt
similarity index 94%
rename from src/main/kotlin/com/lambda/module/hud/Rotation.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Rotation.kt
index b9246cd0b..767a31207 100644
--- a/src/main/kotlin/com/lambda/module/hud/Rotation.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Rotation.kt
@@ -15,14 +15,14 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.config.blocks.FormatterSettings
 import com.lambda.config.editSetting
 import com.lambda.config.withEdits
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.FormattingUtils.format
 
diff --git a/src/main/kotlin/com/lambda/module/hud/Speedometer.kt b/src/main/kotlin/com/lambda/module/modules/hud/Speedometer.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/module/hud/Speedometer.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Speedometer.kt
index 5ef49be74..2030ba102 100644
--- a/src/main/kotlin/com/lambda/module/hud/Speedometer.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Speedometer.kt
@@ -15,14 +15,14 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.SpeedUnit
 import net.minecraft.util.math.Vec3d
 
diff --git a/src/main/kotlin/com/lambda/module/hud/TaskFlowHud.kt b/src/main/kotlin/com/lambda/module/modules/hud/TaskFlowHud.kt
similarity index 84%
rename from src/main/kotlin/com/lambda/module/hud/TaskFlowHud.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/TaskFlowHud.kt
index eaa6aadf4..31de0284d 100644
--- a/src/main/kotlin/com/lambda/module/hud/TaskFlowHud.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/TaskFlowHud.kt
@@ -15,11 +15,12 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
+import com.lambda.module.modules.client.Client
 import com.lambda.task.RootTask
 
 @Suppress("unused")
@@ -28,6 +29,6 @@ object TaskFlowHud : HudModule(
     tag = ModuleTag.HUD,
 ) {
     override fun ImGuiBuilder.buildLayout() {
-        text(RootTask.toString())
+        text(RootTask.taskTree(Client.showInvisibleTasks))
     }
 }
diff --git a/src/main/kotlin/com/lambda/module/hud/Tps.kt b/src/main/kotlin/com/lambda/module/modules/hud/Tps.kt
similarity index 96%
rename from src/main/kotlin/com/lambda/module/hud/Tps.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Tps.kt
index 3c94e178e..2e9104e06 100644
--- a/src/main/kotlin/com/lambda/module/hud/Tps.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Tps.kt
@@ -15,12 +15,12 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.imgui.ImVec2
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.FormattingUtils.format
 import com.lambda.util.ServerTPSUtils
 import com.lambda.util.ServerTPSUtils.recentData
diff --git a/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/src/main/kotlin/com/lambda/module/modules/hud/Watermark.kt
similarity index 94%
rename from src/main/kotlin/com/lambda/module/hud/Watermark.kt
rename to src/main/kotlin/com/lambda/module/modules/hud/Watermark.kt
index fa46da254..c932d1428 100644
--- a/src/main/kotlin/com/lambda/module/hud/Watermark.kt
+++ b/src/main/kotlin/com/lambda/module/modules/hud/Watermark.kt
@@ -15,13 +15,13 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.module.hud
+package com.lambda.module.modules.hud
 
 import com.lambda.graphics.texture.TextureOwner.upload
 import com.lambda.gui.dsl.ImGuiBuilder
 import com.lambda.imgui.ImGui
 import com.lambda.module.HudModule
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object Watermark : HudModule(
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/AutoSpiral.kt b/src/main/kotlin/com/lambda/module/modules/movement/AutoSpiral.kt
index ba2af3e0f..e417c56ca 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/AutoSpiral.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/AutoSpiral.kt
@@ -21,10 +21,10 @@ import baritone.api.pathing.goals.GoalXZ
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.BlockPosIterators
 import com.lambda.util.extension.isNether
 import com.lambda.util.player.RotationUtils.lookAt
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/AutoWalk.kt b/src/main/kotlin/com/lambda/module/modules/movement/AutoWalk.kt
index 4ab1568a8..c45664a9a 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/AutoWalk.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/AutoWalk.kt
@@ -22,10 +22,10 @@ import com.lambda.context.SafeContext
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.breaking.BreakManager
-import com.lambda.interaction.managers.interacting.InteractManager
+import com.lambda.interaction.manager.managers.breaking.BreakManager
+import com.lambda.interaction.manager.managers.interacting.InteractManager
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.math.MathUtils.toDouble
 import com.lambda.util.player.MovementUtils.jumping
 import com.lambda.util.player.MovementUtils.roundedForward
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
index 281766a82..abcfe1088 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt
@@ -27,8 +27,8 @@ import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRend
 import com.lambda.graphics.util.DynamicAABB
 import com.lambda.gui.components.ClickGuiLayout
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.KillAura
-import com.lambda.module.tag.ModuleTag
 import com.lambda.util.ClientPacket
 import com.lambda.util.PacketUtils.handlePacketSilently
 import com.lambda.util.PacketUtils.sendPacketSilently
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
index fe29c57b6..9d0a60c07 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
@@ -19,30 +19,33 @@ package com.lambda.module.modules.movement
 
 import com.lambda.Lambda
 import com.lambda.Lambda.mc
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.hideAllExcept
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.GlideHandler
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.StackSelection.Companion.select
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.selectContainers
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.stacksAvailable
+import com.lambda.interaction.handler.handlers.GlideHandler
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.warn
 import com.lambda.util.KeyCode
 import com.lambda.util.Mouse
 import com.lambda.util.player.PlayerUtils
 import com.lambda.util.player.PlayerUtils.canStartGliding
-import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
-import com.lambda.util.player.SlotUtils.hotbarStacks
 import net.minecraft.client.network.ClientPlayerEntity
 import net.minecraft.item.Items
 import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket
@@ -59,15 +62,16 @@ object BetterFirework : Module(
 		.onPress {
 			if (mc.crosshairTarget?.type == HitResult.Type.BLOCK &&
 				!middleClickCancel &&
-				activateButton.mouse == Lambda.mc.options.pickItemKey.boundKey.code) return@onPress
+				activateButton.mouse == Lambda.mc.options.pickItemKey.boundKey.code
+			) return@onPress
 
-			if (!player.hasFireworks) {
+			if (!hasFireworks) {
 				warn("You need to have fireworks in your inventory to use this module!")
 				return@onPress
 			}
 			// Prevent using multiple times
 			if (takeoffState != TakeoffState.Idle) return@onPress
-			// If already gliding use another firework
+			// If already gliding, use another firework
 			if (player.canStartGliding || player.isGliding) takeoffState = TakeoffState.StartFlying
 			else if (player.canTakeoff) takeoffState = TakeoffState.Jumping
 		}
@@ -83,10 +87,11 @@ object BetterFirework : Module(
 
 	private var takeoffState = TakeoffState.Idle
 
-	val ClientPlayerEntity.hasFireworks: Boolean
-		get() = Items.FIREWORK_ROCKET.select()
-			.filterStacks(hotbarAndInventoryStacks)
-			.isNotEmpty() || offHandStack.item == Items.FIREWORK_ROCKET
+	val hasFireworks: Boolean
+		get() = stacksAvailable(
+			Items.FIREWORK_ROCKET.select(),
+			selectContainers(HotbarContainer, InventoryContainer, OffhandContainer)
+		)
 
 	context(_: SafeContext)
 	private val ClientPlayerEntity.canTakeoff: Boolean
@@ -156,39 +161,41 @@ object BetterFirework : Module(
 	 * Return true if a firework has been used
 	 */
 	fun SafeContext.startFirework(inventory: Boolean) {
-		val stack = selectStack(count = 1) { isItem(Items.FIREWORK_ROCKET) }
-
-		stack.bestItemMatch(player.hotbarStacks)
-			?.let {
-				val request = HotbarRequest(player.hotbarStacks.indexOf(it), this@BetterFirework, keepTicks = 0)
-					.submit(queueIfMismatchedStage = false)
-				if (request.done) {
-					interaction.interactItem(player, Hand.MAIN_HAND)
-					sendSwing()
-				}
-				return
-			}
+		val selection = Items.FIREWORK_ROCKET.select(1)
+
+		val hotbarMatch = findSlot(selection, HotbarContainer.select(), sorted = false)
+		if (hotbarMatch != null) {
+			swapAndFirework(hotbarMatch.index)
+			return
+		}
 
 		if (!inventory) return
 
-		stack.bestItemMatch(player.hotbarAndInventoryStacks)
-			?.let {
-				val swapSlotId = player.hotbarAndInventoryStacks.indexOf(it)
-				val hotbarSlotToSwapWith = player.hotbarStacks.find { slot -> slot.isEmpty }?.let { slot -> player.hotbarStacks.indexOf(slot) } ?: 8
-
-				inventoryRequest {
-					swap(swapSlotId, hotbarSlotToSwapWith)
-					action {
-						val request = HotbarRequest(hotbarSlotToSwapWith, this@BetterFirework, keepTicks = 0, nowOrNothing = true)
-							.submit(queueIfMismatchedStage = false)
-						if (request.done) {
-							interaction.interactItem(player, Hand.MAIN_HAND)
-							sendSwing()
-						}
-					}
-					swap(swapSlotId, hotbarSlotToSwapWith)
-				}.submit()
-			}
+		val inventoryMatch = findSlot(selection, InventoryContainer.select(), sorted = false) ?: return
+		val hotbarSlot =
+			findSlot(
+				StackSelection.ALL_EMPTY,
+				HotbarContainer.select(),
+				sorted = false
+			)?.index
+				?: 8
+
+		inventoryRequest {
+			swapWithHotbar(inventoryMatch.id, hotbarSlot)
+			action { swapAndFirework(hotbarSlot) }
+			swapWithHotbar(inventoryMatch.id, hotbarSlot)
+		}.submit()
+	}
+
+	private fun SafeContext.swapAndFirework(hotbarSlot: Int) {
+		val request =
+			hotbarRequest(hotbarSlot, true) {
+				keepTicks(0)
+			}.submit(false)
+		if (request.done) {
+			interaction.interactItem(player, Hand.MAIN_HAND)
+			sendSwing()
+		}
 	}
 
 	enum class TakeoffState {
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
index 444006b51..eca5c391e 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt
@@ -26,8 +26,8 @@ import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.graphics.util.DynamicAABB
 import com.lambda.gui.components.ClickGuiLayout
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.KillAura
-import com.lambda.module.tag.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.PacketUtils.handlePacketSilently
 import com.lambda.util.PacketUtils.sendPacketSilently
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/ElytraAltitudeControl.kt b/src/main/kotlin/com/lambda/module/modules/movement/ElytraAltitudeControl.kt
index 7116ba655..78bfb41f8 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/ElytraAltitudeControl.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/ElytraAltitudeControl.kt
@@ -18,15 +18,15 @@
 package com.lambda.module.modules.movement
 
 import com.lambda.config.Tab
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.movement.BetterFirework.startFirework
-import com.lambda.module.tag.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.SpeedUnit
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/EntityControl.kt b/src/main/kotlin/com/lambda/module/modules/movement/EntityControl.kt
index 03bff2f1d..240b35840 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/EntityControl.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/EntityControl.kt
@@ -21,7 +21,7 @@ import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.world.fastEntitySearch
 import net.minecraft.entity.passive.AbstractHorseEntity
 import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Jesus.kt b/src/main/kotlin/com/lambda/module/modules/movement/Jesus.kt
index 7f024e4bb..059ce26e3 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Jesus.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Jesus.kt
@@ -24,7 +24,7 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.events.WorldEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.NamedEnum
 import com.lambda.util.extension.isElytraFlying
 import com.lambda.util.math.MathUtils.toInt
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/NoFall.kt b/src/main/kotlin/com/lambda/module/modules/movement/NoFall.kt
index 5275eb08a..82b2f4bea 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/NoFall.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/NoFall.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.movement
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.PacketUtils.sendPacket
 import com.lambda.util.math.component1
 import com.lambda.util.math.component2
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/NoJumpCooldown.kt b/src/main/kotlin/com/lambda/module/modules/movement/NoJumpCooldown.kt
index 1746e1e95..4e0ddef0d 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/NoJumpCooldown.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/NoJumpCooldown.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.movement
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object NoJumpCooldown : Module(
 	name = "NoJumpCooldown",
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/SafeWalk.kt b/src/main/kotlin/com/lambda/module/modules/movement/SafeWalk.kt
index e34f6db0f..87dad538c 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/SafeWalk.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/SafeWalk.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.movement
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.player.MovementUtils.motionX
 import com.lambda.util.player.MovementUtils.motionZ
 import com.lambda.util.player.MovementUtils.sneaking
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt b/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt
index 58283a22b..da02e15a4 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt
@@ -17,8 +17,8 @@
 
 package com.lambda.module.modules.movement
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.automation.setDefaultAutomationConfig
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
@@ -26,9 +26,9 @@ import com.lambda.event.events.ClientEvent
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.NamedEnum
 import com.lambda.util.extension.contains
 import com.lambda.util.extension.isElytraFlying
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Sprint.kt b/src/main/kotlin/com/lambda/module/modules/movement/Sprint.kt
index d31ac27cc..7dfe55128 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Sprint.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Sprint.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.movement
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object Sprint : Module(
     name = "Sprint",
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt b/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt
index 4436fabd8..e62130b09 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt
@@ -26,10 +26,10 @@ import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.KillAura
-import com.lambda.module.tag.ModuleTag
 import net.minecraft.entity.effect.StatusEffects
 import net.minecraft.util.math.Vec3d
 import java.awt.Color
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt b/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt
index 98cf8c9fa..c89e52c07 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt
@@ -23,8 +23,8 @@ import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.KillAura
-import com.lambda.module.tag.ModuleTag
 import com.lambda.threading.runConcurrent
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.PacketUtils.handlePacketSilently
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Timer.kt b/src/main/kotlin/com/lambda/module/modules/movement/Timer.kt
index 359f78bde..2765c5fbe 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Timer.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Timer.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.movement
 import com.lambda.event.events.ClientEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object Timer : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Velocity.kt b/src/main/kotlin/com/lambda/module/modules/movement/Velocity.kt
index cd2470c54..76e27baa0 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/Velocity.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/Velocity.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.movement
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket
 
 object Velocity : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFly.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFly.kt
index 4ea420a64..4f1389c3e 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFly.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFly.kt
@@ -18,9 +18,9 @@
 package com.lambda.module.modules.movement.elytrafly
 
 import com.lambda.config.Tab
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
@@ -29,12 +29,12 @@ import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.movement.elytrafly.modes.BounceElytraFly
 import com.lambda.module.modules.movement.elytrafly.modes.GeneralElytraFly
 import com.lambda.module.modules.movement.elytrafly.modes.GrimControlElytraFly
-import com.lambda.module.tag.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.NamedEnum
 import com.lambda.util.Timer
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFlyMode.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFlyMode.kt
index 804876f1d..e178d4e71 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFlyMode.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ElytraFlyMode.kt
@@ -24,17 +24,19 @@ import com.lambda.event.Muteable
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.GlideHandler.ELYTRA_SELECTION
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.inventory.InventoryManager
-import com.lambda.interaction.managers.inventory.InventoryRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.container.containers.ArmorContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
+import com.lambda.interaction.handler.handlers.GlideHandler.ELYTRA_SELECTION
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.inventory.InvRequestBuilder
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.FlyMode
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.fakeFly
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.logError
-import com.lambda.util.player.SlotUtils.armorSlots
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
 import net.minecraft.entity.Entity
 import net.minecraft.entity.EquipmentSlot
 import net.minecraft.item.Items
@@ -86,45 +88,48 @@ abstract class ElytraFlyMode(
 			}
 		}
 
-		val elytraSlot = findElytra() ?: run {
-			logError("Fake Fly requires an elytra in your inventory, preferably in your hotbar.")
-			ElytraFly.disable()
-			return false
-		}
+		val elytraSlot = findElytra()
+			?: run {
+				logError("Fake Fly requires an elytra in your inventory, preferably in your hotbar")
+				ElytraFly.disable()
+				return false
+			}
 		val elytraInHotbar = elytraSlot.index in 0..8
 
-		val chestSlot = player.armorSlots.getOrNull(1) ?: return false
+		val chestSlot = findSlots(containerSelection = ArmorContainer.select())
+			.orEmpty()
+			.toList()
+			.getOrNull(1)
+			?: return false
 
 		if (elytraInHotbar) {
-			val hotbarRequest = HotbarRequest(
-				elytraSlot.index,
-				ElytraFly,
-				keepTicks = 0,
-				nowOrNothing = true
-			).submit()
+			val hotbarRequest =
+				hotbarRequest(elytraSlot.index, nowOrNothing = true) {
+					keepTicks(0)
+				}.submit()
 			if (!hotbarRequest.done) return false
 		}
-		fun InventoryRequest.InvRequestBuilder.swapChest() {
+
+		fun InvRequestBuilder.swapChest() {
 			if (elytraInHotbar) {
-				interaction.interactItem(player, Hand.MAIN_HAND)
-				InventoryManager.indexInventoryChanges()
+				action { interaction.interactItem(player, Hand.MAIN_HAND) }
 			} else {
 				moveSlot(elytraSlot.id, chestSlot.id)
 				if (!chestSlot.stack.isEmpty) pickup(elytraSlot.id)
 			}
 		}
 
-		val inventoryRequest = inventoryRequest {
-			swapChest()
-			action { startFly() }
-			swapChest()
-		}.submit(false)
+		val inventoryRequest =
+			inventoryRequest {
+				swapChest()
+				action { startFly() }
+				swapChest()
+			}.submit(false)
 
 		return inventoryRequest.done
 	}
 
-	fun SafeContext.findElytra(): Slot? =
-		ELYTRA_SELECTION.filterSlots(player.hotbarAndInventorySlots).minByOrNull { it.index }
+	fun findElytra(): Slot? = findSlot(ELYTRA_SELECTION, ContainerSelection.HOTBAR_AND_INVENTORY)
 
 	protected fun SafeContext.startFly() {
 		player.setFlag(Entity.GLIDING_FLAG_INDEX, true)
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/GrimFireworkBox.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/GrimFireworkBox.kt
index dd3344672..c70ead991 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/GrimFireworkBox.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/GrimFireworkBox.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.movement.elytrafly
 
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import net.minecraft.util.math.MathHelper
 import net.minecraft.util.math.Vec3d
 import kotlin.math.asin
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ObstaclePassingMode.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ObstaclePassingMode.kt
index 36e1a7b1e..36f5cfe17 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ObstaclePassingMode.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/ObstaclePassingMode.kt
@@ -21,16 +21,16 @@ import baritone.api.pathing.goals.GoalGetToBlock
 import com.lambda.config.Config
 import com.lambda.config.ConfigBlock
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.module.hud.Speedometer
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.module.modules.hud.Speedometer
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.FlyMode
 import com.lambda.threading.runGameScheduled
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.CommunicationUtils.logError
 import com.lambda.util.SpeedUnit
+import com.lambda.util.math.blockPos
 import com.lambda.util.math.dist
 import com.lambda.util.math.distSq
-import com.lambda.util.math.flooredBlockPos
 import com.lambda.util.math.isLoaded
 import com.lambda.util.world.raycast.InteractionMask
 import com.lambda.util.world.raycast.RayCastUtils.blockResult
@@ -90,7 +90,7 @@ abstract class ObstaclePassingMode(
 
 		passingToPos?.let { passingTo ->
 			if (passingTo distSq startPos < playerPos distSq startPos) {
-				val atClosestPointBlockPos = closestLinePoint.flooredBlockPos == player.blockPos
+				val atClosestPointBlockPos = closestLinePoint.blockPos == player.blockPos
 				if (!atClosestPointBlockPos) pathToValidPoint(closestLinePoint, snappedDir, false)
 				else {
 					BaritoneHandler.cancel()
@@ -172,7 +172,7 @@ abstract class ObstaclePassingMode(
 
 	private fun passTo(pos: Vec3d) {
 		passingToPos = pos
-		BaritoneHandler.setGoalAndPath(GoalGetToBlock(pos.flooredBlockPos))
+		BaritoneHandler.setGoalAndPath(GoalGetToBlock(pos.blockPos))
 	}
 
 	protected fun Vec3d.findClosestPointOnLine(snappedDirection: Vec3d): Vec3d {
@@ -185,7 +185,7 @@ abstract class ObstaclePassingMode(
 	private fun Vec3d.isObstructed(direction: Vec3d) =
 		if (!isLoaded) false
 		else {
-			flooredBlockPos.down().let { downPos ->
+			blockPos.down().let { downPos ->
 				!safeContext.blockState(downPos).isSolidBlock(safeContext.world, downPos)
 			} ||
 					if (add(0.0, passerConfig.minObstacleHeight, 0.0).rayCastObstructed(direction)) true
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/BounceElytraFly.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/BounceElytraFly.kt
index e69aad8a4..178bd19cb 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/BounceElytraFly.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/BounceElytraFly.kt
@@ -19,17 +19,17 @@ package com.lambda.module.modules.movement.elytrafly.modes
 
 import com.lambda.config.Config
 import com.lambda.config.Group
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.context.SafeContext
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.handlers.GlideHandler
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.module.hud.Speedometer
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.GlideHandler
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
+import com.lambda.module.modules.hud.Speedometer
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.FlyMode
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.fakeFly
 import com.lambda.module.modules.movement.elytrafly.ObstaclePassingMode
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/GrimControlElytraFly.kt b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/GrimControlElytraFly.kt
index 095762b84..368ec50fc 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/GrimControlElytraFly.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/elytrafly/modes/GrimControlElytraFly.kt
@@ -18,14 +18,16 @@
 package com.lambda.module.modules.movement.elytrafly.modes
 
 import com.lambda.config.Config
-import com.lambda.context.SafeContext
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.PlayerPacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.material.StackSelection.Companion.select
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.modules.movement.BetterFirework.startFirework
 import com.lambda.module.modules.movement.elytrafly.ElytraFly
 import com.lambda.module.modules.movement.elytrafly.ElytraFly.FlyMode
@@ -36,8 +38,6 @@ import com.lambda.module.modules.render.Freecam
 import com.lambda.util.NamedEnum
 import com.lambda.util.TickTimer
 import com.lambda.util.math.MathUtils.toFloat
-import com.lambda.util.player.SlotUtils.hotbarStacks
-import com.lambda.util.player.SlotUtils.inventoryStacks
 import net.minecraft.item.ItemStack
 import net.minecraft.item.Items
 import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket
@@ -141,11 +141,22 @@ class GrimControlElytraFly(
 			val rot = RotationManager.activeRotation
 			val flipFlop = rotFlipFlop.toFloat() * 0.0001f
 			rotFlipFlop = !rotFlipFlop
-			event.packet = PlayerMoveC2SPacket.Full(player.pos, rot.yawF + flipFlop, rot.pitchF, player.isOnGround, player.horizontalCollision)
+			event.packet =
+				PlayerMoveC2SPacket.Full(
+					player.pos,
+					rot.yawF + flipFlop,
+					rot.pitchF,
+					player.isOnGround,
+					player.horizontalCollision
+				)
 		}
 
 		listen { event ->
-			if (event.packet !is PlayerMoveC2SPacket || !player.isGliding || flipFlopMode.isFlipFlopping(hasFirework) || moving) return@listen
+			if (event.packet !is PlayerMoveC2SPacket ||
+				!player.isGliding ||
+				flipFlopMode.isFlipFlopping(hasFirework) ||
+				moving
+			) return@listen
 			if (stillTickTimer.hasSurpassed(packetGap)) {
 				stillTickTimer.reset()
 				return@listen
@@ -154,9 +165,12 @@ class GrimControlElytraFly(
 		}
 	}
 
-	private fun SafeContext.findFirework(): ItemStack? {
-		val stack = Items.FIREWORK_ROCKET.select()
-		return stack.bestItemMatch(player.hotbarStacks) ?: if (inventory) stack.bestItemMatch(player.inventoryStacks) else null
+	private fun findFirework(): ItemStack? {
+		val selection = Items.FIREWORK_ROCKET.select()
+		return findStack(selection, HotbarContainer.select(), false)
+			?: if (inventory) {
+				findStack(selection, InventoryContainer.select(), false)
+			} else null
 	}
 
 	override fun pausingMovement() = still
diff --git a/src/main/kotlin/com/lambda/module/modules/network/AutoReconnect.kt b/src/main/kotlin/com/lambda/module/modules/network/AutoReconnect.kt
index c4e208f43..3b901d1d7 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/AutoReconnect.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/AutoReconnect.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.network
 
 import com.lambda.Lambda.mc
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.client.gui.screen.Screen
 import net.minecraft.client.gui.screen.multiplayer.ConnectScreen
 import net.minecraft.client.gui.screen.world.SelectWorldScreen
diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
index c6774f7a2..c150e5484 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt
@@ -22,7 +22,7 @@ import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runConcurrent
 import com.lambda.threading.runGameScheduled
 import com.lambda.threading.runSafe
diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt
index f1b0aa7c2..a62fd1f71 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt
@@ -17,11 +17,11 @@
 
 package com.lambda.module.modules.network
 
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.ReflectionUtils.className
 import com.lambda.util.collections.LimitedDecayQueue
diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt
index efcf05299..3660e73d5 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt
@@ -24,7 +24,7 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.UnsafeListener.Companion.listenConcurrentlyUnsafe
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runIO
 import com.lambda.util.CommunicationUtils
 import com.lambda.util.CommunicationUtils.info
diff --git a/src/main/kotlin/com/lambda/module/modules/network/Rubberband.kt b/src/main/kotlin/com/lambda/module/modules/network/Rubberband.kt
index 45fb7f1b7..148fb311c 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/Rubberband.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/Rubberband.kt
@@ -21,7 +21,7 @@ import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.PlayerPacketEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.warn
 import com.lambda.util.collections.LimitedOrderedSet
 import com.lambda.util.math.dist
diff --git a/src/main/kotlin/com/lambda/module/modules/network/ServerSpoof.kt b/src/main/kotlin/com/lambda/module/modules/network/ServerSpoof.kt
index 39dbcfdd1..d034bca9c 100644
--- a/src/main/kotlin/com/lambda/module/modules/network/ServerSpoof.kt
+++ b/src/main/kotlin/com/lambda/module/modules/network/ServerSpoof.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.network
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.text.ClickEvents
 import com.lambda.util.text.buildText
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AntiAFK.kt b/src/main/kotlin/com/lambda/module/modules/player/AntiAFK.kt
index 25f485e17..4091c92b2 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AntiAFK.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AntiAFK.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.player
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.util.Hand
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt b/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
index 77ba3ca05..5b6cfca06 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
@@ -17,14 +17,14 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.math.distSq
 import net.minecraft.entity.Entity
 import net.minecraft.entity.player.PlayerEntity
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoEat.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoEat.kt
index e8ea02ee0..a6da8b71d 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AutoEat.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AutoEat.kt
@@ -17,18 +17,18 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.EatConfig.Companion.reasonEating
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.EatTask.Companion.eat
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.start
+import com.lambda.task.tasks.eat
+import com.lambda.task.tasks.wrappers.thenAction
 import com.lambda.threading.runSafeAutomated
 
 @Suppress("unused")
@@ -49,7 +49,9 @@ object AutoEat : Module(
             val reason = runSafeAutomated { reasonEating() }
             if (eatTask != null || !reason.shouldEat()) return@listen
 
-            eatTask = eat().thenAction { eatTask = null }.run()
+            eatTask = eat()
+                .thenAction { eatTask = null }
+                .start()
         }
 
         onDisable {
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoElytraSwap.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoElytraSwap.kt
index 091341b9e..8e7bb76ab 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AutoElytraSwap.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AutoElytraSwap.kt
@@ -17,19 +17,27 @@
 
 package com.lambda.module.modules.player
 
+import com.lambda.config.automation.setDefaultAutomationConfig
+import com.lambda.config.hideAllExcept
+import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.GlideHandler.CHESTPLATE_SELECTION
-import com.lambda.interaction.handlers.GlideHandler.ELYTRA_SELECTION
-import com.lambda.interaction.handlers.GlideHandler.manuallySwapped
-import com.lambda.interaction.handlers.GlideHandler.swapped
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.container.containers.ArmorContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
+import com.lambda.interaction.handler.handlers.GlideHandler.CHESTPLATE_SELECTION
+import com.lambda.interaction.handler.handlers.GlideHandler.ELYTRA_SELECTION
+import com.lambda.interaction.handler.handlers.GlideHandler.manuallySwapped
+import com.lambda.interaction.handler.handlers.GlideHandler.swapped
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.combat.AutoArmor
-import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafeAutomated
 import com.lambda.util.CommunicationUtils.warn
 import com.lambda.util.player.PlayerUtils.canGlideWithChestPiece
-import com.lambda.util.player.SlotUtils.armorSlots
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
 import net.minecraft.screen.slot.Slot
 
 object AutoElytraSwap : Module(
@@ -41,32 +49,38 @@ object AutoElytraSwap : Module(
 	val glideDelay by setting("Glide Delay", 0, 0..20, 1, "The delay, in ticks, between swapping to elytra, and starting to glide", unit = " ticks")
 
 	init {
+		setDefaultAutomationConfig()
+			.withEdits {
+				hideAllExcept(buildConfig::inventoryLimit)
+			}
+
 		onDisable { restore() }
 	}
 
-	context(safeContext: SafeContext)
 	fun manualSwap(elytra: Boolean): Boolean {
 		val swapSlot =
-			safeContext.player.hotbarAndInventorySlots.let { slots ->
-				if (elytra) ELYTRA_SELECTION.filterSlots(slots)
-				else CHESTPLATE_SELECTION.filterSlots(slots)
-			}.minByOrNull { it.index } ?: run {
+			findSlot(
+				if (elytra) ELYTRA_SELECTION
+				else CHESTPLATE_SELECTION,
+				ContainerSelection.HOTBAR_AND_INVENTORY
+			) ?: run {
 				AutoElytraSwap.warn("The required armor piece was not found for AutoElytraSwap to work.")
 				return false
 			}
 
-		return safeContext.swapWithChestplate(swapSlot)
+		return swapWithChestplate(swapSlot)
 	}
 
-	private fun SafeContext.swapWithChestplate(swapSlot: Slot): Boolean {
-		val chestplateSlot = player.armorSlots[1]
-		return AutoElytraSwap.inventoryRequest {
-			if (swapSlot.index in 0..8) swap(chestplateSlot.id, swapSlot.index)
-			else {
-				moveSlot(swapSlot.id, chestplateSlot.id)
-				if (!chestplateSlot.stack.isEmpty) pickup(swapSlot.id)
-			}
-		}.submit(false).done
+	private fun swapWithChestplate(swapSlot: Slot): Boolean {
+		val chestplateSlot = findSlots(containerSelection = ArmorContainer.select(), sorted = false)
+			.orEmpty()
+			.toList()
+			.getOrNull(1)
+			?: return false
+		val sourceContainer = if (swapSlot.index in 0..8) HotbarContainer else InventoryContainer
+		return runSafeAutomated {
+			ArmorContainer.swap(chestplateSlot, swapSlot, sourceContainer)
+		} ?: false
 	}
 
 	context(safeContext: SafeContext)
diff --git a/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt b/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt
index 5bbfb4bf3..e264d9983 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt
@@ -18,14 +18,13 @@
 package com.lambda.module.modules.player
 
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.FriendHandler
-import com.lambda.interaction.handlers.FriendHandler.befriend
-import com.lambda.interaction.handlers.FriendHandler.isFriend
-import com.lambda.interaction.handlers.FriendHandler.unfriend
+import com.lambda.interaction.handler.handlers.FriendHandler
+import com.lambda.interaction.handler.handlers.FriendHandler.befriend
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
+import com.lambda.interaction.handler.handlers.FriendHandler.unfriend
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.InputUtils.isSatisfied
 import com.lambda.util.world.raycast.RayCastUtils.entityResult
diff --git a/src/main/kotlin/com/lambda/module/modules/player/EndermanLook.kt b/src/main/kotlin/com/lambda/module/modules/player/EndermanLook.kt
index b7cdcee1d..7507a3f31 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/EndermanLook.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/EndermanLook.kt
@@ -17,16 +17,16 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.math.distSq
 import net.minecraft.entity.EquipmentSlot
 import net.minecraft.entity.mob.EndermanEntity
diff --git a/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt
index 82e5425e7..76a5204fc 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.editTypedSettings
 import com.lambda.config.hide
@@ -27,9 +27,9 @@ import com.lambda.event.events.PlayerEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.simulation.context.BuildContext
-import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
+import com.lambda.interaction.manager.managers.breaking.breakRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafeAutomated
 import java.util.concurrent.ConcurrentLinkedQueue
 
diff --git a/src/main/kotlin/com/lambda/module/modules/player/Interact.kt b/src/main/kotlin/com/lambda/module/modules/player/Interact.kt
index 48fec9a24..6ae205a89 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/Interact.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/Interact.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.player
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object Interact : Module(
     name = "Interact",
diff --git a/src/main/kotlin/com/lambda/module/modules/player/InventoryMove.kt b/src/main/kotlin/com/lambda/module/modules/player/InventoryMove.kt
index 0fdbccd43..d6aed9e9d 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/InventoryMove.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/InventoryMove.kt
@@ -22,10 +22,10 @@ import com.lambda.config.blocks.RotationConfig
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.gui.LambdaScreen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.InputUtils
 import com.lambda.util.InputUtils.isKeyPressed
 import com.lambda.util.math.MathUtils.toFloatSign
diff --git a/src/main/kotlin/com/lambda/module/modules/player/InventoryResync.kt b/src/main/kotlin/com/lambda/module/modules/player/InventoryResync.kt
index c432a001d..8c2491110 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/InventoryResync.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/InventoryResync.kt
@@ -19,9 +19,9 @@ package com.lambda.module.modules.player
 
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.TickTimer
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt b/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt
index b38be09d0..9dadac63c 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt
@@ -17,27 +17,32 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.InventoryEvent
 import com.lambda.event.events.PlayerEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.container.ContainerType
+import com.lambda.interaction.container.NestedContainer
+import com.lambda.interaction.container.OpenedContainerContext
+import com.lambda.interaction.container.containers.external.EnderChestContainer
+import com.lambda.interaction.container.selection.containerSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findContainer
+import com.lambda.interaction.handler.handlers.ContainerSearchScope
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock
-import com.lambda.task.tasks.OpenContainerTask
-import com.lambda.task.tasks.PlaceContainerTask
-import com.lambda.task.wrappers.then
-import com.lambda.task.wrappers.thenAction
-import com.lambda.util.item.ItemUtils.shulkerBoxes
+import com.lambda.task.start
+import com.lambda.task.tasks.wrappers.then
+import com.lambda.util.CommunicationUtils.warn
+import com.lambda.util.item.ItemUtils.SHULKER_BOXES
 import net.minecraft.item.Items
 import net.minecraft.screen.ScreenHandler
 import net.minecraft.screen.slot.SlotActionType
-import net.minecraft.util.math.BlockPos
+import java.util.*
 
+@Suppress("unused")
 object InventoryTweaks : Module(
     name = "InventoryTweaks",
     tag = ModuleTag.PLAYER,
@@ -45,10 +50,10 @@ object InventoryTweaks : Module(
     private val instantShulker by setting("Instant Shulker", true, description = "Right-click shulker boxes in your inventory to instantly place them and open them.")
     private val instantEChest by setting("Instant Ender-Chest", true, description = "Right-click ender chests in your inventory to instantly place them and open them.")
 
-    private var placedPos: BlockPos? = null
-    private var placeAndOpen: Task<*>? = null
-    private var lastBreak: Task<*>? = null
-    private var lastOpenScreen: ScreenHandler? = null
+    private val openContexts = LinkedList()
+    var lastOpenScreen: ScreenHandler? = null
+    private var isClosing = false
+    private var task: Task<*>? = null
 
     init {
         setDefaultAutomationConfig()
@@ -56,32 +61,69 @@ object InventoryTweaks : Module(
                 hideAllExcept(::breakConfig, ::interactConfig, ::inventoryConfig, ::hotbarConfig)
             }
 
-        listen {
-            if (it.action != SlotActionType.PICKUP || it.button != 1) return@listen
-            val slot = it.screenHandler.getSlot(it.slot)
-            if (!(instantShulker && slot.stack.item in shulkerBoxes) && !(instantEChest && slot.stack.item == Items.ENDER_CHEST)) return@listen
-            it.cancel()
-            lastOpenScreen = null
-            placeAndOpen = PlaceContainerTask(slot, this@InventoryTweaks).then { placePos ->
-                placedPos = placePos
-                OpenContainerTask(placePos, this@InventoryTweaks).thenAction { screenHandler ->
-                    lastOpenScreen = screenHandler
+        listen { event ->
+            if (event.action != SlotActionType.PICKUP || event.button != 1) return@listen
+            val slot = event.screenHandler.getSlot(event.slot) ?: return@listen
+            val stack = slot.stack
+
+            when (stack.item) {
+                in SHULKER_BOXES -> if (!instantShulker) return@listen
+                Items.ENDER_CHEST -> if (!instantEChest) return@listen
+                else -> return@listen
+            }
+
+            val targetContainer =
+                if (stack.item == Items.ENDER_CHEST) EnderChestContainer
+                else findContainer(
+                    containerSelection(ContainerSearchScope.Accessed) {
+                        ofAnyType(ContainerType.ShulkerBox)
+                        predicate { container ->
+                            container is NestedContainer && container.index == slot.index
+                        }
+                    }
+                ) ?: return@listen
+
+            event.cancel()
+
+            task?.cancel()
+            task = targetContainer
+                .access()
+                ?.onSuccess { ctx ->
+                    openContexts.push(ctx)
+                    lastOpenScreen = player.currentScreenHandler
                 }
-            }.run()
+                ?.onFailure { e -> warn("Failed to open ${stack.name.string}: ${e.message}") }
+                ?.onCompletion { task = null }
+                ?.start()
         }
 
         listen { event ->
-            if (event.screenHandler != lastOpenScreen) return@listen
-            lastOpenScreen = null
-            placedPos?.let {
-                lastBreak = breakAndCollectBlock(it).run()
-                placedPos = null
+            if (isClosing || task != null) return@listen
+            if (event.screenHandler != lastOpenScreen || openContexts.isEmpty()) return@listen
+
+            isClosing = true
+            val contextsToClose = mutableListOf()
+            while (openContexts.isNotEmpty()) {
+                contextsToClose.add(openContexts.pop())
             }
+
+            task?.cancel()
+            task =
+                contextsToClose.fold?>(null) { acc, ctx ->
+                    val task = ctx.close() ?: return@fold acc
+	                acc?.then(task) ?: task
+                }?.onFailure { e -> warn("Failed to close containers: ${e.message}") }
+                    ?.onCompletion { isClosing = false }
+                    ?.start()
+                    ?: run {
+                        isClosing = false
+                        return@listen
+                    }
         }
 
         onDisable {
-            placeAndOpen?.cancel()
-            lastBreak?.cancel()
+            openContexts.clear()
+            isClosing = false
         }
     }
 }
diff --git a/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt b/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt
index df99b71f8..2255713db 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/LineLock.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.player
 
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
@@ -26,10 +26,10 @@ import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import net.minecraft.util.math.Vec3d
 import java.awt.Color
diff --git a/src/main/kotlin/com/lambda/module/modules/player/NoForceRotate.kt b/src/main/kotlin/com/lambda/module/modules/player/NoForceRotate.kt
index 7ca7798fb..9ef7a1953 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/NoForceRotate.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/NoForceRotate.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.player
 import com.lambda.event.events.PlayerEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 @Suppress("unused")
 object NoForceRotate : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
index 13ab6cdbc..bab9ddda0 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt
@@ -18,12 +18,12 @@
 package com.lambda.module.modules.player
 
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.BreakConfig.SwingMode
 import com.lambda.config.editSetting
 import com.lambda.config.editTypedSettings
-import com.lambda.config.entries.Setting.Companion.disabled
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.disabled
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.hide
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
@@ -37,9 +37,9 @@ import com.lambda.interaction.construction.simulation.context.BuildContext
 import com.lambda.interaction.construction.simulation.result.results.BreakResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
+import com.lambda.interaction.manager.managers.breaking.breakRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockState
diff --git a/src/main/kotlin/com/lambda/module/modules/player/PortalGui.kt b/src/main/kotlin/com/lambda/module/modules/player/PortalGui.kt
index dea4ec2e1..3b7022704 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/PortalGui.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/PortalGui.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.player
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object PortalGui : Module(
     name = "PortalGui",
diff --git a/src/main/kotlin/com/lambda/module/modules/player/Reach.kt b/src/main/kotlin/com/lambda/module/modules/player/Reach.kt
index 13855a2e6..5a6e38b84 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/Reach.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/Reach.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.player
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object Reach : Module(
 	name = "Reach",
diff --git a/src/main/kotlin/com/lambda/module/modules/player/Replay.kt b/src/main/kotlin/com/lambda/module/modules/player/Replay.kt
index 56b5d83eb..5213b46d5 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/Replay.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/Replay.kt
@@ -27,20 +27,19 @@ import com.google.gson.JsonSerializationContext
 import com.google.gson.JsonSerializer
 import com.lambda.brigadier.CommandResult
 import com.lambda.config.blocks.RotationConfig
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.context.SafeContext
 import com.lambda.event.EventFlow.lambdaScope
 import com.lambda.event.events.MovementEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.gui.components.ClickGuiLayout
-import com.lambda.interaction.handlers.TimerHandler
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.handler.handlers.TimerHandler
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.player.Replay.InputAction.Companion.toAction
-import com.lambda.module.tag.ModuleTag
 import com.lambda.sound.SoundHandler.playSound
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.CommunicationUtils.logError
diff --git a/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt b/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
index 32036b1c1..36e52a146 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
@@ -17,16 +17,16 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import kotlin.math.roundToInt
 
 @Suppress("unused")
diff --git a/src/main/kotlin/com/lambda/module/modules/player/StackReplenish.kt b/src/main/kotlin/com/lambda/module/modules/player/StackReplenish.kt
index 4e962aea4..182849b72 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/StackReplenish.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/StackReplenish.kt
@@ -17,21 +17,25 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hide
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
-import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.util.item.ItemStackUtils.slotId
-import com.lambda.util.player.SlotUtils.hotbarStacks
-import com.lambda.util.player.SlotUtils.inventoryStacks
+import com.lambda.module.ModuleTag
 import net.minecraft.item.ItemStack
 import net.minecraft.item.Items
+import net.minecraft.screen.slot.Slot
 
 @Suppress("unused")
 object StackReplenish : Module(
@@ -47,29 +51,37 @@ object StackReplenish : Module(
 			.withEdits {
 				hideAllExcept(::inventoryConfig)
 				inventoryConfig.apply {
-					hide(::disposables, ::swapWithDisposables, ::providerPriority, ::storePriority)
+					hide(::disposables)
 				}
 			}
 
 		listen {
 			if (player.currentScreenHandler.cursorStack.item !== Items.AIR) return@listen
-			player.hotbarStacks.forEach { stack -> checkReplenish(stack) }
-			if (offhand) checkReplenish(player.offHandStack)
+			findSlots(containerSelection = HotbarContainer.select())?.forEach { slot -> checkReplenish(slot) }
+			if (offhand) findSlots(containerSelection = OffhandContainer.select())?.forEach { slot -> checkReplenish(slot) }
 		}
 	}
 
-	private fun SafeContext.checkReplenish(stack: ItemStack) {
+	private fun checkReplenish(targetSlot: Slot) {
+		val stack = targetSlot.stack
+		if (stack.isEmpty || !stack.isStackable) return
 		if (stack.count.toFloat() / stack.maxCount >= (minStackPercent.toFloat() / 100)) return
-		if (!stack.isStackable) return
 
-		player.inventoryStacks.forEach { invStack ->
-			if (!ItemStack.areItemsAndComponentsEqual(invStack, stack)) return@forEach
-			val invId = invStack.slotId
-			val completing = stack.count + invStack.count >= stack.maxCount
-			val tooMany = invStack.count + stack.count > stack.maxCount
+		val replenishSlots =
+			findSlots(
+				stackSelection(StackSelection.UNBOUNDED) {
+					predicate { s, _ -> ItemStack.areItemsAndComponentsEqual(s, stack) }
+				},
+				InventoryContainer.select()
+			) ?: return
+
+		replenishSlots.forEach { replenishSlot ->
+			val invStack = replenishSlot.stack
+			val completing = targetSlot.stack.count + invStack.count >= stack.maxCount
+			val tooMany = targetSlot.stack.count + invStack.count > stack.maxCount
 			inventoryRequest {
-				moveSlot(invId, stack.slotId)
-				if (tooMany) pickup(invId)
+				moveSlot(replenishSlot.id, targetSlot.id)
+				if (tooMany) pickup(replenishSlot.id)
 			}.submit()
 			if (completing) return
 		}
diff --git a/src/main/kotlin/com/lambda/module/modules/player/ToolSaver.kt b/src/main/kotlin/com/lambda/module/modules/player/ToolSaver.kt
index e0fea783f..c531a5714 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/ToolSaver.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/ToolSaver.kt
@@ -17,20 +17,23 @@
 
 package com.lambda.module.modules.player
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.event.events.ContainerEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
+import com.lambda.threading.runSafeAutomated
 import com.lambda.util.EnchantmentUtils.forEachEnchantment
 import com.lambda.util.EnchantmentUtils.getEnchantment
-import com.lambda.util.player.SlotUtils.hotbarSlots
-import com.lambda.util.player.SlotUtils.inventorySlots
 import net.minecraft.item.ItemStack
 import net.minecraft.screen.slot.Slot
 
@@ -50,10 +53,16 @@ object ToolSaver : Module(
 			}
 
 		listen {
-			val endangeredStacks = player.hotbarSlots.filter { it.stack.isEndangered }
+			val endangeredSlots =
+				findSlots(
+					stackSelection(StackSelection.UNBOUNDED) { predicate { stack, _ -> stack.isEndangered } },
+					HotbarContainer.select()
+				)
 
-			val inventorySlots = player.inventorySlots
-			val swaps = endangeredStacks
+			val inventorySlots = findSlots(containerSelection = InventoryContainer.select())
+
+			val swaps = endangeredSlots
+				.orEmpty()
 				.mapNotNull { endangered ->
 					val sorter = compareByDescending { swapSlot ->
 						if (!replace) 0
@@ -74,23 +83,22 @@ object ToolSaver : Module(
 						it.stack.isStackable
 					}
 					val swapWith = inventorySlots
+						.orEmpty()
 						.filter { !it.stack.isEndangered }
 						.sortedWith(sorter)
 						.firstOrNull()
 						?: return@mapNotNull null
 					endangered to swapWith
 				}
+				.toList()
 
 			if (swaps.isEmpty()) return@listen
 
-			inventoryRequest {
-				swaps.forEach {
-					pickup(it.first.id)
-					pickup(it.second.id)
-					if (!it.second.stack.isEmpty)
-						pickup(it.first.id)
+			runSafeAutomated {
+				swaps.forEach { (endangered, swapWith) ->
+					HotbarContainer.swap(endangered, swapWith, InventoryContainer)
 				}
-			}.submit()
+			}
 		}
 
 		listen { event ->
diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt
index 81eae6976..17d7a2dc3 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt
@@ -27,7 +27,7 @@ import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.extension.tickDelta
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Bobbing.kt b/src/main/kotlin/com/lambda/module/modules/render/Bobbing.kt
index d3d9303d5..f255e0c81 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Bobbing.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Bobbing.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.render
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object Bobbing : Module(
 	name = "Bobbing",
diff --git a/src/main/kotlin/com/lambda/module/modules/render/CameraTweaks.kt b/src/main/kotlin/com/lambda/module/modules/render/CameraTweaks.kt
index 21c383419..0647ea3b1 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/CameraTweaks.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/CameraTweaks.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.render
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 
 object CameraTweaks : Module(
     name = "CameraTweaks",
diff --git a/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt
index d8c79f5d4..0ee377384 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt
@@ -19,18 +19,18 @@ package com.lambda.module.modules.render
 
 import com.lambda.Lambda.mc
 import com.lambda.config.Tab
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.settings.complex.Bind
-import com.lambda.interaction.material.container.containers.EnderChestContainer
+import com.lambda.interaction.container.containers.external.EnderChestContainer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.InputUtils.isSatisfied
 import com.lambda.util.KeyCode
-import com.lambda.util.item.ItemStackUtils.bundleContents
-import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
-import com.lambda.util.item.ItemUtils.bundles
-import com.lambda.util.item.ItemUtils.shulkerBoxes
+import com.lambda.util.item.ItemStackUtils.bundleStacks
+import com.lambda.util.item.ItemStackUtils.shulkerBoxStacks
+import com.lambda.util.item.ItemUtils.BUNDLES
+import com.lambda.util.item.ItemUtils.SHULKER_BOXES
 import com.lambda.util.text.buildText
 import com.lambda.util.text.literal
 import net.minecraft.block.ShulkerBoxBlock
@@ -261,7 +261,7 @@ object ContainerPreview : Module(
 
     private fun getContainerContents(stack: ItemStack): List {
         return when {
-            isShulkerBox(stack) -> stack.shulkerBoxContents
+            isShulkerBox(stack) -> stack.shulkerBoxStacks
             isEnderChest(stack) -> EnderChestContainer.stacks
             else -> emptyList()
         }
@@ -360,7 +360,7 @@ object ContainerPreview : Module(
         val hash = container.hashCode()
 
         return containerCache.computeIfAbsent(hash) {
-            val contents = container.shulkerBoxContents + container.bundleContents
+            val contents = container.shulkerBoxStacks + container.bundleStacks
             if (contents.isEmpty()) return@computeIfAbsent ContainerPreviewInfo(null, false)
 
             val group = contents.filter { stack -> stack.item != Items.AIR }
@@ -379,7 +379,7 @@ object ContainerPreview : Module(
     }
 
     @JvmStatic
-    fun isShulkerBox(stack: ItemStack) = stack.item in shulkerBoxes
+    fun isShulkerBox(stack: ItemStack) = stack.item in SHULKER_BOXES
 
     @JvmStatic
     fun isEnderChest(stack: ItemStack) = stack.item == Items.ENDER_CHEST && EnderChestContainer.stacks.isNotEmpty()
@@ -388,7 +388,7 @@ object ContainerPreview : Module(
     fun isPreviewableContainer(stack: ItemStack) = isShulkerBox(stack) || isEnderChest(stack)
 
     @JvmStatic
-    fun isBundle(stack: ItemStack) = stack.item in bundles
+    fun isBundle(stack: ItemStack) = stack.item in BUNDLES
 
 	@JvmStatic
 	fun drawOnItem(drawContext: DrawContext, state: GuiRenderState, entity: LivingEntity?, world: World?, stack: ItemStack, x: Int, y: Int, seed: Int) {
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Esp.kt b/src/main/kotlin/com/lambda/module/modules/render/Esp.kt
index 1639b59ee..be0d21c83 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Esp.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Esp.kt
@@ -23,7 +23,7 @@ import com.lambda.config.blocks.EntityColorSettings
 import com.lambda.config.blocks.EntitySelectionSettings
 import com.lambda.config.blocks.OutlineSettings
 import com.lambda.config.blocks.WorldLineSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hide
 import com.lambda.config.withEdits
@@ -31,7 +31,7 @@ import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.NamedEnum
 import com.lambda.util.math.setAlpha
diff --git a/src/main/kotlin/com/lambda/module/modules/render/ExtraTab.kt b/src/main/kotlin/com/lambda/module/modules/render/ExtraTab.kt
index d70c37a5a..fd561bcd2 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/ExtraTab.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/ExtraTab.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.render
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import java.awt.Color
 
 object ExtraTab : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/render/FreeLook.kt b/src/main/kotlin/com/lambda/module/modules/render/FreeLook.kt
index a67af3e35..486efbd56 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/FreeLook.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/FreeLook.kt
@@ -20,10 +20,10 @@ package com.lambda.module.modules.render
 import com.lambda.Lambda.mc
 import com.lambda.event.events.PlayerEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.extension.rotation
 import net.minecraft.client.option.Perspective
 
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Freecam.kt b/src/main/kotlin/com/lambda/module/modules/render/Freecam.kt
index 1b876ac8b..d7076d2c8 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Freecam.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Freecam.kt
@@ -18,9 +18,9 @@
 package com.lambda.module.modules.render
 
 import com.lambda.Lambda.mc
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
@@ -31,11 +31,11 @@ import com.lambda.event.events.RenderEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.RotationMode
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Fullbright.kt b/src/main/kotlin/com/lambda/module/modules/render/Fullbright.kt
index 9eb06d0b9..abf415b9a 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Fullbright.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Fullbright.kt
@@ -17,12 +17,12 @@
 
 package com.lambda.module.modules.render
 
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.entity.effect.StatusEffectInstance
 import net.minecraft.entity.effect.StatusEffects
 
diff --git a/src/main/kotlin/com/lambda/module/modules/render/LightLevels.kt b/src/main/kotlin/com/lambda/module/modules/render/LightLevels.kt
index eb590e40b..7d3de14c9 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/LightLevels.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/LightLevels.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.render
 
 import com.lambda.config.Group
 import com.lambda.config.blocks.WorldLineSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hide
 import com.lambda.config.withEdits
@@ -29,10 +29,10 @@ import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
-import com.lambda.util.math.flooredBlockPos
+import com.lambda.util.math.blockPos
 import com.lambda.util.math.setAlpha
 import com.lambda.util.math.vec3d
 import com.lambda.util.world.toBlockPos
@@ -92,7 +92,7 @@ object LightLevels : Module(
 
 			runSafe {
 				if (areaMode.player) buildPositions(positions, player.blockPos)
-				if (areaMode.camera) buildPositions(positions, mc.gameRenderer.camera.pos.flooredBlockPos)
+				if (areaMode.camera) buildPositions(positions, mc.gameRenderer.camera.pos.blockPos)
 				positions.forEach { pos ->
 					buildRender(pos, dashStyle)
 				}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt
index fedeea4cd..270f73c83 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.render
 
 import com.lambda.Lambda.mc
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.client.font.TextRenderer
 import net.minecraft.client.gl.RenderPipelines
 import net.minecraft.client.gui.DrawContext
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
index 94add83e2..176106c69 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
@@ -30,9 +30,9 @@ import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRend
 import com.lambda.graphics.mc.renderer.RendererUtils.worldToScreenNormalized
 import com.lambda.graphics.text.FontHandler
 import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox
-import com.lambda.interaction.handlers.FriendHandler.isFriend
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.fullHealth
 import com.lambda.util.extension.maxFullHealth
diff --git a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
index 1287a2fcb..2ba504765 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
@@ -22,7 +22,7 @@ import com.lambda.config.blocks.EntitySelectionSettings
 import com.lambda.config.editTypedSettings
 import com.lambda.config.withEdits
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.EntityUtils.createNameMap
 import com.lambda.util.ReflectionUtils.scanResult
 import net.minecraft.block.entity.BlockEntity
diff --git a/src/main/kotlin/com/lambda/module/modules/render/RadiusESP.kt b/src/main/kotlin/com/lambda/module/modules/render/RadiusESP.kt
index 478cb318b..455ea2ebb 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/RadiusESP.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/RadiusESP.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.render
 
 import com.lambda.config.Group
 import com.lambda.config.blocks.WorldLineSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hide
 import com.lambda.config.withEdits
@@ -28,7 +28,7 @@ import com.lambda.graphics.mc.RenderBuilder
 import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.math.setAlpha
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Search.kt b/src/main/kotlin/com/lambda/module/modules/render/Search.kt
index 15752cf49..a0b0f134d 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Search.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Search.kt
@@ -21,7 +21,7 @@ import com.lambda.config.Group
 import com.lambda.config.blocks.ScreenLineSettings
 import com.lambda.config.blocks.WorldLineSettings
 import com.lambda.config.editTypedSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hide
 import com.lambda.config.settings.collections.onDeselect
@@ -36,7 +36,7 @@ import com.lambda.graphics.util.DirectionMask
 import com.lambda.graphics.util.DirectionMask.buildSideMesh
 import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.EntityUtils.decorationEntityMap
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Time.kt b/src/main/kotlin/com/lambda/module/modules/render/Time.kt
index d5e384c68..986b7876d 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Time.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Time.kt
@@ -21,7 +21,7 @@ import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket
 
 object Time : Module(
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt
index c5774a800..f2033ad5c 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt
@@ -26,9 +26,9 @@ import com.lambda.config.hide
 import com.lambda.config.withEdits
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.graphics.mc.renderer.RendererUtils.worldToScreenNormalized
-import com.lambda.interaction.handlers.FriendHandler.isFriend
+import com.lambda.interaction.handler.handlers.FriendHandler.isFriend
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import com.lambda.util.extension.prevPos
 import com.lambda.util.extension.tickDelta
diff --git a/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt b/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt
index ff2acce3d..b109d3b5a 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt
@@ -19,12 +19,12 @@ package com.lambda.module.modules.render
 
 import com.lambda.Lambda.mc
 import com.lambda.config.Tab
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.event.events.ButtonEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import net.minecraft.client.network.AbstractClientPlayerEntity
 import net.minecraft.client.util.math.MatrixStack
 import net.minecraft.item.ItemStack
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Weather.kt b/src/main/kotlin/com/lambda/module/modules/render/Weather.kt
index 13a6b5239..5c218aaf1 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Weather.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Weather.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.render
 
 import com.lambda.config.Tab
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafe
 import net.minecraft.world.World
 
diff --git a/src/main/kotlin/com/lambda/module/modules/render/WorldColors.kt b/src/main/kotlin/com/lambda/module/modules/render/WorldColors.kt
index 227d0e209..b8307141e 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/WorldColors.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/WorldColors.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.render
 
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.math.vec3d
 import net.minecraft.util.math.Vec3d
 import java.awt.Color
diff --git a/src/main/kotlin/com/lambda/module/modules/render/XRay.kt b/src/main/kotlin/com/lambda/module/modules/render/XRay.kt
index 6e9d1e2a6..679913e81 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/XRay.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/XRay.kt
@@ -18,12 +18,12 @@
 package com.lambda.module.modules.render
 
 import com.lambda.Lambda.mc
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.settings.collections.onDeselect
 import com.lambda.config.settings.collections.onSelect
 import com.lambda.context.SafeContext
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.BlockUtils
 import net.minecraft.block.Blocks
 import net.minecraft.fluid.Fluids
@@ -47,7 +47,7 @@ object XRay : Module(
 		Blocks.ANCIENT_DEBRIS
 	)
 
-	val fluids = BlockUtils.fluids - Fluids.EMPTY
+	val fluids = BlockUtils.FLUIDS - Fluids.EMPTY
 
 	@JvmStatic val opacity by setting("Opacity", 40, 1..100, 1, "Opacity of the non x-rayed blocks, (automatically overridden as 0 when running Sodium)").onValueChange(::reload)
 	@JvmStatic val blockSelection by setting("Block Selection", defaultBlocks, Registries.BLOCK - setOf(Blocks.WATER, Blocks.LAVA), description = "Block selection that will be shown (whitelist) or hidden (blacklist)")
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
index d991feb93..d654a69c5 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt
@@ -20,7 +20,7 @@ package com.lambda.module.modules.render
 import com.lambda.event.events.ButtonEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.NamedEnum
 import java.lang.Math.clamp
 
diff --git a/src/main/kotlin/com/lambda/module/modules/world/AirPlace.kt b/src/main/kotlin/com/lambda/module/modules/world/AirPlace.kt
index eae5e120d..7ac8b39a9 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/AirPlace.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/AirPlace.kt
@@ -19,7 +19,7 @@ package com.lambda.module.modules.world
 
 import com.lambda.Lambda.mc
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.settings.complex.Bind
 import com.lambda.config.withEdits
@@ -32,11 +32,11 @@ import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.interaction.construction.simulation.context.BuildContext
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.interacting.InteractRequest
-import com.lambda.interaction.managers.interacting.InteractRequest.Companion.interactRequest
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation
+import com.lambda.interaction.manager.managers.interacting.InteractRequest
+import com.lambda.interaction.manager.managers.interacting.interactRequest
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotation
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.InputUtils.isSatisfied
 import com.lambda.util.KeyCode
diff --git a/src/main/kotlin/com/lambda/module/modules/world/AutoPortal.kt b/src/main/kotlin/com/lambda/module/modules/world/AutoPortal.kt
index 3202ed162..516892f62 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/AutoPortal.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/AutoPortal.kt
@@ -19,15 +19,12 @@ package com.lambda.module.modules.world
 
 import baritone.api.pathing.goals.GoalBlock
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.WorldLineSettings
-import com.lambda.config.editSetting
 import com.lambda.config.forEachSetting
 import com.lambda.config.hide
 import com.lambda.config.hideBlock
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onRelease
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
@@ -35,20 +32,25 @@ import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
 import com.lambda.graphics.util.DirectionMask
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.module.Module
+import com.lambda.module.ModuleTag
 import com.lambda.module.modules.world.AutoPortal.PosHandler.currAnchorPos
 import com.lambda.module.modules.world.AutoPortal.PosHandler.obiPositions
 import com.lambda.module.modules.world.AutoPortal.PosHandler.portalPositions
 import com.lambda.module.modules.world.AutoPortal.PosHandler.prevAnchorPos
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
-import com.lambda.task.wrappers.thenOrNull
+import com.lambda.task.start
+import com.lambda.task.tasks.build
+import com.lambda.task.tasks.wrappers.thenOrNull
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.BlockUtils.isEmpty
@@ -59,10 +61,7 @@ import com.lambda.util.extension.tickDelta
 import com.lambda.util.math.lerp
 import com.lambda.util.math.setAlpha
 import com.lambda.util.math.vec3d
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
-import com.lambda.util.player.SlotUtils.hotbarSlots
 import net.minecraft.block.Blocks
-import net.minecraft.item.FlintAndSteelItem
 import net.minecraft.item.Items
 import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket
 import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket
@@ -76,7 +75,7 @@ import net.minecraft.util.math.Vec3d
 @Suppress("unused")
 object AutoPortal : Module(
 	name = "AutoPortal",
-	description = "Automatically places and lights a nether portal",
+	description = "Automatically builds a nether portal",
 	tag = ModuleTag.WORLD
 ) {
 	private const val RENDER_GROUP = "Renders"
@@ -105,7 +104,7 @@ object AutoPortal : Module(
 					else null
 				}
 				.onCompletion { buildTask = null }
-				.run()
+				.start()
 		}
 	private val corners by setting("Corners", false)
 	private val light by setting("Light", true, "Attempts to automatically light the portal after building")
@@ -136,9 +135,6 @@ object AutoPortal : Module(
 		setDefaultAutomationConfig()
 			.withEdits {
 				hideBlock(::eatConfig)
-				hotbarConfig::tickStageMask.editSetting {
-					defaultValue(mutableSetOf(TickEvent.Pre, TickEvent.Input.Post))
-				}
 			}
 
 		listen {
@@ -276,7 +272,7 @@ object AutoPortal : Module(
 			}
 	}
 
-	private class LightTask(
+	private class LightTask @Ta5kBuilder constructor(
 		private val pos: BlockPos,
 		private val walkIn: Boolean
 	) : Task() {
@@ -323,45 +319,41 @@ object AutoPortal : Module(
 				return
 			}
 
-			val sel = selectStack(1) { isItem() }
+			val selection = Items.FLINT_AND_STEEL.select()
 
-			val hotbarStack = sel.filterSlots(player.hotbarSlots).firstOrNull()
-			if (hotbarStack != null) {
-				val request = HotbarRequest(
-					hotbarStack.index,
-					this@AutoPortal,
-					keepTicks = 0
-				).submit(queueIfMismatchedStage = false)
+			val hotbarIndex = selection.bestMatch(HotbarContainer.slots)?.index
+			if (hotbarIndex != null) {
+				val request =
+					hotbarRequest(hotbarIndex) {
+						keepTicks(0)
+					}.submit(false)
 				if (request.done) block()
 				return
 			}
 
 			val invSlot =
-				if (inventory) sel.filterSlots(player.hotbarAndInventorySlots).firstOrNull()
+				if (inventory) findSlot(selection, InventoryContainer.select())
 				else null
 			if (invSlot == null) {
 				failure("No Flint and Steel!")
 				return
 			}
 			val hotbarSlotToSwapWith =
-				player.hotbarSlots.find { slot ->
-					slot.stack.isEmpty
-				}?.index ?: 8
+				findSlot(
+					stackSelection(StackSelection.UNBOUNDED) { isEmpty() },
+					HotbarContainer.select()
+				)?.index ?: 8
 
 			inventoryRequest {
-				swap(invSlot.id, hotbarSlotToSwapWith)
+				swapWithHotbar(invSlot.id, hotbarSlotToSwapWith)
 				action {
-					val request = HotbarRequest(
-						hotbarSlotToSwapWith,
-						this@AutoPortal,
-						keepTicks = 0,
-						nowOrNothing = true
-					).submit(queueIfMismatchedStage = false)
-					if (request.done) {
-						block()
-					}
+					val request =
+						hotbarRequest(hotbarSlotToSwapWith) {
+							keepTicks(0)
+						}.submit(false)
+					if (request.done) block()
 				}
-				swap(invSlot.id, hotbarSlotToSwapWith)
+				swapWithHotbar(invSlot.id, hotbarSlotToSwapWith)
 			}.submit()
 		}
 	}
diff --git a/src/main/kotlin/com/lambda/module/modules/world/AutoSign.kt b/src/main/kotlin/com/lambda/module/modules/world/AutoSign.kt
index d2f1e502b..068c898ad 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/AutoSign.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/AutoSign.kt
@@ -21,7 +21,7 @@ import com.ibm.icu.util.Calendar
 import com.lambda.event.events.GuiEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runConcurrent
 import com.lambda.threading.runSafeGameScheduled
 import com.lambda.util.PacketUtils.sendPacket
diff --git a/src/main/kotlin/com/lambda/module/modules/world/AutoVillagerCycle.kt b/src/main/kotlin/com/lambda/module/modules/world/AutoVillagerCycle.kt
index 64511f511..e99a9faa3 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/AutoVillagerCycle.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/AutoVillagerCycle.kt
@@ -18,26 +18,25 @@
 package com.lambda.module.modules.world
 
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.hideAllExcept
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.PacketEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
-import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
+import com.lambda.interaction.construction.blueprint.toBlueprint
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.sound.SoundHandler.playSound
-import com.lambda.task.RootTask.run
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.start
+import com.lambda.task.tasks.build
+import com.lambda.task.tasks.wrappers.thenAction
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.BlockUtils.isEmpty
@@ -220,7 +219,7 @@ object AutoVillagerCycle : Module(
 				.thenAction {
 					switchState(CycleState.OpenVillager)
 				}
-				.run()
+				.start()
 		}
 		switchState(CycleState.WaitLectern)
 	}
@@ -281,7 +280,7 @@ object AutoVillagerCycle : Module(
 				lecternPos.toStructure(TargetState.Empty)
 					.build(finishOnDone = true)
 					.thenAction { switchState(CycleState.PlaceLectern) }
-					.run()
+					.start()
 			}
 			switchState(CycleState.WaitBreak)
 			return
diff --git a/src/main/kotlin/com/lambda/module/modules/world/EnderChestFarmer.kt b/src/main/kotlin/com/lambda/module/modules/world/EnderChestFarmer.kt
index a7d4bf189..47cf245a4 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/EnderChestFarmer.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/EnderChestFarmer.kt
@@ -18,7 +18,7 @@
 package com.lambda.module.modules.world
 
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.editTypedSettings
 import com.lambda.config.hideAllExcept
@@ -35,20 +35,19 @@ import com.lambda.interaction.construction.simulation.result.Dependent
 import com.lambda.interaction.construction.simulation.result.results.GenericResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.ContainerHandler.transfer
-import com.lambda.interaction.handlers.breaking.RebreakHandler
-import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
-import com.lambda.interaction.managers.interacting.InteractRequest.Companion.interactRequest
-import com.lambda.interaction.material.StackSelection.Companion.select
-import com.lambda.interaction.material.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.moveStack
+import com.lambda.interaction.manager.managers.breaking.breakRequest
+import com.lambda.interaction.manager.managers.interacting.interactRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.CommunicationUtils.logError
 import com.lambda.util.EnchantmentUtils.getEnchantment
-import com.lambda.util.item.ItemStackUtils.empty
+import com.lambda.util.item.ItemStackUtils.emptySpace
 import com.lambda.util.item.ItemStackUtils.hasSpace
 import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
 import net.minecraft.block.BlockState
@@ -80,11 +79,11 @@ object EnderChestFarmer : Module(
 
 	private val SafeContext.inventoryFull
 		get() = player.hotbarAndInventoryStacks.let { stacks ->
-			stacks.empty == 0 && stacks.none { it.item == Items.OBSIDIAN && it.hasSpace }
+			stacks.emptySpace == 0 && stacks.none { it.item == Items.OBSIDIAN && it.hasSpace }
 		}
 
 	private val SafeContext.hasEnderChests
-		get() = Items.ENDER_CHEST.select().filterStacks(player.hotbarAndInventoryStacks).isNotEmpty()
+		get() = Items.ENDER_CHEST.select().filter(player.hotbarAndInventoryStacks).isNotEmpty()
 
 	private val SafeContext.missingPickaxe: String?
 		get() {
@@ -146,10 +145,7 @@ object EnderChestFarmer : Module(
 		}
 
 		onDisable {
-			farmPos?.let { pos ->
-				this@EnderChestFarmer.info("Mined ${obsidianCount - startObsidian} obsidian")
-				if (RebreakHandler.reBreak?.context?.blockPos == pos) RebreakHandler.clearRebreak()
-			}
+			this@EnderChestFarmer.info("Mined ${obsidianCount - startObsidian} obsidian")
 			farmPos = null
 			pendingActions.clear()
 		}
@@ -211,7 +207,7 @@ object EnderChestFarmer : Module(
 			.map { if (it is Dependent) it.lastDependency else it }
 			.filterIsInstance()
 			.firstOrNull() ?: return false
-		wrongItem.neededSelection.transfer(HotbarContainer)
+		wrongItem.neededSelection.moveStack(toSelection = HotbarContainer.select())
 		return true
 	}
 }
diff --git a/src/main/kotlin/com/lambda/module/modules/world/HighwayTools.kt b/src/main/kotlin/com/lambda/module/modules/world/HighwayTools.kt
index 22cd0d2f9..82f0aee02 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/HighwayTools.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/HighwayTools.kt
@@ -17,18 +17,18 @@
 
 package com.lambda.module.modules.world
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editTypedSettings
 import com.lambda.config.withEdits
 import com.lambda.interaction.construction.blueprint.Blueprint.Companion.emptyStructure
 import com.lambda.interaction.construction.blueprint.PropagatingBlueprint.Companion.propagatingBlueprint
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.start
+import com.lambda.task.tasks.build
 import com.lambda.util.CommunicationUtils.info
 import com.lambda.util.Describable
 import com.lambda.util.NamedEnum
@@ -131,7 +131,7 @@ object HighwayTools : Module(
 		        emptyStructure()
 	        }
         }.build(collectDrops = buildConfig.collectDrops, lifeMaintenance = true)
-            .run()
+            .start()
     }
 
     private fun generateSlice(): Structure {
diff --git a/src/main/kotlin/com/lambda/module/modules/world/MapDownloader.kt b/src/main/kotlin/com/lambda/module/modules/world/MapDownloader.kt
index b4de4c5e7..b35d66736 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/MapDownloader.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/MapDownloader.kt
@@ -19,12 +19,13 @@ package com.lambda.module.modules.world
 
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStacks
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.util.FileUtils.locationBoundDirectory
 import com.lambda.util.FolderRegistry
 import com.lambda.util.StringUtils.hashString
-import com.lambda.util.player.SlotUtils.allStacks
 import com.lambda.util.world.entitySearch
 import net.minecraft.block.MapColor
 import net.minecraft.entity.decoration.ItemFrameEntity
@@ -43,7 +44,9 @@ object MapDownloader : Module(
         listen {
             val mapStates = entitySearch(128.0)
                 .mapNotNull { FilledMapItem.getMapState(it.heldItemStack, world) } +
-                    player.allStacks.mapNotNull { FilledMapItem.getMapState(it, world) }
+                    findStacks(containerSelection = ContainerSelection.PLAYER)
+                        .orEmpty()
+                        .mapNotNull { FilledMapItem.getMapState(it, world) }
 
             mapStates.forEach { map ->
                 val name = map.hash
diff --git a/src/main/kotlin/com/lambda/module/modules/world/Nuker.kt b/src/main/kotlin/com/lambda/module/modules/world/Nuker.kt
index 6c5d677c9..ed7aa9790 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/Nuker.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/Nuker.kt
@@ -17,10 +17,10 @@
 
 package com.lambda.module.modules.world
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.BreakConfig
 import com.lambda.config.editTypedSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.settings.complex.Bind
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
@@ -29,10 +29,10 @@ import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.blueprint.TickingBlueprint.Companion.tickingBlueprint
 import com.lambda.interaction.construction.verify.TargetState
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.start
+import com.lambda.task.tasks.build
 import com.lambda.util.BlockUtils.blockPos
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.CommunicationUtils.info
@@ -140,7 +140,7 @@ object Nuker : Module(
 
 			selection
 		}.build(finishOnDone = false, async = async)
-			.run()
+			.start()
 	}
 
 	private fun SafeContext.isWithinDigDirection(pos: BlockPos): Boolean {
diff --git a/src/main/kotlin/com/lambda/module/modules/world/Printer.kt b/src/main/kotlin/com/lambda/module/modules/world/Printer.kt
index 97e290420..a8284547c 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/Printer.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/Printer.kt
@@ -19,9 +19,9 @@ package com.lambda.module.modules.world
 
 import com.lambda.Lambda.mc
 import com.lambda.config.Group
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.blocks.WorldLineSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.forEachSetting
 import com.lambda.config.hideAllExcept
 import com.lambda.config.withEdits
@@ -33,10 +33,10 @@ import com.lambda.interaction.construction.simulation.result.results.BreakResult
 import com.lambda.interaction.construction.simulation.result.results.InteractResult
 import com.lambda.interaction.construction.verify.TargetState
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
+import com.lambda.task.start
+import com.lambda.task.tasks.build
 import com.lambda.util.BlockUtils.blockPos
 import com.lambda.util.CommunicationUtils.logError
 import com.lambda.util.Describable
@@ -149,7 +149,8 @@ object Printer : Module(
 				.filter { DataManager.getRenderLayerRange().isPositionWithinRange(it) && inSchematic(it) }
 				.associateWith { TargetState.State(schematicWorld.getBlockState(it)) }
 				.filter { air || !it.value.blockState.isAir }
-		}.build(finishOnDone = false, async = async) { filterBuildResults(it) }.run()
+		}.build(finishOnDone = false, async = async) { filterBuildResults(it) }
+			.start()
 	}
 
 	/**
diff --git a/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt b/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt
index f4fd7a190..b6c031f89 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.module.modules.world
 
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editSetting
 import com.lambda.config.editTypedSettings
 import com.lambda.config.hide
@@ -30,11 +30,13 @@ import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.simulation.context.BuildContext
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.interacting.InteractRequest.Companion.interactRequest
-import com.lambda.interaction.material.StackSelection.Companion.selectStack
-import com.lambda.interaction.material.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStack
+import com.lambda.interaction.manager.managers.interacting.interactRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
+import com.lambda.module.ModuleTag
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockPos
 import com.lambda.util.BlockUtils.blockState
@@ -74,11 +76,13 @@ object Scaffold : Module(
 			}
 
 		listen {
-			val selection = selectStack {
-				{ it.blockItem.let { blockItem -> blockItem != null && blockItem.block !in blacklistedBlocks } }
+			val selection = stackSelection {
+				predicate { stack, _ ->
+					stack.blockItem.let { blockItem -> blockItem != null && blockItem.block !in blacklistedBlocks }
+				}
 			}
-			val stack = player.mainHandStack.takeIf { selection.filterStack(it) }
-				?: selection.filterStacks(HotbarContainer.stacks).firstOrNull() ?: return@listen
+			val stack = player.mainHandStack.takeIf { selection.matches(it) }
+				?: findStack(selection, HotbarContainer.select(), sorted = false) ?: return@listen
 			val playerSupport = player.blockPos.down()
 			val alreadySupported = blockState(playerSupport).hasSolidTopSurface(world, playerSupport, player)
 			if (alreadySupported) return@listen
diff --git a/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt b/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt
index d25a7e5b4..084e71d98 100644
--- a/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt
+++ b/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt
@@ -20,13 +20,12 @@ package com.lambda.module.modules.world
 import baritone.api.pathing.goals.GoalBlock
 import com.lambda.Lambda.mc
 import com.lambda.config.Tab
-import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
+import com.lambda.config.automation.setDefaultAutomationConfig
 import com.lambda.config.editTypedSettings
-import com.lambda.config.entries.Setting.Companion.onValueChange
+import com.lambda.config.entries.onValueChange
 import com.lambda.config.hide
 import com.lambda.config.hideBlock
 import com.lambda.config.settings.complex.Bind
-import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
 import com.lambda.config.withEdits
 import com.lambda.context.SafeContext
 import com.lambda.event.events.ButtonEvent
@@ -38,21 +37,31 @@ import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
 import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
-import com.lambda.interaction.managers.rotating.RotationManager
-import com.lambda.interaction.material.container.containers.EnderChestContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.containers.OffhandContainer
+import com.lambda.interaction.container.containers.external.EnderChestContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlots
+import com.lambda.interaction.handler.handlers.ContainerHandler.findStacks
+import com.lambda.interaction.handler.handlers.ContainerHandler.stacksAvailable
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.dist
+import com.lambda.interaction.manager.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.module.Module
-import com.lambda.module.tag.ModuleTag
-import com.lambda.task.RootTask.run
+import com.lambda.module.ModuleTag
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
-import com.lambda.task.tasks.OpenContainerTask
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.start
+import com.lambda.task.tasks.build
+import com.lambda.task.tasks.openContainer
+import com.lambda.task.tasks.wrappers.thenAction
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockEntity
 import com.lambda.util.BlockUtils.blockState
@@ -66,13 +75,6 @@ import com.lambda.util.extension.containerStacks
 import com.lambda.util.extension.rotation
 import com.lambda.util.math.distSq
 import com.lambda.util.math.setAlpha
-import com.lambda.util.player.SlotUtils.allSlots
-import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
-import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
-import com.lambda.util.player.SlotUtils.hotbarSlots
-import com.lambda.util.player.SlotUtils.hotbarStacks
-import com.lambda.util.player.SlotUtils.inventoryStacks
-import com.lambda.util.player.SlotUtils.offHandSlots
 import com.lambda.util.text.bold
 import com.lambda.util.text.buildText
 import com.lambda.util.text.color
@@ -97,6 +99,7 @@ import org.lwjgl.glfw.GLFW
 import java.awt.Color
 import kotlin.math.min
 
+//ToDo: Transform to use the updated container api alongside properly implementing StashContainer and using tasks for chaining instead of a state machine.
 @Suppress("unused")
 object StashMover : Module(
 	name = "StashMover",
@@ -363,7 +366,7 @@ object StashMover : Module(
 				)
 				task = null
 			}
-			.run()
+			.start()
 	}
 
 	fun pauseUnpause() {
@@ -457,7 +460,12 @@ object StashMover : Module(
 					MoverState.DispensingPearl -> handleDispensingPearl()
 					MoverState.AwaitingPearl ->
 						checkTimerProgress(MoverState.DispensingPearl, pearlButtonTimeout) {
-							if (player.hotbarAndInventoryStacks.any { it.item === Items.ENDER_PEARL }) {
+							val hasPearl =
+								stacksAvailable(
+									Items.ENDER_PEARL.select(),
+									ContainerSelection.HOTBAR_AND_INVENTORY
+								)
+							if (hasPearl) {
 								pearlThrown = false
 								moverState = MoverState.ThrowingPearl
 								true
@@ -504,7 +512,12 @@ object StashMover : Module(
 				if (moverState != MoverState.AwaitingTeleport) return@listen
 				val packet = event.packet
 				if (packet !is PlayerPositionLookS2CPacket) return@listen
-				if (finished && player.hotbarAndInventoryStacks.all { it.isEmpty }) {
+				val allMoved =
+					!stacksAvailable(
+						StackSelection.ALL_EMPTY,
+						ContainerSelection.HOTBAR_AND_INVENTORY
+					)
+				if (finished && allMoved) {
 					success(finishedMessage)
 					return@listen
 				}
@@ -523,12 +536,19 @@ object StashMover : Module(
 				moverState = MoverState.OpeningPullContainer
 				return
 			}
-			if (!moveFromContainerToContainer(screenHandler.containerSlots, player.hotbarAndInventoryStacks)) return
+			val move =
+				moveFromContainerToContainer(
+					screenHandler.containerSlots,
+					findStacks(containerSelection = ContainerSelection.HOTBAR_AND_INVENTORY)
+						.orEmpty()
+						.toList()
+				)
+			if (!move) return
 			pullContainer?.let { container ->
 				if (screenHandler.containerStacks.all { it.isEmpty }) {
 					pullContainers.remove(container)
 					pulledContainers.add(container)
-					if (player.hotbarAndInventoryStacks.any { it.isEmpty }) {
+					if (stacksAvailable(StackSelection.ALL_EMPTY, ContainerSelection.HOTBAR_AND_INVENTORY)) {
 						moverState = MoverState.OpeningPullContainer
 						return
 					}
@@ -546,7 +566,14 @@ object StashMover : Module(
 				moverState = MoverState.OpeningPutEnderChest
 				return
 			}
-			if (moveFromContainerToContainer(player.hotbarAndInventorySlots, screenHandler.containerStacks)) {
+			val move =
+				moveFromContainerToContainer(
+					findSlots(containerSelection = ContainerSelection.HOTBAR_AND_INVENTORY, sorted = false)
+						.orEmpty()
+						.toList(),
+					screenHandler.containerStacks
+				)
+			if (move) {
 				moverState = MoverState.OpeningPullContainer
 			}
 		}
@@ -562,7 +589,7 @@ object StashMover : Module(
 					pulledContainers.clear()
 					moverState = MoverState.MessagingForPearl
 				}
-				.execute(this)
+				.start()
 		}
 
 		private fun SafeContext.handleMessagingForPearl() {
@@ -583,7 +610,12 @@ object StashMover : Module(
 				rotation(rotation)
 			}.submit()
 			if (!rotationRequest.done || rotation dist RotationManager.serverRotation > 0.001) return
-			val throwSlots = player.hotbarAndInventorySlots.filter { !it.stack.isEmpty }
+			val throwSlots =
+				findSlots(
+					StackSelection.ALL_NOT_EMPTY,
+					ContainerSelection.HOTBAR_AND_INVENTORY
+				).orEmpty()
+					.toList()
 			if (throwSlots.isNotEmpty()) {
 				val inventoryRequest = inventoryRequest(settleForLess = true) {
 					throwSlots.forEach { slot ->
@@ -600,12 +632,19 @@ object StashMover : Module(
 				moverState = MoverState.OpeningPutContainer
 				return
 			}
-			if (!moveFromContainerToContainer(player.hotbarAndInventorySlots, screenHandler.containerStacks)) return
+			val move =
+				moveFromContainerToContainer(
+					findSlots(containerSelection = ContainerSelection.HOTBAR_AND_INVENTORY, sorted = false)
+						.orEmpty()
+						.toList(),
+					screenHandler.containerStacks
+				)
+			if (!move) return
 			putContainer?.let { container ->
 				if (screenHandler.containerStacks.all { !it.isEmpty }) {
 					putContainers.remove(container)
 					filledContainers.add(container)
-					if (player.hotbarAndInventoryStacks.any { !it.isEmpty }) {
+					if (stacksAvailable(StackSelection.ALL_NOT_EMPTY, ContainerSelection.HOTBAR_AND_INVENTORY)) {
 						moverState = MoverState.OpeningPutContainer
 						return
 					}
@@ -619,9 +658,14 @@ object StashMover : Module(
 				moverState = MoverState.OpeningPullEnderChest
 				return
 			}
-			if (moveFromContainerToContainer(screenHandler.containerSlots, player.hotbarAndInventoryStacks)) {
-				putOrThrowItems()
-			}
+			val move =
+				moveFromContainerToContainer(
+					screenHandler.containerSlots,
+					findStacks(containerSelection = ContainerSelection.HOTBAR_AND_INVENTORY)
+						.orEmpty()
+						.toList()
+				)
+			if (move) putOrThrowItems()
 		}
 
 		private fun SafeContext.handleDispensingPearl() {
@@ -631,13 +675,17 @@ object StashMover : Module(
 				return
 			}
 			if (BaritoneHandler.isActive) return
-			if (player.hotbarStacks.none { it.isEmpty }) {
-				val firstSlot = player.hotbarSlots.getOrNull(0) ?: run { failWithLog("No first slot? This shouldn't occur.", ::failure); return }
-				if (player.inventoryStacks.any { it.isEmpty }) {
+			if (HotbarContainer.stacks.none { it.isEmpty }) {
+				val firstSlot = findSlot(containerSelection = HotbarContainer.select(), sorted = false)
+					?: run {
+						failWithLog("No first slot? This shouldn't occur.", ::failure)
+						return
+					}
+				if (InventoryContainer.stacks.any { it.isEmpty }) {
 					inventoryRequest { quickMove(firstSlot.id) }.submit()
 					return
 				} else if (player.offHandStack.isEmpty) {
-					inventoryRequest { swap(firstSlot.id, 40) }.submit()
+					inventoryRequest { swapWithHotbar(firstSlot.id, 40) }.submit()
 					return
 				}
 				failWithLog("No free slots for an ender pearl!", ::failure)
@@ -648,7 +696,7 @@ object StashMover : Module(
 					tickTimer.reset()
 					moverState = MoverState.AwaitingPearl
 				}
-				?.execute(this@MoverBot)
+				?.start()
 		}
 
 		private fun SafeContext.handleThrowingPearl() {
@@ -662,11 +710,11 @@ object StashMover : Module(
 
 			if (pearlThrown) {
 				if (!player.offHandStack.isEmpty) {
-					if (player.hotbarAndInventoryStacks.none { it.isEmpty }) {
+					if (!stacksAvailable(StackSelection.ALL_EMPTY, ContainerSelection.HOTBAR_AND_INVENTORY)) {
 						failWithLog("No free slots to return the offhand stack to!", ::failure)
 						return
 					}
-					val offhandSlot = player.offHandSlots.firstOrNull() ?: run { failWithLog("No offhand slot? This shouldn't occur.", ::failure); return }
+					val offhandSlot = OffhandContainer.slots.firstOrNull() ?: run { failWithLog("No offhand slot? This shouldn't occur.", ::failure); return }
 					inventoryRequest { quickMove(offhandSlot.id) }.submit()
 				}
 				putOrThrowItems()
@@ -679,17 +727,17 @@ object StashMover : Module(
 			}.submit()
 			if (!rotationRequest.done) return
 			if (player.mainHandStack.item != Items.ENDER_PEARL) {
-				val hotbarSlot = player.hotbarSlots.firstOrNull { it.stack.item === Items.ENDER_PEARL }
+				val hotbarSlot = findSlot(Items.ENDER_PEARL.select(), HotbarContainer.select())
 				if (hotbarSlot != null) {
-					val hotbarRequest = HotbarRequest(hotbarSlot.index, StashMover, nowOrNothing = false).submit()
+					val hotbarRequest = hotbarRequest(hotbarSlot.index).submit()
 					if (!hotbarRequest.done) return
 				} else {
-					val inventorySlot = player.allSlots.firstOrNull { it.stack.item === Items.ENDER_PEARL }
+					val inventorySlot = findSlot(Items.ENDER_PEARL.select(), InventoryContainer.select())
 					if (inventorySlot == null) {
 						failWithLog("No pearl in inventory!", ::failure)
 						return
 					}
-					inventoryRequest { swap(inventorySlot.id, 0) }.submit()
+					inventoryRequest { swapWithHotbar(inventorySlot.id, 0) }.submit()
 					return
 				}
 			}
@@ -748,12 +796,9 @@ object StashMover : Module(
 					return
 				}
 
-			OpenContainerTask(
-				pos,
-				StashMover
-			).thenAction {
-				onOpened(pos)
-			}.execute(this@MoverBot)
+			openContainer(pos)
+				.thenAction { onOpened(pos) }
+				.start()
 		}
 
 		private fun SafeContext.moveFromContainerToContainer(
@@ -826,7 +871,7 @@ object StashMover : Module(
 							?.thenAction {
 								pearlState = PearlState.Waiting
 							}
-							?.execute(this@PearlBot)
+							?.start()
 					}
 					else -> {}
 				}
diff --git a/src/main/kotlin/com/lambda/network/CapeHandler.kt b/src/main/kotlin/com/lambda/network/CapeHandler.kt
index e92ba1540..241c1f653 100644
--- a/src/main/kotlin/com/lambda/network/CapeHandler.kt
+++ b/src/main/kotlin/com/lambda/network/CapeHandler.kt
@@ -21,7 +21,7 @@ import com.lambda.Lambda.LOG
 import com.lambda.Lambda.mc
 import com.lambda.config.Config
 import com.lambda.config.categories.SecretsCategory
-import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
+import com.lambda.config.entries.onValueChangeUnsafe
 import com.lambda.core.Loadable
 import com.lambda.event.events.WorldEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
diff --git a/src/main/kotlin/com/lambda/task/RootTask.kt b/src/main/kotlin/com/lambda/task/RootTask.kt
index cd73c3aa1..006ab80f1 100644
--- a/src/main/kotlin/com/lambda/task/RootTask.kt
+++ b/src/main/kotlin/com/lambda/task/RootTask.kt
@@ -17,22 +17,16 @@
 
 package com.lambda.task
 
-import com.lambda.task.wrappers.TaskSupplier
-import com.lambda.threading.runSafe
+import com.lambda.task.Task.Ta5kBuilder
 
 object RootTask : Task() {
     override val name get() = "Root Task"
 
-    @Ta5kBuilder
-    inline fun > T.run(): T {
-        execute(this@RootTask)
-        return this
-    }
-
-    @Ta5kBuilder
-    fun Task<*>.run(task: TaskSupplier) {
-        runSafe {
-            task(Unit).execute(this@run)
-        }
+    init {
+        state = State.Running
     }
 }
+
+@Ta5kBuilder
+inline fun > T.start(): T =
+    apply { execute(RootTask) }
diff --git a/src/main/kotlin/com/lambda/task/Task.kt b/src/main/kotlin/com/lambda/task/Task.kt
index cbad1a911..9e8cc6bf7 100644
--- a/src/main/kotlin/com/lambda/task/Task.kt
+++ b/src/main/kotlin/com/lambda/task/Task.kt
@@ -15,264 +15,314 @@
  * along with this program.  If not, see .
  */
 
+@file:Suppress("unused")
+
 package com.lambda.task
 
-import com.lambda.Lambda.LOG
 import com.lambda.context.SafeContext
 import com.lambda.event.EventFlow.unsubscribe
 import com.lambda.event.Muteable
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.module.modules.client.Client.verboseDebug
-import com.lambda.task.wrappers.onFail
+import com.lambda.task.tasks.wrappers.withRecovery
 import com.lambda.threading.runSafe
 import com.lambda.util.CommunicationUtils.logError
 import com.lambda.util.Nameable
-import com.lambda.util.StringUtils.capitalize
-import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.DurationUnit
 import kotlin.time.toDuration
 
-@Suppress("unused")
-abstract class Task : Nameable, Muteable {
-    var parent: Task<*>? = null
-    val subTasks = mutableListOf>()
-    var state = State.Init
-    override val isMuted: Boolean get() = state == State.Paused || state == State.Init
-    var age = 0
-    private val depth: Int get() = parent?.depth?.plus(1) ?: 0
-    val isCompleted get() = state == State.Completed
-    val size: Int get() = subTasks.sumOf { it.size } + 1
-
-    private val successCallbacks = mutableListOf Unit>()
-    private val completionCallbacks = mutableListOf Unit>()
-    private val failureCallbacks = mutableListOf Unit>()
-
-    val duration: String get() =
-        (age * 50).toDuration(DurationUnit.MILLISECONDS).toComponents { days, hours, minutes, seconds, nanoseconds ->
-            "${"%03d".format(days)}:${"%02d".format(hours)}:${"%02d".format(minutes)}:${"%02d".format(seconds)}.${"${nanoseconds / 1_000_000}".take(2)}"
-        }
-
-    enum class State {
-        Init,
-        Running,
-        Paused,
-        Cancelled,
-        Failed,
-        Completed;
-
-        val display get() = name.lowercase().capitalize()
-    }
-
-    init {
-        listen({ Int.MAX_VALUE }) { age++ }
-    }
-
-    /**
-     * "Typo" in name is used to force the dsl style green
-     * (color is based on name hash, don't ask me who came up with this)
-     */
-    @DslMarker
-    annotation class Ta5kBuilder
-
-    /**
-     * Invoked when the task starts execution.
-     *
-     * This method serves as a lifecycle hook and can be overridden to define
-     * custom behavior or initialization steps necessary before the task begins.
-     * It provides a `SafeContext` to access relevant context-sensitive properties
-     * or actions safely.
-     *
-     * By default, this method does not contain any logic. Subclasses may override
-     * it to implement specific functionality, such as logging, resource allocation,
-     * or preparing preconditions for task execution.
-     */
-    @Ta5kBuilder
-    protected open fun SafeContext.onStart() {}
-
-    /**
-     * This function is called when the task is canceled.
-     * It can be overridden for tasks that need to perform cleanup operations,
-     * such as cancelling a block breaking progress, releasing resources,
-     * or stopping any ongoing operations that were started by the task.
-     */
-    @Ta5kBuilder
-    protected open fun SafeContext.onCancel() {}
-
-    /**
-     * Executes the current task as a subtask of the specified owner task.
-     *
-     * This method adds the current task to the owner's subtasks, sets the parent relationship,
-     * logs the execution details, and invokes the necessary lifecycle hooks. Additionally,
-     * it manages the state of the parent task and starts any required listeners for execution.
-     *
-     * @param owner The parent task that will execute this task as a sub task. Must not be the same as this task.
-     * @param pauseParent Defines whether the parent task should be paused during the execution of this task. Defaults to `true`.
-     * @return The current task instance as a `Task` to support chaining or further configuration.
-     * @throws IllegalArgumentException if the owner task is the same as the task being executed.
-     */
-    @Ta5kBuilder
-    fun execute(owner: Task<*>, pauseParent: Boolean = true): Task {
-        require(owner != this) { "Cannot execute a task as a sub task of itself" }
-        owner.subTasks.add(this)
-        parent = owner
-        if (verboseDebug) LOG.info("${owner.name} started $name")
-        if (pauseParent) {
-            if (verboseDebug) LOG.info("$name pausing parent ${owner.name}")
-            if (owner !is RootTask) owner.pause()
-        }
-        state = State.Running
-        runSafe { runCatching { onStart() }.onFailure { failure(it) } }
-        return this
-    }
-
-    @Ta5kBuilder
-    protected fun success(result: Result) {
-        unsubscribe()
-        state = State.Completed
-
-        parent?.onSubTaskSuccess(this)
-        parent?.onSubTaskCompletion(this)
-
-        runSafe {
-            successCallbacks.forEach { it.invoke(this, result) }
-            completionCallbacks.forEach { it.invoke(this) }
-        }
-
-        parent?.subTasks?.remove(this)
-    }
-
-    @Ta5kBuilder
-    protected fun Task.success() {
-        success(Unit)
-    }
-
-    @Ta5kBuilder
-    protected fun failure(
-        e: Throwable,
-        stacktrace: MutableList> = mutableListOf(),
-    ) {
-        state = State.Failed
-        unsubscribe()
-        cancelSubTasks()
-        stacktrace.add(this)
-
-        parent?.onSubTaskFailure(this, e)
-            ?: run {
-                if (!verboseDebug) return@run
-                val message =
-                    buildString {
-                        val first = stacktrace.firstOrNull() ?: return@buildString
-                        append("${first.name} failed: ${e.message}\n")
-                        stacktrace.drop(1).forEach {
-                            append("  -> ${it.name}\n")
-                        }
-                    }
-                logError(message)
-            }
-        parent?.onSubTaskCompletion(this)
-
-        runSafe {
-            failureCallbacks.forEach { it.invoke(this, e) }
-            completionCallbacks.forEach { it.invoke(this) }
-        }
-
-        parent?.subTasks?.remove(this)
-    }
-
-    @Ta5kBuilder
-    protected fun failure(message: String) = failure(IllegalStateException(message))
-
-    @Ta5kBuilder
-    fun activate() {
-        if (state != State.Paused) return
-        state = State.Running
-    }
-
-    @Ta5kBuilder
-    fun pause() {
-        if (state != State.Running) return
-        state = State.Paused
-    }
-
-    @Ta5kBuilder
-    fun cancel() = internalCancel(true)
-
-    private fun internalCancel(removeFromParent: Boolean = true) {
-        unsubscribe()
-        runSafe { onCancel() }
-        cancelSubTasks()
-        if (removeFromParent) parent?.subTasks?.remove(this)
-        parent?.activate()
-        if (this is RootTask) return
-        if (state == State.Completed || state == State.Cancelled) return
-        state = State.Cancelled
-    }
-
-    @Ta5kBuilder
-    fun cancelSubTasks() {
-        subTasks.forEach { it.internalCancel(removeFromParent = false) }
-        subTasks.clear()
-    }
-
-    @Ta5kBuilder
-    protected open fun onSubTaskSuccess(subTask: Task<*>) {
-        activate()
-    }
-
-    @Ta5kBuilder
-    protected open fun onSubTaskCompletion(subTask: Task<*>) {
-        activate()
-    }
-
-    @Ta5kBuilder
-    protected open fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
-        failure(cause)
-    }
-
-    /**
-     * Registers a callback for if the task succeeds.
-     */
-    @Ta5kBuilder
-    fun onSuccess(callback: SafeContext.(Result) -> Unit): Task {
-        successCallbacks.add(callback)
-        return this
-    }
-
-    /**
-     * Registers a callback for if the task fails.
-     *
-     * This could be mistaken for [onFail] which is used to wrap the given task with another task that runs a recovery task if this one fails.
-     */
-    @Ta5kBuilder
-    fun onFailure(callback: SafeContext.(Throwable) -> Unit): Task {
-        failureCallbacks.add(callback)
-        return this
-    }
-
-    /**
-     * Registers a callback for when the task completes.
-     *
-     * This is called regardless of whether the task succeeds or fails.
-     */
-    @Ta5kBuilder
-    fun onCompletion(callback: SafeContext.() -> Unit): Task {
-        completionCallbacks.add(callback)
-        return this
-    }
-
-    override fun toString() =
-        buildString { appendTaskTree(this@Task) }
-
-    private fun StringBuilder.appendTaskTree(task: Task<*>, level: Int = 0, maxEntries: Int = 10) {
-        if (task.state == State.Cancelled) return
-        appendLine("${" ".repeat(level * 4)}${task.name}" + if (task !is RootTask) " [${task.state.display}] ${(task.age * 50).milliseconds}" else "")
-        val left = task.subTasks.size - maxEntries
-        if (left > 0) {
-            appendLine("${" ".repeat((level + 1) * 4)}...and $left more tasks")
-        }
-        task.subTasks.takeLast(maxEntries).forEach {
-            appendTaskTree(it, level + 1)
-        }
-    }
+abstract class Task : Nameable, Muteable {
+	var state = State.Init
+	var age = 0
+
+	var parent: Task<*>? = null
+	val subTasks = mutableListOf>()
+	private val depth: Int get() = parent?.depth?.plus(1) ?: 0
+	val size: Int get() = subTasks.sumOf { it.size } + 1
+
+	override val isMuted: Boolean get() = state == State.Paused || state == State.Init
+
+	open val isInvisible = false
+
+	private val successCallbacks = mutableListOf Unit>()
+	private val completionCallbacks = mutableListOf Unit>()
+	private val failureCallbacks = mutableListOf Unit>()
+
+	val duration: String
+		get() =
+			(age * 50).toDuration(DurationUnit.MILLISECONDS).toComponents { days, hours, minutes, seconds, nanoseconds ->
+				"${"%03d".format(days)}:${"%02d".format(hours)}:${"%02d".format(minutes)}:${"%02d".format(seconds)}.${"${nanoseconds / 1_000_000}".take(2)}"
+			}
+
+	enum class State {
+		Init,
+		Running,
+		Paused,
+		Cancelled,
+		Failed,
+		Completed;
+	}
+
+	init {
+		listen({ Int.MAX_VALUE }) { age++ }
+	}
+
+	/**
+	 * "Typo" in name is used to force the dsl style green
+	 * (color is based on name hash, don't ask me who came up with this)
+	 */
+	@DslMarker
+	annotation class Ta5kBuilder
+
+	@Ta5kBuilder
+	context(parentTask: Task<*>)
+	fun start(pauseParent: Boolean = true) =
+		this.execute(parentTask, pauseParent)
+
+	/**
+	 * Executes the current task as a subtask of the specified owner task.
+	 *
+	 * This method adds the current task to the owner's subtasks, sets the parent relationship,
+	 * logs the execution details, and invokes the necessary lifecycle hooks. Additionally,
+	 * it manages the state of the parent task and starts any required listeners for execution.
+	 *
+	 * @param parent The parent task that will execute this task as a sub task. Must not be the same as this task.
+	 * @param pauseParent Defines whether the parent task should be paused during the execution of this task. Defaults to `true`.
+	 * @return The current task instance as a `Task` to support chaining or further configuration.
+	 * @throws IllegalArgumentException if the owner task is the same as the task being executed.
+	 */
+	@Ta5kBuilder
+	fun execute(parent: Task<*>, pauseParent: Boolean = true): Task =
+		apply {
+	    	require(parent != this) { "Cannot execute a task as a sub task of itself" }
+	    	if (state != State.Init) return@apply
+			if (this@Task is RootTask) return@apply
+	    	parent.subTasks.add(this)
+	    	this@Task.parent = parent
+	    	if (pauseParent) {
+			    if (parent !is RootTask) parent.pause()
+		    }
+			state = State.Running
+	    	runSafe {
+				runCatching {
+					onStart()
+				}.onFailure {
+					failure(it)
+				}
+			}
+	    }
+
+	/**
+	 * Invoked when the task starts execution.
+	 *
+	 * This method serves as a lifecycle hook and can be overridden to define
+	 * custom behavior or initialization steps necessary before the task begins.
+	 * It provides a `SafeContext` to access relevant context-sensitive properties
+	 * or actions safely.
+	 *
+	 * By default, this method does not contain any logic. Subclasses may override
+	 * it to implement specific functionality, such as logging, resource allocation,
+	 * or preparing preconditions for task execution.
+	 */
+	@Ta5kBuilder
+	protected open fun SafeContext.onStart() {}
+
+	/**
+	 * This function is called when the task is canceled.
+	 * It can be overridden for tasks that need to perform cleanup operations,
+	 * such as cancelling a block breaking progress, releasing resources,
+	 * or stopping any ongoing operations that were started by the task.
+	 */
+	@Ta5kBuilder
+	protected open fun SafeContext.onCancel() {}
+
+	@Ta5kBuilder
+	protected fun success(result: R) {
+		if (state != State.Running && state != State.Paused) return
+		if (this is RootTask) return
+		unsubscribe()
+		state = State.Completed
+		cancelSubTasks()
+
+		parent?.onSubTaskSuccess(this)
+		parent?.onSubTaskCompletion(this)
+
+		runSafe {
+			successCallbacks.forEach { it.invoke(this, result) }
+			completionCallbacks.forEach { it.invoke(this) }
+		}
+
+		parent?.subTasks?.remove(this)
+	}
+
+	@Ta5kBuilder
+	protected fun Task.success() {
+		success(Unit)
+	}
+
+	@Ta5kBuilder
+	protected fun failure(
+		e: Throwable,
+		stacktrace: MutableList> = mutableListOf(),
+	) {
+		if (state != State.Running && state != State.Paused) return
+		if (this is RootTask) return
+		unsubscribe()
+		state = State.Failed
+		cancelSubTasks()
+		stacktrace.add(this)
+
+		parent?.onSubTaskFailure(this, e)
+			?: run {
+				if (!verboseDebug) return@run
+				logError(
+					buildString {
+						val first = stacktrace.firstOrNull() ?: return@buildString
+						append("${first.name} failed: ${e.message}\n")
+						stacktrace.drop(1).forEach {
+							append("  -> ${it.name}\n")
+						}
+					}
+				)
+			}
+		parent?.onSubTaskCompletion(this)
+
+		runSafe {
+			failureCallbacks.forEach { it.invoke(this, e) }
+			completionCallbacks.forEach { it.invoke(this) }
+		}
+
+		parent?.subTasks?.remove(this)
+	}
+
+	@Ta5kBuilder
+	protected fun failure(message: String) = failure(IllegalStateException(message))
+
+	@Ta5kBuilder
+	fun cancel() = internalCancel(true)
+
+	@Ta5kBuilder
+	private fun internalCancel(removeFromParent: Boolean = true) {
+		if (state != State.Running && state != State.Paused) return
+		cancelSubTasks()
+		if (this is RootTask) return
+		unsubscribe()
+		state = State.Cancelled
+		runSafe {
+			onCancel()
+			completionCallbacks.forEach { it.invoke(this) }
+		}
+
+		if (removeFromParent) parent?.subTasks?.remove(this)
+		parent?.activate()
+	}
+
+	@Ta5kBuilder
+	private fun cancelSubTasks() {
+		subTasks.toList().forEach { it.internalCancel(removeFromParent = false) }
+		subTasks.clear()
+	}
+
+	@Ta5kBuilder
+	fun pause() {
+		if (state != State.Running) return
+		state = State.Paused
+	}
+
+	@Ta5kBuilder
+	fun activate() {
+		if (state != State.Paused) return
+		state = State.Running
+	}
+
+	@Ta5kBuilder
+	protected open fun onSubTaskSuccess(subTask: Task<*>) {
+		activate()
+	}
+
+	@Ta5kBuilder
+	protected open fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		failure(cause)
+	}
+
+	@Ta5kBuilder
+	protected open fun onSubTaskCompletion(subTask: Task<*>) {
+		activate()
+	}
+
+	/**
+	 * Registers a callback for if the task succeeds.
+	 */
+	@Ta5kBuilder
+	fun onSuccess(callback: SafeContext.(R) -> Unit): Task {
+		successCallbacks.add(callback)
+		return this
+	}
+
+	/**
+	 * Registers a callback for if the task fails.
+	 *
+	 * This could be mistaken for [withRecovery] which is used to wrap the given task with another task that runs a recovery task if this one fails.
+	 */
+	@Ta5kBuilder
+	fun onFailure(callback: SafeContext.(Throwable) -> Unit): Task {
+		failureCallbacks.add(callback)
+		return this
+	}
+
+	/**
+	 * Registers a callback for when the task completes.
+	 *
+	 * This is called regardless of whether the task succeeds or fails.
+	 */
+	@Ta5kBuilder
+	fun onCompletion(callback: SafeContext.() -> Unit): Task {
+		completionCallbacks.add(callback)
+		return this
+	}
+
+	override fun toString() = taskTree()
+
+	fun taskTree(showInvisible: Boolean = false) =
+		buildString { appendTaskTree(this@Task, showInvisible = showInvisible) }
+
+	private fun StringBuilder.appendTaskTree(
+		task: Task<*>,
+		level: Int = 0,
+		maxEntries: Int = 10,
+		showInvisible: Boolean = false
+	) {
+		if (task.state == State.Cancelled) return
+		if (!showInvisible && task.isInvisible) {
+			task.displaySubTasks(false).forEach {
+				appendTaskTree(it, level, maxEntries, false)
+			}
+			return
+		}
+		appendLine("${" ".repeat(level * 4)}${task.name}" + if (task !is RootTask) " [${task.state.name}] $duration" else "")
+		val subTasks = task.displaySubTasks(showInvisible)
+		val left = subTasks.size - maxEntries
+		if (left > 0) {
+			appendLine("${" ".repeat((level + 1) * 4)}...and $left more tasks")
+		}
+		subTasks.takeLast(maxEntries).forEach {
+			appendTaskTree(it, level + 1, maxEntries, showInvisible)
+		}
+	}
+
+	private fun Task<*>.displaySubTasks(showInvisible: Boolean): List> =
+		subTasks.flatMap { subTask ->
+			when {
+				subTask.state == State.Cancelled -> emptyList()
+				!showInvisible && subTask.isInvisible -> subTask.displaySubTasks(false)
+				else -> listOf(subTask)
+			}
+		}
 }
+
+typealias TaskGenerator = SafeContext.(R) -> Task
+typealias TaskOrNullGenerator = SafeContext.(R) -> Task?
+
+typealias TaskSupplier = SafeContext.() -> Task
+typealias TaskOrNullSupplier = SafeContext.() -> Task?
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/tasks/AcquirePlacedBlockTask.kt b/src/main/kotlin/com/lambda/task/tasks/AcquirePlacedBlockTask.kt
new file mode 100644
index 000000000..5f3f903e1
--- /dev/null
+++ b/src/main/kotlin/com/lambda/task/tasks/AcquirePlacedBlockTask.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.task.tasks
+
+import com.lambda.context.Automated
+import com.lambda.context.SafeContext
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.wrappers.softFail
+import com.lambda.threading.runSafeAutomated
+import com.lambda.util.BlockUtils.item
+import net.minecraft.block.Block
+import net.minecraft.util.math.BlockPos
+
+@Ta5kBuilder
+context(automated: Automated)
+fun acquirePlacedBlock(
+	block: Block,
+	maxSearchRadius: Int = 10
+) = AcquirePlacedBlockTask(block, maxSearchRadius, automated)
+
+class AcquirePlacedBlockTask @Ta5kBuilder internal constructor(
+	val block: Block,
+	val searchRadius: Int = 10,
+	automated: Automated
+) : Task(), Automated by automated {
+	override val name get() = "Acquiring placed ${block.name.string}"
+
+	init {
+		listen {
+			runSafeAutomated {
+				val slot = findSlot(block.item.select(), ContainerSelection.HOTBAR_AND_INVENTORY)
+					?: run {
+						failure("No $block found in range or hotbar/inventory!")
+						return@listen
+					}
+
+				placeContainer(slot)
+					.onSuccess { success(it) }
+					.start()
+			}
+		}
+	}
+
+	override fun SafeContext.onStart() {
+		findBlock(block, searchRadius)
+			.softFail()
+			.start()
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/tasks/AcquireMaterialTask.kt b/src/main/kotlin/com/lambda/task/tasks/AcquireStackTask.kt
similarity index 53%
rename from src/main/kotlin/com/lambda/task/tasks/AcquireMaterialTask.kt
rename to src/main/kotlin/com/lambda/task/tasks/AcquireStackTask.kt
index e55d2fa9d..4a24c38f2 100644
--- a/src/main/kotlin/com/lambda/task/tasks/AcquireMaterialTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/AcquireStackTask.kt
@@ -19,15 +19,25 @@ package com.lambda.task.tasks
 
 import com.lambda.context.Automated
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.ContainerHandler
-import com.lambda.interaction.handlers.ContainerHandler.findContainerWithMaterial
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.slotsAvailable
 import com.lambda.task.Task
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.Task.Ta5kBuilder
 import com.lambda.threading.runSafeAutomated
 
-class AcquireMaterialTask @Ta5kBuilder constructor(
+@Ta5kBuilder
+context(automated: Automated)
+fun acquireStack(selection: StackSelection) =
+    AcquireStackTask(selection, automated)
+
+@Ta5kBuilder
+context(automated: Automated)
+fun acquireStack(selection: () -> StackSelection) =
+    AcquireStackTask(selection(), automated)
+
+class AcquireStackTask @Ta5kBuilder internal constructor(
     val selection: StackSelection,
     automated: Automated
 ) : Task(), Automated by automated {
@@ -36,18 +46,13 @@ class AcquireMaterialTask @Ta5kBuilder constructor(
 
     override fun SafeContext.onStart() {
         runSafeAutomated {
-            selection.findContainerWithMaterial()
-                ?.transferByTask(selection, HotbarContainer)
-                ?.thenAction {
-                    success(selection)
-                }?.execute(this@AcquireMaterialTask)
-                ?: failure(ContainerHandler.NoContainerFound(selection)) // ToDo: Create crafting path
+            if (slotsAvailable(selection, HotbarContainer.select())) {
+                success(selection)
+                return@runSafeAutomated
+            }
+            selection.transfer(toSelection = HotbarContainer.select())
+                .onSuccess { result -> success(result.stackSelection) }
+                .start()
         }
     }
-
-    companion object {
-        @Ta5kBuilder
-        fun Automated.acquire(selection: () -> StackSelection) =
-            AcquireMaterialTask(selection(), this)
-    }
 }
diff --git a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt
index c2abfdfa2..fa3ac2493 100644
--- a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt
@@ -17,8 +17,6 @@
 
 package com.lambda.task.tasks
 
-import baritone.api.pathing.goals.GoalBlock
-import com.lambda.Lambda.LOG
 import com.lambda.config.blocks.EatConfig.Companion.reasonEating
 import com.lambda.context.Automated
 import com.lambda.context.AutomatedSafeContext
@@ -29,8 +27,8 @@ import com.lambda.event.listener.SafeListener.Companion.listen
 import com.lambda.interaction.construction.blueprint.Blueprint
 import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
 import com.lambda.interaction.construction.blueprint.PropagatingBlueprint
-import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
 import com.lambda.interaction.construction.blueprint.TickingBlueprint
+import com.lambda.interaction.construction.blueprint.toBlueprint
 import com.lambda.interaction.construction.simulation.BuildGoal
 import com.lambda.interaction.construction.simulation.Simulation.Companion.simulation
 import com.lambda.interaction.construction.simulation.context.BuildContext
@@ -46,14 +44,14 @@ import com.lambda.interaction.construction.simulation.result.results.InteractRes
 import com.lambda.interaction.construction.simulation.result.results.PreSimResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
-import com.lambda.interaction.managers.interacting.InteractRequest.Companion.interactRequest
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.container.SlotIdentity
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.manager.managers.breaking.breakRequest
+import com.lambda.interaction.manager.managers.interacting.interactRequest
 import com.lambda.module.modules.client.Client
 import com.lambda.task.Task
-import com.lambda.task.tasks.EatTask.Companion.eat
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.wrappers.softFail
 import com.lambda.threading.runConcurrent
 import com.lambda.threading.runSafe
 import com.lambda.threading.runSafeAutomated
@@ -62,13 +60,13 @@ import com.lambda.util.EntityUtils.getClosestPointTo
 import com.lambda.util.EntityUtils.getPositionsWithinBox
 import com.lambda.util.FormattingUtils.format
 import com.lambda.util.extension.Structure
-import com.lambda.util.extension.playerSlots
 import com.lambda.util.math.dist
-import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.runBlocking
 import net.minecraft.block.BlockState
 import net.minecraft.entity.ItemEntity
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
 import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket
 import net.minecraft.network.packet.s2c.play.ChunkDeltaUpdateS2CPacket
 import net.minecraft.network.packet.s2c.play.EntityPositionS2CPacket
@@ -79,314 +77,329 @@ import java.util.concurrent.ConcurrentLinkedQueue
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.math.sqrt
 
-class BuildTask private constructor(
-    private val blueprint: Blueprint,
-    private val finishOnDone: Boolean,
-    private val collectDrops: Boolean,
-    private val lifeMaintenance: Boolean,
-    private val async: Boolean,
-    automated: Automated,
-    private val buildResultFilter: SafeContext.(BuildResult) -> Boolean,
-) : Task(), Automated by automated {
-    override val name: String get() = "Building $blueprint with ${(breaks / (age / 20.0 + 0.001)).format(precision = 1)} b/s ${(placements / (age / 20.0 + 0.001)).format(precision = 1)} p/s"
-
-    private val pendingInteractions = ConcurrentLinkedQueue()
-    private val atMaxPendingInteractions
-        get() = pendingInteractions.size >= buildConfig.maxPendingActions
-
-    private var placements = 0
-    private var breaks = 0
-    private val dropsToCollect = mutableSetOf()
-    var eatTask: Task<*>? = null
-
-    private val onItemDrop: ((item: ItemEntity) -> Unit)?
-        get() = if (collectDrops) { item ->
-            dropsToCollect.add(item)
-        } else null
-
-    private var firstSim = true
-    private var job: Job? = null
-    private var results: Collection = mutableSetOf()
-    private var reSimPositions = Collections.synchronizedSet(linkedSetOf())
-    private var viableResults: Sequence = emptySequence()
-
-    override fun SafeContext.onStart() {
-        iteratePropagating()
-    }
-
-    init {
-        listen {
-            if (!firstSim || !async) {
-                if (checkEmpty() || !async) return@listen
-            }
-            firstSim = false
-            runSafeAutomated {
-                if (preSim()) {
-                    job = null
-                    return@listen
-                }
-                startSimulation()
-            }
-        }
-
-        listen {
-            runSafeAutomated {
-                if (async) {
-                    if (job != null) {
-                        runBlocking { job?.join(); job = null }
-                        results =
-                            results.filter { it.pos !in reSimPositions } +
-                                    blueprint.structure.filter {
-                                        it.key in reSimPositions
-                                    }.sim()
-                        reSimPositions.clear()
-                        setViableResults()
-                        processResults()
-                    }
-                    return@listen
-                }
-
-                if (preSim()) return@listen
-                simulate()
-                setViableResults()
-                processResults()
-            }
-        }
-
-        listen { event ->
-            if (!async) return@listen
-            runSafe {
-                when (val packet = event.packet) {
-                    is PlayerPositionLookS2CPacket -> {
-                        job?.cancel(CancellationException("Player position moved, all simulations are compromised."))
-                        job = null
-                    }
-                    is BlockUpdateS2CPacket -> checkReSims(packet.pos, packet.state)
-                    is ChunkDeltaUpdateS2CPacket -> packet.visitUpdates { pos, state -> checkReSims(pos.toImmutable(), state) }
-                    is EntityPositionS2CPacket -> {
-                        val entity = world.getEntityById(packet.entityId) ?: return@listen
-                        val currentBox = entity.boundingBox
-                        val maxDist = buildConfig.blockReach + sqrt(3.0)
-                        val eyePos = player.eyePos
-                        val newBox = currentBox.offset(packet.change.position.subtract(entity.pos))
-                        if (currentBox.getClosestPointTo(eyePos) dist eyePos > maxDist &&
-                            newBox.getClosestPointTo(eyePos) dist eyePos > maxDist) return@listen
-                        reSimPositions.addAll(currentBox.getPositionsWithinBox() + newBox.getPositionsWithinBox())
-                    }
-                }
-            }
-        }
-    }
-
-    private fun SafeContext.checkReSims(pos: BlockPos, state: BlockState) {
-        val currentState = blockState(pos)
-        if (currentState === state) return
-        val pos = pos
-        reSimPositions.addAll(
-            arrayOf(
-                pos,
-                pos.up(),
-                pos.down(),
-                pos.north(),
-                pos.south(),
-                pos.east(),
-                pos.west()
-            )
-        )
-    }
-
-    private fun AutomatedSafeContext.startSimulation() {
-        if (job != null) return
-        job = runConcurrent { simulate() }
-    }
-
-    private fun AutomatedSafeContext.preSim(): Boolean {
-        when {
-            lifeMaintenance && eatTask == null && runSafeAutomated { reasonEating() }.shouldEat() -> {
-                eatTask = eat()
-                    .thenAction { eatTask = null }
-                    .execute(this@BuildTask)
-
-                return true
-            }
-            eatTask != null -> return true
-        }
-
-        if (blueprint is TickingBlueprint) {
-            blueprint.tick()
-                ?: run {
-                    failure("Failed to tick the ticking blueprint")
-                    return true
-                }
-        }
-
-        return collectDrops()
-    }
-
-    private fun AutomatedSafeContext.simulate() {
-        results = blueprint.structure.sim()
-    }
-
-    private fun SafeContext.setViableResults() {
-        viableResults = results
-            .asSequence()
-            .filter { result ->
-                val finalResult = (result as? Dependent)?.lastDependency ?: result
-                pendingInteractions.none {
-                    it.blockPos == finalResult.pos
-                } && (finalResult !is Contextual || finalResult.context.canUse())
-            }
-            .filter { buildResultFilter(it) }
-            .sorted()
-    }
-
-    private fun AutomatedSafeContext.processResults() {
-        Client.drawables = results
-            .filterIsInstance()
-            .plus(pendingInteractions.toList())
-            .toList()
-
-        val bestResult = viableResults.firstOrNull() ?: return
-        handleResult(bestResult, viableResults)
-    }
-
-    private fun checkEmpty(): Boolean {
-        if (finishOnDone && blueprint.structure.isEmpty()) {
-            failure("Structure is empty")
-            return true
-        }
-        return false
-    }
-
-    private fun AutomatedSafeContext.handleResult(result: BuildResult, allResults: Sequence) {
-        if (result !is Dependent && result !is Contextual && pendingInteractions.isNotEmpty()) return
-
-        when (result) {
-            is PreSimResult.Done,
-            is PreSimResult.Unbreakable,
-            is PreSimResult.Restricted,
-            is PreSimResult.NoPermission,
-            is GenericResult.Ignored -> {
-                if (iteratePropagating()) return
-                if (finishOnDone) success(blueprint.structure)
-            }
-
-            is GenericResult.NotVisible,
-            is InteractResult.NoIntegrity -> {
-                if (!buildConfig.pathing) return
-                val sim = blueprint.simulation()
-                val goal = BuildGoal(sim, player.blockPos)
-                BaritoneHandler.setGoalAndPath(goal)
-            }
-
-            is Navigable -> {
-                if (buildConfig.pathing) BaritoneHandler.setGoalAndPath(result.goal ?: return)
-            }
-
-            is Contextual -> {
-                if (atMaxPendingInteractions) return
-                when (result) {
-                    is BreakResult.Break ->
-                        allResults.breakRequest(pendingInteractions) {
-                            onStop { breaks++ }
-                            onItemDrop?.let { onItemDrop ->
-                                onItemDrop { onItemDrop(it) }
-                            }
-                        }?.submit()
-
-                    is InteractResult.Interact -> {
-	                    allResults.interactRequest(pendingInteractions, false) {
-		                    onPlace { placements++ }
-	                    }?.submit()
-                    }
-                }
-            }
-
-            is Dependent -> handleResult(result.lastDependency, allResults)
-
-            is Resolvable -> {
-	            LOG.info("Resolving: ${result.name}")
-                result.resolve()
-            }
-        }
-    }
-
-    private fun SafeContext.collectDrops() =
-        dropsToCollect
-            .firstOrNull()
-            ?.let { itemDrop ->
-                if (pendingInteractions.isNotEmpty()) return@let true
-
-                if (!world.entities.contains(itemDrop)) {
-                    dropsToCollect.remove(itemDrop)
-                    BaritoneHandler.cancel()
-                    return@let true
-                }
-
-                if (player.hotbarAndInventoryStacks.none { it.isEmpty }) {
-                    val stackToThrow = player.currentScreenHandler.playerSlots.firstOrNull {
-                        it.stack.item in inventoryConfig.disposables
-                    } ?: run {
-                        failure("No item in inventory to throw but inventory is full and cant pick up item drop")
-                        return@let true
-                    }
-                    inventoryRequest {
-                        throwStack(stackToThrow.id)
-                    }.submit()
-                    return@let true
-                }
-
-                BaritoneHandler.setGoalAndPath(GoalBlock(itemDrop.blockPos))
-                true
-            } ?: false
-
-    fun iteratePropagating() =
-        if (blueprint is PropagatingBlueprint) {
-            blueprint.next() ?: failure("Failed to propagate the next blueprint")
-            true
-        } else false
-
-    companion object {
-        @Ta5kBuilder
-        fun Automated.build(
-            finishOnDone: Boolean = true,
-            collectDrops: Boolean = buildConfig.collectDrops,
-            lifeMaintenance: Boolean = false,
-            async: Boolean = false,
-            buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
-            blueprint: () -> Blueprint
-        ) = BuildTask(blueprint(), finishOnDone, collectDrops, lifeMaintenance, async, this, buildResultFilter)
-
-        @Ta5kBuilder
-        context(automated: Automated)
-        fun Structure.build(
-            finishOnDone: Boolean = true,
-            collectDrops: Boolean = automated.buildConfig.collectDrops,
-            lifeMaintenance: Boolean = false,
-            async: Boolean = false,
-            buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
-        ) = BuildTask(toBlueprint(), finishOnDone, collectDrops, lifeMaintenance, async, automated, buildResultFilter)
-
-        @Ta5kBuilder
-        context(automated: Automated)
-        fun Blueprint.build(
-            finishOnDone: Boolean = true,
-            collectDrops: Boolean = automated.buildConfig.collectDrops,
-            lifeMaintenance: Boolean = false,
-            async: Boolean = false,
-            buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
-        ) = BuildTask(this, finishOnDone, collectDrops, lifeMaintenance, async, automated, buildResultFilter)
-
-        @Ta5kBuilder
-        fun Automated.breakAndCollectBlock(
-            blockPos: BlockPos,
-            finishOnDone: Boolean = true,
-            lifeMaintenance: Boolean = false,
-            async: Boolean = false,
-            buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
-        ) = BuildTask(
-            blockPos.toStructure(TargetState.Empty).toBlueprint(),
-            finishOnDone, true, lifeMaintenance, async, this, buildResultFilter
-        )
-    }
+@Ta5kBuilder
+fun Automated.build(
+	finishOnDone: Boolean = true,
+	collectDrops: Boolean = buildConfig.collectDrops,
+	lifeMaintenance: Boolean = false,
+	async: Boolean = false,
+	buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
+	blueprint: () -> Blueprint
+) = BuildTask(blueprint(), finishOnDone, collectDrops, lifeMaintenance, async, this, buildResultFilter)
+
+@Ta5kBuilder
+context(automated: Automated)
+fun Structure.build(
+	finishOnDone: Boolean = true,
+	collectDrops: Boolean = automated.buildConfig.collectDrops,
+	lifeMaintenance: Boolean = false,
+	async: Boolean = false,
+	buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
+) = BuildTask(toBlueprint(), finishOnDone, collectDrops, lifeMaintenance, async, automated, buildResultFilter)
+
+@Ta5kBuilder
+context(automated: Automated)
+fun Blueprint.build(
+	finishOnDone: Boolean = true,
+	collectDrops: Boolean = automated.buildConfig.collectDrops,
+	lifeMaintenance: Boolean = false,
+	async: Boolean = false,
+	buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
+) = BuildTask(this, finishOnDone, collectDrops, lifeMaintenance, async, automated, buildResultFilter)
+
+@Ta5kBuilder
+context(automated: Automated)
+fun breakAndCollect(
+	blockPos: BlockPos,
+	finishOnDone: Boolean = true,
+	lifeMaintenance: Boolean = false,
+	async: Boolean = false,
+	buildResultFilter: SafeContext.(BuildResult) -> Boolean = { true },
+) = BuildTask(
+	blockPos.toStructure(TargetState.Empty).toBlueprint(),
+	finishOnDone, true, lifeMaintenance, async, automated, buildResultFilter
+)
+
+data class BuildOutcome(
+	val structure: Structure,
+	val pickups: List,
+	val pickupTypes: Set
+)
+
+data class BuildStatus(
+	val pickedUpTypes: Set,
+	val dropsPending: List
+)
+
+class BuildTask @Ta5kBuilder internal constructor(
+	private val blueprint: Blueprint,
+	private val finishOnDone: Boolean,
+	private val collectDrops: Boolean,
+	private val lifeMaintenance: Boolean,
+	private val async: Boolean,
+	automated: Automated,
+	private val buildResultFilter: SafeContext.(BuildResult) -> Boolean,
+) : Task(), Automated by automated {
+	override val name: String get() = "Building $blueprint with ${(breaks / (age / 20.0 + 0.001)).format(precision = 1)} b/s ${(placements / (age / 20.0 + 0.001)).format(precision = 1)} p/s"
+
+	private val pendingInteractions = ConcurrentLinkedQueue()
+	private val atMaxPendingInteractions
+		get() = pendingInteractions.size >= buildConfig.maxPendingActions
+
+	private var placements = 0
+	private var breaks = 0
+	private val dropsToCollect = mutableSetOf()
+	private val pickups = mutableListOf()
+	private val pickupTypes = mutableSetOf()
+	private val preTickCallbacks = mutableListOf Boolean>()
+
+	private val onItemDrop: ((item: ItemEntity) -> Unit)?
+		get() = if (collectDrops) { item ->
+			dropsToCollect.add(item)
+		} else null
+
+	private var job: Job? = null
+	private var results: Collection = mutableSetOf()
+	private var reSimPositions = Collections.synchronizedSet(hashSetOf())
+	private var viableResults: Sequence = emptySequence()
+
+	override fun SafeContext.onStart() {
+		iteratePropagating()
+	}
+
+	@Ta5kBuilder
+	fun onPreTick(callback: context(BuildTask) AutomatedSafeContext.(BuildStatus) -> Boolean) =
+		apply { preTickCallbacks.add(callback) }
+
+	init {
+		onCompletion { BaritoneHandler.cancel() }
+
+		listen {
+			if (checkEmpty() || !async) return@listen
+			runSafeAutomated {
+				if (preSim()) return@listen
+				startSimulation()
+			}
+		}
+
+		listen {
+			runSafeAutomated {
+				if (async) {
+					if (job != null) {
+						runBlocking { job?.join(); job = null }
+						results = results.filter { it.pos !in reSimPositions } +
+								blueprint.structure.filter { it.key in reSimPositions }.sim()
+						reSimPositions.clear()
+						setViableResults()
+						processResults()
+					}
+					return@listen
+				}
+
+				if (preSim()) return@listen
+				simulate()
+				setViableResults()
+				processResults()
+			}
+		}
+
+		listen { event ->
+			if (!async) return@listen
+			runSafe {
+				when (val packet = event.packet) {
+					is PlayerPositionLookS2CPacket -> {
+						job?.cancel(CancellationException("Player position moved, all simulations are compromised."))
+						job = null
+					}
+					is BlockUpdateS2CPacket -> checkReSims(packet.pos, packet.state)
+					is ChunkDeltaUpdateS2CPacket -> packet.visitUpdates { pos, state -> checkReSims(pos.toImmutable(), state) }
+					is EntityPositionS2CPacket -> {
+						val entity = world.getEntityById(packet.entityId) ?: return@listen
+						val currentBox = entity.boundingBox
+						val maxDist = buildConfig.blockReach + sqrt(3.0)
+						val eyePos = player.eyePos
+						val newBox = currentBox.offset(packet.change.position.subtract(entity.pos))
+						if (currentBox.getClosestPointTo(eyePos) dist eyePos > maxDist &&
+							newBox.getClosestPointTo(eyePos) dist eyePos > maxDist
+						) return@listen
+						reSimPositions.addAll(currentBox.getPositionsWithinBox() + newBox.getPositionsWithinBox())
+					}
+				}
+			}
+		}
+	}
+
+	private fun SafeContext.checkReSims(pos: BlockPos, state: BlockState) {
+		val currentState = blockState(pos)
+		if (currentState === state) return
+		val pos = pos
+		reSimPositions.addAll(
+			arrayOf(
+				pos,
+				pos.up(),
+				pos.down(),
+				pos.north(),
+				pos.south(),
+				pos.east(),
+				pos.west()
+			)
+		)
+	}
+
+	private fun AutomatedSafeContext.startSimulation() {
+		if (job != null) return
+		job = runConcurrent { simulate() }
+	}
+
+	private fun AutomatedSafeContext.preSim(): Boolean {
+		if (runPreTick()) return true
+		if (lifeMaintenance && runSafeAutomated { reasonEating() }.shouldEat()) {
+			eat().start()
+			return true
+		}
+
+		if (blueprint is TickingBlueprint) {
+			blueprint.tick()
+				?: run {
+					failure("Failed to tick the ticking blueprint")
+					return true
+				}
+		}
+
+		return collectDrops()
+	}
+
+	private fun AutomatedSafeContext.runPreTick(): Boolean {
+		if (preTickCallbacks.isEmpty()) return false
+		val status =
+			BuildStatus(
+				pickedUpTypes = pickupTypes,
+				dropsPending = dropsToCollect.map { it.stack.copy() }
+			)
+		preTickCallbacks.forEach { callback ->
+			val veto = runCatching { callback(status) }
+				.getOrElse {
+					failure(it)
+					return true
+				}
+			if (veto) return true
+		}
+		return false
+	}
+
+	private fun SafeContext.currentPickups(): List {
+		if (pickups.isEmpty()) return emptyList()
+		val latest = LinkedHashMap()
+		pickups.forEach { pickup -> pickup.slot?.let { latest[it] = pickup.stack } }
+		return latest.entries
+			.filter { (slot, stack) -> slot.holds(stack) }
+			.map { (slot, stack) -> PickedUpItem(stack.copy(), slot) }
+	}
+
+	private fun AutomatedSafeContext.simulate() {
+		results = blueprint.structure.sim()
+	}
+
+	private fun SafeContext.setViableResults() {
+		viableResults = results
+			.asSequence()
+			.filter { result ->
+				val finalResult = (result as? Dependent)?.lastDependency ?: result
+				pendingInteractions.none {
+					it.blockPos == finalResult.pos
+				} && (finalResult !is Contextual || finalResult.context.canUse())
+			}
+			.filter { buildResultFilter(it) }
+			.sorted()
+	}
+
+	private fun AutomatedSafeContext.processResults() {
+		Client.drawables = results
+			.filterIsInstance()
+			.plus(pendingInteractions.toList())
+			.toList()
+
+		val bestResult = viableResults.firstOrNull() ?: return
+		handleResult(bestResult, viableResults)
+	}
+
+	private fun checkEmpty(): Boolean {
+		if (finishOnDone && blueprint.structure.isEmpty()) {
+			failure("Structure is empty")
+			return true
+		}
+		return false
+	}
+
+	private fun AutomatedSafeContext.handleResult(result: BuildResult, allResults: Sequence) {
+		if (result !is Dependent && result !is Contextual && pendingInteractions.isNotEmpty()) return
+
+		when (result) {
+			is PreSimResult.Done,
+			is PreSimResult.Unbreakable,
+			is PreSimResult.Restricted,
+			is PreSimResult.NoPermission,
+			is GenericResult.Ignored -> {
+				if (iteratePropagating()) return
+				if (finishOnDone) success(BuildOutcome(blueprint.structure, currentPickups(), pickupTypes))
+			}
+
+			is GenericResult.NotVisible,
+			is InteractResult.NoIntegrity -> {
+				if (!buildConfig.pathing) return
+				val sim = blueprint.simulation()
+				val goal = BuildGoal(sim, player.blockPos)
+				BaritoneHandler.setGoalAndPath(goal)
+			}
+
+			is Navigable -> {
+				if (buildConfig.pathing) BaritoneHandler.setGoalAndPath(result.goal ?: return)
+			}
+
+			is Contextual -> {
+				if (atMaxPendingInteractions) return
+				when (result) {
+					is BreakResult.Break ->
+						allResults.breakRequest(pendingInteractions) {
+							onStop { breaks++ }
+							onItemDrop?.let { onItemDrop ->
+								onItemDrop { onItemDrop(it) }
+							}
+						}?.submit()
+
+					is InteractResult.Interact -> {
+						allResults.interactRequest(pendingInteractions, false) {
+							onPlace { placements++ }
+						}?.submit()
+					}
+				}
+			}
+
+			is Dependent -> handleResult(result.lastDependency, allResults)
+
+			is Resolvable -> {
+				result.resolve()
+					.softFail()
+					.start()
+			}
+		}
+	}
+
+	private fun AutomatedSafeContext.collectDrops(): Boolean {
+		if (dropsToCollect.isEmpty()) return false
+		if (pendingInteractions.isNotEmpty()) return true
+		collectDrops(dropsToCollect)
+			.onSuccess {
+				pickups.addAll(it)
+				pickupTypes.addAll(it.map { it.stack.item })
+			}
+			.start()
+		return true
+	}
+
+	private fun iteratePropagating() =
+		if (blueprint is PropagatingBlueprint) {
+			blueprint.next() ?: failure("Failed to propagate the next blueprint")
+			true
+		} else false
 }
diff --git a/src/main/kotlin/com/lambda/task/tasks/CollectDropsTask.kt b/src/main/kotlin/com/lambda/task/tasks/CollectDropsTask.kt
new file mode 100644
index 000000000..f682fd884
--- /dev/null
+++ b/src/main/kotlin/com/lambda/task/tasks/CollectDropsTask.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.task.tasks
+
+import baritone.api.pathing.goals.GoalBlock
+import com.lambda.context.Automated
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.event.events.PacketEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.interaction.container.SlotIdentity
+import com.lambda.interaction.container.identity
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.handler.handlers.ContainerHandler.stacksAvailable
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.threading.runSafeAutomated
+import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
+import net.minecraft.entity.ItemEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.network.packet.s2c.play.ItemPickupAnimationS2CPacket
+import net.minecraft.screen.slot.Slot
+import net.minecraft.util.math.BlockPos
+import java.util.concurrent.ConcurrentHashMap
+
+@Ta5kBuilder
+context(automated: Automated)
+fun collectDrops(drops: MutableSet) = CollectDropsTask(drops, automated)
+
+data class PickedUpItem(
+	val stack: ItemStack,
+	val slot: SlotIdentity?
+)
+
+class CollectDropsTask @Ta5kBuilder internal constructor(
+	private val drops: MutableSet,
+	automated: Automated
+) : Task>(), Automated by automated {
+	override val name get() = "Collecting ${drops.size} item drop(s)"
+
+	private val pickups = mutableListOf()
+	private var lastSeen: Map? = null
+	private var lastSeenSync: Int? = null
+	private val attributed = mutableMapOf()
+	private val closingAge = mutableMapOf()
+	private val dropStacks = mutableMapOf()
+	private val packets = ConcurrentHashMap()
+	private val lost = mutableMapOf()
+	private var selfId = -1
+	private var lastGoal: BlockPos? = null
+
+	init {
+		onCompletion { BaritoneHandler.cancel() }
+
+		listen { event ->
+			val packet = event.packet as? ItemPickupAnimationS2CPacket ?: return@listen
+			packets[packet.entityId] = packet.collectorEntityId
+		}
+
+		listen {
+			runSafeAutomated {
+				pollNextItem()
+			}
+		}
+	}
+
+	private fun AutomatedSafeContext.pollNextItem() {
+		val live = liveStacks()
+		val syncId = player.currentScreenHandler.syncId
+		selfId = player.id
+		drops.forEach { stackOf(it) }
+		val previous = lastSeen
+		if (previous != null && lastSeenSync == syncId) {
+			attributeGrowth(previous, live)
+		}
+		ageClosing()
+		lastSeen = live
+		lastSeenSync = syncId
+
+		val gone = drops.filter { !world.entities.contains(it) }
+		gone.forEach { drop ->
+			if (isTakenByOther(drop)) lost[drop] = packets[drop.id]
+			else if (remainderOf(drop) > 0) closingAge.putIfAbsent(drop, 0)
+		}
+		if (gone.isNotEmpty()) {
+			drops.removeAll(gone.toSet())
+			BaritoneHandler.cancel()
+		}
+
+		val target = drops.firstOrNull()
+		if (target == null) {
+			if (closingAge.isEmpty()) {
+				BaritoneHandler.cancel()
+				if (lost.isEmpty()) success(pickups.toList())
+				else failure(describeLost())
+			}
+			return
+		}
+
+		val emptySlotAvailable =
+			stacksAvailable(
+				stackSelection(StackSelection.UNBOUNDED) {
+					isEmpty()
+				},
+				ContainerSelection.HOTBAR_AND_INVENTORY
+			)
+		if (!emptySlotAvailable) {
+			val slotToThrow =
+				findSlot(
+					stackSelection { ofAnyItems(inventoryConfig.disposables) },
+					ContainerSelection.HOTBAR_AND_INVENTORY,
+					sorted = false
+				) ?: run {
+					failure("Inventory is full and no disposable items to throw")
+					return
+				}
+
+			inventoryRequest {
+				throwStack(slotToThrow.id)
+			}.submit()
+			return
+		}
+
+		if (target.blockPos != lastGoal) {
+			lastGoal = target.blockPos
+			BaritoneHandler.setGoalAndPath(GoalBlock(target.blockPos))
+		}
+	}
+
+	private fun AutomatedSafeContext.liveStacks() =
+		player.hotbarAndInventorySlots.associateWith { it.stack.copy() }
+
+	private fun stackOf(drop: ItemEntity): ItemStack {
+		val live = drop.stack
+		val snap = dropStacks[drop]
+		if (live.isEmpty) return snap ?: ItemStack.EMPTY
+		if (snap == null || !ItemStack.areItemsAndComponentsEqual(live, snap) || live.count > snap.count) {
+			dropStacks[drop] = live.copy()
+		}
+		return dropStacks[drop] ?: ItemStack.EMPTY
+	}
+
+	private fun remainderOf(drop: ItemEntity) = stackOf(drop).count - (attributed[drop] ?: 0)
+
+	private fun attributeGrowth(before: Map, live: Map) {
+		live.forEach { (slot, now) ->
+			if (now.isEmpty) return@forEach
+			val was = before[slot]?.takeIf { ItemStack.areItemsAndComponentsEqual(it, now) }?.count ?: 0
+			if (now.count <= was) return@forEach
+			consumeGrowth(now, now.count - was, slot)
+		}
+	}
+
+	private fun isTakenByOther(drop: ItemEntity) = packets[drop.id]?.let { it != selfId } == true
+
+	private fun consumeGrowth(stack: ItemStack, count: Int, slot: Slot) {
+		var left = count
+		for (drop in closingAge.keys.toList()) {
+			if (left <= 0) break
+			left -= takeFrom(drop, stack, left, slot)
+		}
+		if (left <= 0) return
+		drops.forEach { drop ->
+			if (left <= 0) return
+			left -= takeFrom(drop, stack, left, slot)
+		}
+	}
+
+	private fun takeFrom(drop: ItemEntity, stack: ItemStack, left: Int, slot: Slot): Int {
+		if (isTakenByOther(drop)) return 0
+		if (!matchesDrop(stack, stackOf(drop))) return 0
+		val take = minOf(left, remainderOf(drop))
+		if (take <= 0) return 0
+		pickups += PickedUpItem(stack.copyWithCount(take), slot.identity())
+		attributed[drop] = (attributed[drop] ?: 0) + take
+		if (remainderOf(drop) <= 0) closingAge.remove(drop)
+		return take
+	}
+
+	private fun matchesDrop(stack: ItemStack, snap: ItemStack): Boolean {
+		if (stack.isEmpty || snap.isEmpty) return false
+		return stack.item == snap.item && stack.name.string == snap.name.string
+	}
+
+	private fun ageClosing() {
+		closingAge.entries.toList().forEach { (drop, age) ->
+			if (age < CLOSE_GRACE_TICKS) closingAge[drop] = age + 1
+			else {
+				val collector = packets.remove(drop.id)
+				val left = remainderOf(drop)
+				if (collector == selfId) {
+					if (left > 0) pickups += PickedUpItem(stackOf(drop).copyWithCount(left), null)
+					attributed.remove(drop)
+				} else {
+					lost[drop] = collector
+				}
+				closingAge.remove(drop)
+			}
+		}
+	}
+
+	private fun AutomatedSafeContext.describeLost(): String {
+		val parts = lost.map { (drop, collector) ->
+			val what = stackOf(drop).let { "${remainderOf(drop)}x ${it.name.string}" }
+			val who = collector?.let { id -> world.getEntityById(id)?.name?.string } ?: "unknown causes"
+			"$what lost to $who"
+		}
+		return "Failed to collect ${lost.size} drop(s): ${parts.joinToString("; ")}"
+	}
+
+	private companion object {
+		const val CLOSE_GRACE_TICKS = 20
+	}
+}
diff --git a/src/main/kotlin/com/lambda/task/tasks/ContainerTransferTask.kt b/src/main/kotlin/com/lambda/task/tasks/ContainerTransferTask.kt
index 02dede417..975e95a11 100644
--- a/src/main/kotlin/com/lambda/task/tasks/ContainerTransferTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/ContainerTransferTask.kt
@@ -18,92 +18,582 @@
 package com.lambda.task.tasks
 
 import com.lambda.context.Automated
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
 import com.lambda.event.EventFlow.post
 import com.lambda.event.events.ContainerEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
-import com.lambda.interaction.material.StackSelection
-import com.lambda.interaction.material.container.ExternalContainer
-import com.lambda.interaction.material.container.MaterialContainer
-import com.lambda.interaction.material.container.containers.InventoryContainer
+import com.lambda.interaction.container.Container
+import com.lambda.interaction.container.ExternalContainer
+import com.lambda.interaction.container.containers.CursorContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.ContainerSelection
+import com.lambda.interaction.container.selection.StackSelection
+import com.lambda.interaction.container.selection.mutate
+import com.lambda.interaction.container.selection.selectContainers
+import com.lambda.interaction.container.selection.stackSelection
+import com.lambda.interaction.handler.handlers.ContainerHandler.findContainers
+import com.lambda.interaction.handler.handlers.PacketLimitHandler.availablePackets
+import com.lambda.interaction.handler.handlers.PacketType
+import com.lambda.interaction.manager.managers.inventory.InvRequestFailureReason
+import com.lambda.interaction.manager.managers.inventory.inventoryRequest
 import com.lambda.task.Task
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.tasks.wrappers.taskOrNull
+import com.lambda.task.tasks.wrappers.then
+import com.lambda.task.tasks.wrappers.thenOrNull
 import com.lambda.threading.runSafeAutomated
+import com.lambda.util.item.ItemStackUtils.count
+import com.lambda.util.item.ItemStackUtils.equal
+import com.lambda.util.item.ItemStackUtils.hasSpace
+import com.lambda.util.item.StackMovePlanner
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.screen.slot.Slot
 
-class ContainerTransferTask(
-	private var fromContainer: MaterialContainer,
-	private val toContainer: MaterialContainer,
-	private val stackSelection: StackSelection,
-	automated: Automated,
-	private val failIfNoMaterial: Boolean = false
-) : Task(), Automated by automated {
-	override val name = "Transferring $stackSelection from $fromContainer to $toContainer"
-
-	private var delegateTask: Task<*>? = null
-
-	init {
-		listen {
-			if (delegateTask?.isCompleted == true) {
-				success()
-				return@listen
-			}
-			runSafeAutomated {
-				val fromExternal = fromContainer as? ExternalContainer
-				val slots = fromContainer.slots
-				val toSlots = toContainer.slots
-				fromExternal.takeIf { slots.isEmpty() }?.let { fromExternal ->
-					if (toContainer is ExternalContainer && toSlots.isEmpty()) {
-						fromContainer.transferByTask(stackSelection, InventoryContainer)
-							.thenAction { fromContainer = InventoryContainer }
-							.execute(this@ContainerTransferTask)
-						return@listen
+@Ta5kBuilder
+context(automated: Automated)
+fun transfer(
+	fromStack: StackSelection,
+	fromSelection: ContainerSelection = ContainerSelection.ACCESSED,
+	toSelection: ContainerSelection,
+	toStack: StackSelection = StackSelection.EVERYTHING,
+	allowQuickMove: Boolean = true,
+) = ContainerTransferTask(fromStack, fromSelection, toSelection, toStack, allowQuickMove, automated)
+
+@Ta5kBuilder
+@JvmName("transferExt")
+context(automated: Automated)
+fun StackSelection.transfer(
+	fromSelection: ContainerSelection = ContainerSelection.ACCESSED,
+	toSelection: ContainerSelection,
+	toStack: StackSelection = StackSelection.EVERYTHING,
+	allowQuickMove: Boolean = true,
+) = transfer(this, fromSelection, toSelection, toStack, allowQuickMove)
+
+class TransferResult internal constructor(
+	val stackSelection: StackSelection,
+	val containerSelection: ContainerSelection
+)
+
+class ContainerTransferTask @Ta5kBuilder internal constructor(
+	private val fromStack: StackSelection,
+	private val fromSelection: ContainerSelection,
+	private val toSelection: ContainerSelection,
+	private val toStack: StackSelection,
+	private val allowQuickMove: Boolean,
+	automated: Automated
+) : Task(), Automated by automated {
+	override val name = "Transferring $fromStack"
+
+	private val unlimited = fromStack.isUnbounded
+
+	private var transferred = 0
+	private val remaining
+		get() =
+			if (unlimited) Int.MAX_VALUE
+			else (fromStack.count - transferred).coerceAtLeast(0)
+	private val isComplete
+		get() = !unlimited && remaining <= 0
+
+	private var staged = 0
+
+	private var lastSource: Container? = null
+
+	private val progressText
+		get() = "$transferred moved"
+
+	private val resultSlots = mutableMapOf()
+	private val resultContainers = mutableSetOf()
+
+	private val fromQueue = mutableListOf()
+	private val toQueue = mutableListOf()
+
+	private val stagingContainers: List
+		get() = listOf(InventoryContainer, HotbarContainer)
+			.filter { it.isAccessible }
+
+	private val pullLimit
+		get() =
+			if (unlimited) Int.MAX_VALUE
+			else (remaining - staged).coerceAtLeast(0)
+
+	private val pushLimit
+		get() =
+			if (unlimited) staged
+			else minOf(remaining, staged)
+
+	override fun SafeContext.onStart() {
+		fromQueue +=
+			findContainers(
+				fromSelection.mutate {
+					hasStack(fromStack.mutate(1))
+				}
+			)
+		if (fromQueue.isEmpty() || (!unlimited && !(fromStack isIn fromQueue))) {
+			fail("Could not find source containers for $fromStack (${describeState()})")
+			return
+		}
+
+		toQueue += findContainers(toSelection).filter { canReceive(it) }
+		if (toQueue.isEmpty()) {
+			fail("Could not find destination containers with space for $fromStack (${describeState()})")
+			return
+		}
+
+		nextStep()
+	}
+
+	private fun SafeContext.describeState() =
+		"fromQueue=${fromQueue.size} toQueue=${toQueue.size} " +
+			"cursor=${player.currentScreenHandler.cursorStack} screen=${player.currentScreenHandler.syncId}"
+
+	private fun SafeContext.nextStep() {
+		if (isComplete) {
+			success(buildResult())
+			return
+		}
+
+		val from = fromQueue.firstOrNull()
+			?: run {
+				when {
+					unlimited && transferred > 0 -> success(buildResult())
+					toQueue.isEmpty() -> fail("No destination container has space left for $fromStack ($progressText, ${describeState()})")
+					else -> fail("Ran out of source containers for $fromStack ($progressText, ${describeState()})")
+				}
+				return
+			}
+		val to = toQueue.firstOrNull { !it.haveMatchingInventories(from) }
+			?: run {
+				fail("No destination container has space left for $fromStack ($progressText, ${describeState()})")
+				return
+			}
+		lastSource = from
+
+		val prevTransferred = transferred
+		val step =
+			if (from is ExternalContainer && to is ExternalContainer) tripChain(from)
+			else directChain(from, to)
+
+		step
+			.onSuccess {
+				if (isComplete) {
+					success(buildResult())
+					return@onSuccess
+				}
+
+				if (transferred <= prevTransferred) {
+					toQueue.removeAll { it.haveMatchingInventories(to) }
+				}
+				nextStep()
+			}
+			.start()
+	}
+
+	private fun buildResult() =
+		TransferResult(
+			fromStack.mutate {
+				sortedWith {
+					compareByDescending { stackAndSlot ->
+						val slot = stackAndSlot.slot
+						slot != null &&
+							resultSlots.entries.find {
+								slot.index == it.key.index
+							}?.let { moveEntry ->
+								slot.inventory::class == moveEntry.key.inventory::class &&
+										moveEntry.key.stack.equal(moveEntry.value)
+							} == true
+					}
+				}
+			},
+			selectContainers(
+				containers = resultContainers.toTypedArray(),
+				scope = toSelection.scope
+			)
+		)
+
+	private fun directChain(from: Container, to: Container): Task<*> {
+		val toContainers =
+			if (to is ExternalContainer) listOf(to)
+			else listOf(to) +
+					toQueue.filter {
+						it !== to && it !is ExternalContainer &&
+								!it.haveMatchingInventories(from)
 					}
-					delegateTask = fromExternal.accessThen {
-						fromContainer.transferByTask(stackSelection, toContainer)
-					}?.execute(this@ContainerTransferTask) ?: run {
-						checkFail()
-						return@listen
+
+		return taskOrNull { from.access() }.then { fromContext ->
+			taskOrNull { to.access() }.then { toContext ->
+				MoveTask(
+					fromContainers = listOf(from),
+					toContainers = toContainers,
+					fromSelection = fromStack,
+					toSelection = toStack,
+					limit = remaining
+				).thenOrNull {
+					toContainers
+						.filter { !canReceive(it) }
+						.forEach { full ->
+							toQueue.removeAll { it.haveMatchingInventories(full) }
+						}
+					toContext?.close()
+				}.thenOrNull {
+					if (from.count(fromStack) <= 0) {
+						fromQueue.removeAll { it.haveMatchingInventories(from) }
 					}
-					return@listen
-				}
-				if (toContainer is ExternalContainer && toSlots.isEmpty()) {
-					delegateTask = toContainer.accessThen {
-						fromContainer.transferByTask(stackSelection, toContainer)
-					}?.execute(this@ContainerTransferTask) ?: run {
-						checkFail()
-						return@listen
+					fromContext?.close()
+				}
+			}
+		}
+	}
+
+	private fun tripChain(from: Container): Task<*> =
+		taskOrNull { from.access() }
+			.then { fromContext ->
+				MoveTask(
+					fromContainers = listOf(from),
+					toContainers = stagingContainers,
+					fromSelection = fromStack,
+					toSelection = StackSelection.EVERYTHING,
+					limit = pullLimit,
+					allowReplace = false,
+					trackResult = false
+				).thenOrNull { pulled ->
+					staged += pulled
+					if (from.count(fromStack) <= 0) {
+						fromQueue.removeAll { it.haveMatchingInventories(from) }
 					}
-					return@listen
+					fromContext?.close()
+				}
+			}
+			.then { pushChain(from) }
+
+	private fun pushChain(from: Container): Task<*> {
+		val to = toQueue.firstOrNull { !it.haveMatchingInventories(from) }
+			?: return taskOrNull { null }
+		if (staged <= 0) return taskOrNull { null }
+
+		return taskOrNull { to.access() }
+			.then { toContext ->
+				MoveTask(
+					fromContainers = stagingContainers,
+					toContainers = listOf(to),
+					fromSelection = fromStack,
+					toSelection = toStack,
+					limit = pushLimit
+				).thenOrNull { pushed ->
+					staged = (staged - pushed).coerceAtLeast(0)
+					if (pushed <= 0 || !canReceive(to)) {
+						toQueue.removeAll { it.haveMatchingInventories(to) }
+					}
+					toContext?.close()
+				}
+			}
+			.thenOrNull {
+				if (isComplete || staged <= 0) return@thenOrNull null
+
+				if (toQueue.any { !it.haveMatchingInventories(from) }) pushChain(from)
+				else null
+			}
+	}
+
+	private fun canReceive(container: Container) =
+		container.spaceLeft(fromStack.mutate(1)) > 0 ||
+				container.count(toStack.mutate { notEmpty(); withoutSelection(fromStack) }) > 0
+
+	private data class SlotPair(
+		val fromContainer: Container,
+		val fromSlot: Slot,
+		val toContainer: Container,
+		val toSlot: Slot
+	) {
+		operator fun get(target: StackMovePlanner.Target) =
+			when (target) {
+				StackMovePlanner.Target.Source -> fromSlot
+				StackMovePlanner.Target.Destination -> toSlot
+			}
+	}
+
+	private inner class MoveTask @Ta5kBuilder constructor(
+		private val fromContainers: List,
+		private val toContainers: List,
+		private val fromSelection: StackSelection,
+		toSelection: StackSelection,
+		private val limit: Int,
+		private val allowReplace: Boolean = true,
+		private val trackResult: Boolean = true
+	) : Task() {
+		override val name get() = "Moving ${this@MoveTask.fromSelection}"
+		private var moved = 0
+
+		private val countsAir = fromSelection.countEmptyAsOne
+		private val wholeStacks = fromSelection.alwaysWholeStacks
+
+		private val baseToSelection = toSelection
+
+		private val swappableSelection =
+			toSelection.mutate {
+				notEmpty()
+				withoutSelection(this@MoveTask.fromSelection)
+			}
+
+		init {
+			listen {
+				runSafeAutomated {
+					do {
+						val done = tick()
+					} while (!done)
 				}
+			}
+		}
+
+		private fun AutomatedSafeContext.tick(): Boolean {
+			val currentLimit =
+				if (limit == Int.MAX_VALUE) Int.MAX_VALUE
+				else (limit - moved).coerceAtLeast(0)
+			if (currentLimit <= 0) {
+				finish()
+				return true
+			}
+
+			val currentSelection =
+				fromSelection.mutate(
+					if (wholeStacks || limit == Int.MAX_VALUE) StackSelection.UNBOUNDED
+					else minOf(currentLimit, fromSelection.count.coerceAtLeast(1))
+				) { if (!countsAir) notEmpty() }
+
+			val pair =
+				fromContainers.firstNotNullOfOrNull { fromContainer ->
+					val fromSlot = fromContainer.findSlot(currentSelection) ?: return@firstNotNullOfOrNull null
 
-				fromContainer.getSlot(stackSelection)?.let { fromSlot ->
-					toContainer.getReplaceableSlot()?.let { toSlot ->
-						val transferEvent = ContainerEvent.Transfer(fromSlot, toSlot, fromContainer, toContainer)
-						if (transferEvent.post().isCanceled()) {
-							failure("Transfer prevented by an internal interruption")
-							return@listen
+					toContainers.firstNotNullOfOrNull { toContainer ->
+						if (toContainer.haveMatchingInventories(fromContainer)) return@firstNotNullOfOrNull null
+						if (!fromContainer.canSwapWith(toContainer)) return@firstNotNullOfOrNull null
+						findToSlot(fromSlot, toContainer)?.let { toSlot ->
+							SlotPair(fromContainer, fromSlot, toContainer, toSlot)
 						}
-						inventoryRequest {
-							if (fromContainer.swapMethodPriority > toContainer.swapMethodPriority)
-								with(fromContainer) { transfer(fromSlot, toSlot) }
-							else with(toContainer) { transfer(toSlot, fromSlot) }
-							onComplete { success() }
-						}.submit()
-						return@listen
 					}
 				}
 
-				checkFail()
+			if (pair == null) {
+				finish()
+				return true
 			}
+
+			return move(
+				pair,
+				if (wholeStacks) pair.fromSlot.stack.count(countsAir)
+				else currentLimit
+			)
+		}
+
+		private fun AutomatedSafeContext.findToSlot(fromSlot: Slot, toContainer: Container): Slot? {
+			val stack = fromSlot.stack
+			return toContainer.findSlot(
+				stackSelection {
+					predicate { candidate, _ -> !candidate.isEmpty && candidate.hasSpace && candidate.sameItemAs(stack) }
+					canInsert(stack)
+					sortedWith(baseToSelection.comparator)
+				}
+			) ?: toContainer.findReplaceSlot(
+				stack,
+				stackSelection(StackSelection.UNBOUNDED) {
+					isEmpty()
+					sortedWith(baseToSelection.comparator)
+				}
+			) ?: findSwappableSlot(fromSlot, toContainer)
+		}
+
+		private fun AutomatedSafeContext.findSwappableSlot(fromSlot: Slot, toContainer: Container): Slot? {
+			if (!allowReplace) return null
+			return toContainer.findReplaceSlot(fromSlot.stack, swappableSelection)?.takeIf { fromSlot.canInsert(it.stack) }
 		}
-	}
 
+		private fun AutomatedSafeContext.move(pair: SlotPair, atMost: Int): Boolean {
+			val stack = pair.fromSlot.stack
+			val size = stack.count(countsAir)
+			val count = minOf(size, atMost)
+			val cursor = CursorContainer.stacks.firstOrNull() ?: ItemStack.EMPTY
+			if (pair.toSlot.stack.isForeignTo(stack)) {
+				if (count != size) {
+					finish()
+					return true
+				}
+				return swapOut(pair)
+			}
+			if (stack.isEmpty) {
+				recordMoved(size)
+				finish()
+				return true
+			}
+			return when (count) {
+				size if cursor.isEmpty &&
+						allowQuickMove &&
+						quickMoveStaysInside(pair.fromContainer, stack) -> quickMoveWhole(pair)
+
+				size if cursor.isEmpty &&
+						pair.toSlot.stack.isEmpty -> swapOut(pair)
 
-	private fun checkFail(): Boolean =
-		failIfNoMaterial.also {
-			failure(NoMaterialAccessException(stackSelection))
+				else -> clickThrough(pair, count)
+			}
+		}
+
+		private fun AutomatedSafeContext.swapOut(pair: SlotPair): Boolean {
+			val count = pair.fromSlot.stack.count(countsAir)
+			val request = pair.fromContainer.swapRequest(pair.fromSlot, pair.toSlot, pair.toContainer)?.submit()
+			return when {
+				request == null -> {
+					finish()
+					true
+				}
+				!request.done -> {
+					finishUnlessTransient(request.failureReason)
+					true
+				}
+				else -> {
+					recordMoved(count)
+					recordDestination(pair.toSlot, pair.toContainer)
+					false
+				}
+			}
 		}
 
-	private class NoMaterialAccessException(stackSelection: StackSelection) : IllegalStateException("Unable to access $stackSelection.")
-}
\ No newline at end of file
+		private fun AutomatedSafeContext.quickMoveWhole(pair: SlotPair): Boolean {
+			if (availablePackets(PacketType.Inventory) < 1) return true
+			val transferEvent =
+				ContainerEvent.Transfer(
+					pair.fromSlot,
+					pair.toSlot,
+					pair.fromContainer,
+					pair.toContainer
+				)
+			if (transferEvent.post().isCanceled()) return true
+
+			val before = pair.fromSlot.stack.copy()
+			val destinationsBefore =
+				if (trackResult) toContainers.map { container -> container.slots.map { it.stack.copy() } }
+				else emptyList()
+			val request = inventoryRequest { quickMove(pair.fromSlot.id) }.submit()
+			if (!request.done) {
+				finishUnlessTransient(request.failureReason)
+				return true
+			}
+
+			val left = pair.fromSlot.stack.takeIf { it.sameItemAs(before) }?.count(countsAir) ?: 0
+			val count = before.count(countsAir) - left
+			if (count <= 0) {
+				finish()
+				return true
+			}
+			recordMoved(count)
+			if (trackResult) recordReceivingSlots(destinationsBefore)
+			return false
+		}
+
+		private fun AutomatedSafeContext.clickThrough(pair: SlotPair, count: Int): Boolean {
+			if (!CursorContainer.isAccessible) {
+				finish()
+				return true
+			}
+			val stack = pair.fromSlot.stack
+			val cursor = CursorContainer.stacks.firstOrNull() ?: return true
+			if (cursor.isForeignTo(stack)) {
+				finish()
+				return true
+			}
+
+			val budget = availablePackets(PacketType.Inventory)
+			val beforeTo = pair.toSlot.stack.count
+			val plan =
+				StackMovePlanner.planWithin(
+					state = StackMovePlanner.State(cursor.count, stack.count, beforeTo),
+					maxCount = pair.toSlot.getMaxItemCount(stack),
+					goal = beforeTo + count,
+					clickBudget = budget
+				)
+			if (plan == null) {
+				if (budget >= buildConfig.inventoryLimit) finish()
+				return true
+			}
+
+			val transferEvent =
+				ContainerEvent.Transfer(
+					pair.fromSlot,
+					pair.toSlot,
+					pair.fromContainer,
+					pair.toContainer
+				)
+			if (transferEvent.post().isCanceled()) return true
+			val request =
+				inventoryRequest {
+					plan.clicks.forEach { click -> pickup(pair[click.target].id, click.button.id) }
+				}.submit()
+			if (!request.done) {
+				finishUnlessTransient(request.failureReason)
+				return true
+			}
+
+			recordMoved(plan.destination - beforeTo)
+			recordDestination(pair.toSlot, pair.toContainer)
+			return false
+		}
+
+		private fun AutomatedSafeContext.quickMoveStaysInside(fromContainer: Container, stack: ItemStack): Boolean {
+			if (stack.components.contains(DataComponentTypes.EQUIPPABLE)) return false
+			val playerScreen = player.currentScreenHandler.syncId == 0
+			fun covers(container: Container) = toContainers.any { it.haveMatchingInventories(container) }
+			fun coversOpenContainer() = toContainers.any { it is ExternalContainer && it.isAccessed }
+			return when {
+				fromContainer is ExternalContainer -> covers(InventoryContainer) && covers(HotbarContainer)
+				fromContainer === InventoryContainer -> if (playerScreen) covers(HotbarContainer) else coversOpenContainer()
+				fromContainer === HotbarContainer -> if (playerScreen) covers(InventoryContainer) else coversOpenContainer()
+				else -> false
+			}
+		}
+
+		private fun recordReceivingSlots(before: List>) {
+			toContainers.zip(before).forEach { (container, previous) ->
+				container.slots.forEachIndexed { index, slot ->
+					if (slot.stack.grewFrom(previous.getOrNull(index))) recordDestination(slot, container)
+				}
+			}
+		}
+
+		private fun recordMoved(count: Int) {
+			moved += count
+			if (trackResult) transferred += count
+		}
+
+		private fun recordDestination(slot: Slot, container: Container) {
+			if (!trackResult) return
+			resultSlots[slot] = slot.stack.copy()
+			resultContainers += container
+		}
+
+		private fun finishUnlessTransient(failureReason: InvRequestFailureReason) {
+			if (failureReason == InvRequestFailureReason.None) finish()
+		}
+
+		private fun finish() {
+			success(moved)
+		}
+
+		private fun ItemStack.sameItemAs(other: ItemStack) = ItemStack.areItemsAndComponentsEqual(this, other)
+
+		private fun ItemStack.isForeignTo(other: ItemStack) = !isEmpty && !sameItemAs(other)
+
+		private fun ItemStack.grewFrom(previous: ItemStack?) =
+			!isEmpty && (previous == null || previous.isEmpty || !sameItemAs(previous) || count > previous.count)
+	}
+
+	private fun fail(message: String) {
+		failure(FailedContainerTransferException(message, staged))
+	}
+}
+
+@Suppress("unused")
+class FailedContainerTransferException(
+	message: String,
+	val staged: Int
+) : IllegalStateException(message)
diff --git a/src/main/kotlin/com/lambda/task/tasks/EatTask.kt b/src/main/kotlin/com/lambda/task/tasks/EatTask.kt
index e67320e33..d07ece07e 100644
--- a/src/main/kotlin/com/lambda/task/tasks/EatTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/EatTask.kt
@@ -23,16 +23,23 @@ import com.lambda.context.Automated
 import com.lambda.context.SafeContext
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.managers.hotbar.HotbarRequest
-import com.lambda.interaction.material.container.containers.HotbarContainer
-import com.lambda.interaction.material.container.containers.InventoryContainer
+import com.lambda.interaction.container.containers.HotbarContainer
+import com.lambda.interaction.container.containers.InventoryContainer
+import com.lambda.interaction.container.selection.select
+import com.lambda.interaction.handler.handlers.ContainerHandler.findSlot
+import com.lambda.interaction.manager.managers.hotbar.hotbarRequest
 import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
 import com.lambda.threading.runSafeAutomated
 import net.minecraft.item.ItemStack
 import net.minecraft.util.ActionResult
 import net.minecraft.util.Hand
 
-class EatTask @Ta5kBuilder constructor(
+@Ta5kBuilder
+context(automated: Automated)
+fun eat() = EatTask(automated)
+
+class EatTask @Ta5kBuilder internal constructor(
     automated: Automated
 ) : Task(), Automated by automated {
     override val name: String
@@ -56,20 +63,24 @@ class EatTask @Ta5kBuilder constructor(
                 return@listen
             }
 
-            val foodFinder = reason.selector()
-            val hotbarSlot = foodFinder.filterSlots(HotbarContainer.slots).firstOrNull()
+            val selection = reason.selector()
+            val hotbarSlot = findSlot(selection, HotbarContainer.select())
             if (hotbarSlot != null) {
-                val request = HotbarRequest(
-                    hotbarSlot.index,
-                    this@EatTask,
-                    keepTicks = hotbarConfig.keepTicks.coerceAtLeast(1),
-                    nowOrNothing = false
-                ).submit()
+                val request =
+                    hotbarRequest(hotbarSlot.index) {
+                        keepTicks(hotbarConfig.keepTicks.coerceAtLeast(1))
+                    }.submit()
                 if (!request.done) return@listen
             } else {
-                val inventorySlot = foodFinder.filterSlots(InventoryContainer.slots).firstOrNull()
-                if (inventorySlot != null) runSafeAutomated {
-                    InventoryContainer.transfer(foodFinder, HotbarContainer)
+                if (selection isIn InventoryContainer) {
+                    runSafeAutomated {
+                        transfer(
+                            selection,
+                            InventoryContainer.select(),
+                            HotbarContainer.select()
+                        ).start()
+                        return@listen
+                    }
                 }
                 if (holdingUse) {
                     mc.options.useKey.isPressed = false
@@ -96,10 +107,4 @@ class EatTask @Ta5kBuilder constructor(
             }
         }
     }
-
-    companion object {
-        @Ta5kBuilder
-        context(automated: Automated)
-        fun eat() = EatTask(automated)
-    }
 }
diff --git a/src/main/kotlin/com/lambda/task/tasks/FindPlacedBlockTask.kt b/src/main/kotlin/com/lambda/task/tasks/FindPlacedBlockTask.kt
new file mode 100644
index 000000000..8b755ef92
--- /dev/null
+++ b/src/main/kotlin/com/lambda/task/tasks/FindPlacedBlockTask.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.task.tasks
+
+import com.lambda.context.SafeContext
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.util.world.blockSearch
+import net.minecraft.block.Block
+import net.minecraft.util.math.BlockPos
+
+@Ta5kBuilder
+fun findBlock(block: Block, searchRadius: Int) = FindPlacedBlockTask(block, searchRadius)
+
+class FindPlacedBlockTask @Ta5kBuilder internal constructor(
+	private val block: Block,
+	private val searchRadius: Int
+) : Task() {
+	override val name = "Finding placed block: $block"
+
+	override fun SafeContext.onStart() {
+		val pos =
+			blockSearch(searchRadius, player.blockPos) { _, state ->
+				state.isOf(block)
+			}.keys.firstOrNull()
+
+		if (pos != null) success(pos)
+		else failure(IllegalStateException("Could not find block $block"))
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt b/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt
index cc79f7314..3d4374637 100644
--- a/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt
@@ -22,9 +22,10 @@ import com.lambda.context.Automated
 import com.lambda.event.events.InventoryEvent
 import com.lambda.event.events.TickEvent
 import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.interaction.handlers.BaritoneHandler
-import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
+import com.lambda.interaction.handler.handlers.BaritoneHandler
+import com.lambda.interaction.manager.managers.rotating.rotationRequest
 import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.TickTimer
 import com.lambda.util.player.RotationUtils.lookAtBlock
@@ -34,11 +35,19 @@ import net.minecraft.util.Hand
 import net.minecraft.util.math.BlockPos
 import net.minecraft.util.math.Direction
 
-class OpenContainerTask @Ta5kBuilder constructor(
+@Ta5kBuilder
+context(automated: Automated)
+fun openContainer(
+    blockPos: BlockPos,
+    waitForSlotLoad: Boolean = true,
+    sides: Set = Direction.entries.toSet()
+) = OpenContainerTask(blockPos, waitForSlotLoad, sides, automated)
+
+class OpenContainerTask @Ta5kBuilder internal constructor(
     private val blockPos: BlockPos,
-    private val automated: Automated,
     private val waitForSlotLoad: Boolean = true,
-    private val sides: Set = Direction.entries.toSet()
+    private val sides: Set = Direction.entries.toSet(),
+    automated: Automated
 ) : Task(), Automated by automated {
     override val name get() = "${containerState.description()} at ${blockPos.toShortString()}"
 
@@ -46,6 +55,7 @@ class OpenContainerTask @Ta5kBuilder constructor(
     private var containerState = State.Scoping
 
     private val retryTimer = TickTimer()
+    private var interactions = 0
 
     enum class State {
         Pathing,
@@ -62,6 +72,8 @@ class OpenContainerTask @Ta5kBuilder constructor(
     }
 
     init {
+        onCompletion { BaritoneHandler.cancel() }
+
         listen {
             if (containerState != State.Opening) return@listen
 
@@ -110,6 +122,14 @@ class OpenContainerTask @Ta5kBuilder constructor(
             player.swingHand(Hand.MAIN_HAND)
 
             containerState = State.Opening
+            if (++interactions > MAX_OPEN_ATTEMPTS) {
+                failure("Failed to open container at ${blockPos.toShortString()} after $interactions attempts")
+                return@listen
+            }
         }
     }
+
+    companion object {
+        private const val MAX_OPEN_ATTEMPTS = 20
+    }
 }
diff --git a/src/main/kotlin/com/lambda/task/tasks/PlaceContainerTask.kt b/src/main/kotlin/com/lambda/task/tasks/PlaceContainerTask.kt
index f4fde6b10..58a1f0c88 100644
--- a/src/main/kotlin/com/lambda/task/tasks/PlaceContainerTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/PlaceContainerTask.kt
@@ -20,18 +20,17 @@ package com.lambda.task.tasks
 import com.lambda.context.Automated
 import com.lambda.context.SafeContext
 import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
-import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
+import com.lambda.interaction.construction.blueprint.toBlueprint
 import com.lambda.interaction.construction.simulation.result.results.GenericResult
 import com.lambda.interaction.construction.simulation.result.results.InteractResult
 import com.lambda.interaction.construction.simulation.sim
 import com.lambda.interaction.construction.verify.TargetState
-import com.lambda.interaction.managers.ManagerUtils
+import com.lambda.interaction.manager.ManagerUtils
 import com.lambda.task.Task
-import com.lambda.task.tasks.BuildTask.Companion.build
-import com.lambda.task.wrappers.thenAction
+import com.lambda.task.Task.Ta5kBuilder
 import com.lambda.threading.runSafeAutomated
 import com.lambda.util.BlockUtils.blockPos
-import com.lambda.util.item.ItemUtils.shulkerBoxes
+import com.lambda.util.item.ItemUtils.SHULKER_BOXES
 import com.lambda.util.math.distSq
 import net.minecraft.block.ChestBlock
 import net.minecraft.entity.mob.ShulkerEntity
@@ -41,13 +40,26 @@ import net.minecraft.screen.slot.Slot
 import net.minecraft.util.math.BlockPos
 import net.minecraft.util.math.Direction
 
-class PlaceContainerTask @Ta5kBuilder constructor(
-    val slot: Slot,
+@Ta5kBuilder
+context(automated: Automated)
+fun placeContainer(slot: Slot?) = PlaceContainerTask(slot, automated)
+
+@Ta5kBuilder
+context(automated: Automated)
+fun placeContainer(slot: () -> Slot?) = PlaceContainerTask(slot(), automated)
+
+class PlaceContainerTask @Ta5kBuilder internal constructor(
+    val slot: Slot?,
     automated: Automated
 ) : Task(), Automated by automated {
-    override val name: String get() = "Placing container ${slot.stack.name.string}"
+    override val name: String get() = "Placing container ${slot?.stack?.name?.string}"
 
     override fun SafeContext.onStart() {
+        if (slot == null) {
+            failure("No slot provided")
+            return
+        }
+
         val results = runSafeAutomated {
             BlockPos.iterateOutwards(player.blockPos, 4, 3, 4)
                 .map { it.blockPos }
@@ -75,8 +87,8 @@ class PlaceContainerTask @Ta5kBuilder constructor(
             .toStructure(TargetState.Stack(slot.stack))
             .toBlueprint()
             .build(finishOnDone = true, collectDrops = false)
-            .thenAction { success(containerPosition) }
-            .execute(this@PlaceContainerTask)
+            .onSuccess { success(containerPosition) }
+            .start()
     }
 
     private fun SafeContext.canBeOpened(
@@ -87,7 +99,7 @@ class PlaceContainerTask @Ta5kBuilder constructor(
         Items.ENDER_CHEST -> {
             !ChestBlock.isChestBlocked(world, blockPos)
         }
-        in shulkerBoxes -> {
+        in SHULKER_BOXES -> {
             val box = ShulkerEntity
                 .calculateBoundingBox(0.5f, direction, 0.0f, blockPos.toBottomCenterPos())
                 .offset(blockPos)
diff --git a/src/main/kotlin/com/lambda/task/wrappers/ActionTask.kt b/src/main/kotlin/com/lambda/task/tasks/wrappers/ActionTasks.kt
similarity index 67%
rename from src/main/kotlin/com/lambda/task/wrappers/ActionTask.kt
rename to src/main/kotlin/com/lambda/task/tasks/wrappers/ActionTasks.kt
index 01f3af7c2..4abdb2f51 100644
--- a/src/main/kotlin/com/lambda/task/wrappers/ActionTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/wrappers/ActionTasks.kt
@@ -15,16 +15,30 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.task.wrappers
+package com.lambda.task.tasks.wrappers
 
 import com.lambda.context.SafeContext
 import com.lambda.task.Task
 import com.lambda.task.Task.Ta5kBuilder
 
 @Ta5kBuilder
-infix fun  Task.thenAction(action: SafeContext.(R) -> Unit): Task =
+fun  actionTask(action: SafeContext.() -> R) =
+	ActionTask(action)
+
+@Ta5kBuilder
+infix fun  Task.thenAction(action: SafeContext.(R) -> R2): Task =
 	SequencedActionTask(this, action)
 
+class ActionTask @Ta5kBuilder internal constructor(
+	private val action: SafeContext.() -> R
+) : Task() {
+	override val name get() = "Performing action"
+
+	override fun SafeContext.onStart() {
+		success(action())
+	}
+}
+
 /**
  * A task that performs a given task ([innerTask]) and then a given [action].
  *
@@ -32,18 +46,18 @@ infix fun  Task.thenAction(action: SafeContext.(R) -> Unit): Task =
  *
  * @see thenAction
  */
-class SequencedActionTask(
+class SequencedActionTask @Ta5kBuilder internal constructor(
 	private val innerTask: Task,
-	private val action: SafeContext.(R) -> Unit,
-) : Task() {
+	private val action: SafeContext.(R) -> R2,
+) : Task() {
 	override val name get() = "Performing action after ${innerTask.name}"
+	override val isInvisible = true
 
 	override fun SafeContext.onStart() {
 		innerTask
 			.onSuccess { result ->
-				action(this, result)
-				success(result)
+				success(action(this, result))
 			}
-			.execute(this@SequencedActionTask)
+			.start()
 	}
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/tasks/wrappers/BranchTasks.kt b/src/main/kotlin/com/lambda/task/tasks/wrappers/BranchTasks.kt
new file mode 100644
index 000000000..932d2d4af
--- /dev/null
+++ b/src/main/kotlin/com/lambda/task/tasks/wrappers/BranchTasks.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.task.tasks.wrappers
+
+import com.lambda.context.SafeContext
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.TaskGenerator
+import com.lambda.task.TaskOrNullGenerator
+
+@Ta5kBuilder
+fun  Task.withBranch(
+	onSuccess: TaskGenerator,
+	onFailure: TaskGenerator
+) = BranchTask(this, onSuccess, onFailure)
+
+@Ta5kBuilder
+fun  Task.withBranchOrNull(
+	onSuccess: TaskOrNullGenerator,
+	onFailure: TaskOrNullGenerator
+) = OptionalBranchTask(this, onSuccess, onFailure)
+
+/**
+ * A task that strictly branches into one of two tasks based on the success or failure of the [innerTask].
+ *
+ * @see withBranch
+ */
+class BranchTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val onSuccessGenerator: TaskGenerator,
+	private val onFailureGenerator: TaskGenerator
+) : Task() {
+	override val name get() = "Branching task for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { result ->
+				onSuccessGenerator(this, result)
+					.onSuccess { success(it) }
+					.start()
+			}
+			.onFailure { cause ->
+				onFailureGenerator(this, cause)
+					.onSuccess { success(it) }
+					.start()
+			}
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask != innerTask) super.onSubTaskFailure(subTask, cause)
+	}
+}
+
+/**
+ * A task that strictly branches into one of two optional tasks based on the success or failure of the [innerTask].
+ *
+ * @see withBranchOrNull
+ */
+class OptionalBranchTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val onSuccessGenerator: TaskOrNullGenerator,
+	private val onFailureGenerator: TaskOrNullGenerator
+) : Task() {
+	override val name get() = "Optional branching task for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { result ->
+				onSuccessGenerator(this, result)
+					?.onSuccess { success(it) }
+					?.start()
+					?: success(null)
+			}
+			.onFailure { cause ->
+				onFailureGenerator(this, cause)
+					?.onSuccess { success(it) }
+					?.start()
+					?: failure("Optional onFailure task not present for task: ${innerTask.name}")
+			}
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask != innerTask) super.onSubTaskFailure(subTask, cause)
+	}
+}
+
diff --git a/src/main/kotlin/com/lambda/task/tasks/wrappers/OnFailTasks.kt b/src/main/kotlin/com/lambda/task/tasks/wrappers/OnFailTasks.kt
new file mode 100644
index 000000000..fb6f6f27a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/task/tasks/wrappers/OnFailTasks.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.task.tasks.wrappers
+
+import com.lambda.context.SafeContext
+import com.lambda.task.Task
+import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.TaskGenerator
+import com.lambda.task.TaskOrNullGenerator
+import com.lambda.threading.runSafe
+
+@Ta5kBuilder
+infix fun  Task.onFail(handler: TaskGenerator) =
+	OnFailTask(this, handler)
+
+@Ta5kBuilder
+infix fun  Task.onFailOrNull(handler: TaskOrNullGenerator) =
+	OnFailOrNullTask(this, handler)
+
+@Ta5kBuilder
+infix fun  Task.withRecovery(handler: TaskGenerator) =
+	RecoveryTask(this, handler)
+
+@Ta5kBuilder
+infix fun  Task.withRecoveryOrNull(handler: SafeContext.(Throwable) -> Task?) =
+	OptionalRecoveryTask(this, handler)
+
+/**
+ * A task that performs a given [innerTask] with a callback task in the event the [innerTask] fails.
+ *
+ * Useful for when you want a certain task to trigger when another task fails.
+ * If in need of a task that returns the same type to avoid further branching, see [withRecovery] or [withRecoveryOrNull].
+ *
+ * @see onFail
+ */
+class OnFailTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val onFailTaskGenerator: TaskGenerator
+) : Task() {
+	override val name get() = "On fail task for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { success(it) }
+			.onFailure { cause ->
+				onFailTaskGenerator(cause)
+					.onSuccess { success(null) }
+					.start()
+			}
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask != innerTask) super.onSubTaskFailure(subTask, cause)
+	}
+}
+
+/**
+ * A task that performs a given [innerTask] with an optional callback task in the event the [innerTask] fails.
+ *
+ * Useful for when you want a certain task to trigger based on a predicate when another task fails.
+ * If in need of a task that returns the same type to avoid further branching, see [withRecovery] or [withRecoveryOrNull].
+ *
+ * @see onFailOrNull
+ */
+class OnFailOrNullTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val onFailTaskGenerator: TaskOrNullGenerator
+) : Task() {
+	override val name get() = "On fail task for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { success(it) }
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask == innerTask) {
+			runSafe {
+				onFailTaskGenerator(cause)
+					?.onSuccess { success(null) }
+					?.start()
+					?: super.onSubTaskFailure(subTask, cause)
+			}
+		} else super.onSubTaskFailure(subTask, cause)
+	}
+}
+
+/**
+ * A task that performs a given [innerTask] with a fallback to the task supplied by [recoveryTaskGenerator] in case of failure.
+ * Both the [innerTask] and the task generated by [recoveryTaskGenerator] have the same result type.
+ *
+ * Useful for if you want to have a backup task to return the same result type in case the first fails.
+ * This leaves the task branch clean of further branching while providing protection against individual task failure.
+ *
+ * @see withRecovery
+ */
+class RecoveryTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val recoveryTaskGenerator: TaskGenerator,
+) : Task() {
+	override val name get() = "Recovery task in case of failure for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { success(it) }
+			.onFailure { cause ->
+				recoveryTaskGenerator(this, cause)
+					.onSuccess { success(it) }
+					.start()
+			}
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask != innerTask) super.onSubTaskFailure(subTask, cause)
+	}
+}
+
+/**
+ * A task that performs a given [innerTask] with a fallback to the optional task supplied by [recoveryTaskGenerator] in case of failure.
+ * Both the [innerTask] and the optional task generated by [recoveryTaskGenerator] have the same result type.
+ *
+ * Useful for if you want to have an optional backup task to return the same result type in case the first fails.
+ * This leaves the task branch clean of further branching while providing protection against individual task failure, unless null is returned from [recoveryTaskGenerator].
+ *
+ * @see withRecovery
+ */
+class OptionalRecoveryTask @Ta5kBuilder internal constructor(
+	private val innerTask: Task,
+	private val recoveryTaskGenerator: TaskOrNullGenerator,
+) : Task() {
+	override val name get() = "Optional recovery task in case of failure for ${innerTask.name}"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { success(it) }
+			.start()
+	}
+
+	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
+		if (subTask == innerTask) {
+			runSafe {
+				recoveryTaskGenerator(this, cause)
+					?.onSuccess { success(it) }
+					?.start()
+					?: super.onSubTaskFailure(subTask, cause)
+			}
+		} else super.onSubTaskFailure(subTask, cause)
+	}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/wrappers/SequenceTasks.kt b/src/main/kotlin/com/lambda/task/tasks/wrappers/SequenceTasks.kt
similarity index 54%
rename from src/main/kotlin/com/lambda/task/wrappers/SequenceTasks.kt
rename to src/main/kotlin/com/lambda/task/tasks/wrappers/SequenceTasks.kt
index c8726880c..112296755 100644
--- a/src/main/kotlin/com/lambda/task/wrappers/SequenceTasks.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/wrappers/SequenceTasks.kt
@@ -15,21 +15,22 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.task.wrappers
+package com.lambda.task.tasks.wrappers
 
 import com.lambda.context.SafeContext
 import com.lambda.task.Task
 import com.lambda.task.Task.Ta5kBuilder
+import com.lambda.task.TaskGenerator
+import com.lambda.task.TaskOrNullGenerator
+import com.lambda.task.TaskOrNullSupplier
 
 @Ta5kBuilder
-infix fun  Task.then(supplier: TaskSupplier) =
-	SequencedTask(this, supplier)
+infix fun  Task.then(generator: TaskGenerator) =
+	SequencedTask(this, generator)
 
 @Ta5kBuilder
-infix fun  Task.then(task: Task): Task {
-	require(task != this) { "Cannot link a task to itself" }
-	return SequencedTask(this) { task }
-}
+infix fun  Task.then(task: Task): Task =
+	SequencedTask(this) { task }
 
 @Ta5kBuilder
 fun  Task.then(vararg tasks: Task<*>) =
@@ -38,55 +39,81 @@ fun  Task.then(vararg tasks: Task<*>) =
 	}
 
 @Ta5kBuilder
-infix fun  Task.thenOrNull(supplier: SafeContext.(R) -> Task?) =
-	OptionalSequencedTask(this, supplier)
+infix fun  Task.thenOrNull(generator: TaskOrNullGenerator) =
+	OptionalSequencedTask(this, generator)
+
+@Ta5kBuilder
+fun  taskOrNull(supplier: TaskOrNullSupplier) = TaskOrNullTask(supplier)
 
 /**
- * A task that sequences the [firstTask] with the next task, supplied by [nextTaskSupplier].
+ * A task that sequences the [firstTask] with the next task, supplied by [nextTaskGenerator].
  *
  * Useful for when two tasks must be run sequentially. The result of the first task is fed into the supplier for the second.
  *
  * @see then
  */
-class SequencedTask(
+class SequencedTask @Ta5kBuilder internal constructor(
 	private val firstTask: Task,
-	private val nextTaskSupplier: TaskSupplier,
+	private val nextTaskGenerator: TaskGenerator,
 ) : Task() {
 	override val name get() = "Chaining ${firstTask.name}"
+	override val isInvisible = true
 	var second: Task? = null
 
 	override fun SafeContext.onStart() {
 		firstTask
 			.onSuccess { result ->
-				second = nextTaskSupplier(this, result)
+				second = nextTaskGenerator(this, result)
 					.onSuccess { success(it) }
-					.execute(this@SequencedTask)
+					.start()
 			}
-			.execute(this@SequencedTask)
+			.start()
 	}
 }
 
 /**
- * A task that sequences the [firstTask] with the next optional task, supplied by [nextTaskSupplier].
+ * A task that sequences the [firstTask] with the next optional task, supplied by [nextTaskGenerator].
  *
  * Useful for when you want to sequence a second task after the first based on a predicate. The result of the first task could help shape that outcome.
  *
  * @see thenOrNull
  */
-class OptionalSequencedTask(
+class OptionalSequencedTask @Ta5kBuilder internal constructor(
 	private val firstTask: Task,
-	private val nextTaskSupplier: TaskOrNullSupplier
+	private val nextTaskGenerator: TaskOrNullGenerator
 ) : Task() {
 	override val name get() = "Chaining ${firstTask.name}"
+	override val isInvisible = true
 
 	override fun SafeContext.onStart() {
 		firstTask
 			.onSuccess { result ->
-				nextTaskSupplier(this, result)
+				nextTaskGenerator(this, result)
 					?.onSuccess { success(it) }
-					?.execute(this@OptionalSequencedTask)
+					?.start()
 					?: success(null)
 			}
-			.execute(this@OptionalSequencedTask)
+			.start()
+	}
+}
+
+/**
+ * A task that runs an optional task, supplied by [taskOrNullSupplier].
+ *
+ * Useful for starting a task branch where the first task could or could not be skipped.
+ *
+ * @see taskOrNull
+ */
+class TaskOrNullTask @Ta5kBuilder internal constructor(
+	private val taskOrNullSupplier: TaskOrNullSupplier
+) : Task() {
+	override val name get() = "Optional task"
+	override val isInvisible = true
+
+	override fun SafeContext.onStart() {
+		taskOrNullSupplier()
+			?.onSuccess { success(it) }
+			?.start()
+			?: success(null)
 	}
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/task/wrappers/SoftFailTask.kt b/src/main/kotlin/com/lambda/task/tasks/wrappers/SoftFailTask.kt
similarity index 81%
rename from src/main/kotlin/com/lambda/task/wrappers/SoftFailTask.kt
rename to src/main/kotlin/com/lambda/task/tasks/wrappers/SoftFailTask.kt
index 6163a43b0..d57dc8cc3 100644
--- a/src/main/kotlin/com/lambda/task/wrappers/SoftFailTask.kt
+++ b/src/main/kotlin/com/lambda/task/tasks/wrappers/SoftFailTask.kt
@@ -15,15 +15,12 @@
  * along with this program.  If not, see .
  */
 
-package com.lambda.task.wrappers
+package com.lambda.task.tasks.wrappers
 
 import com.lambda.context.SafeContext
 import com.lambda.task.Task
 import com.lambda.task.Task.Ta5kBuilder
 
-typealias TaskSupplier = SafeContext.(R) -> Task
-typealias TaskOrNullSupplier = SafeContext.(R) -> Task?
-
 @Ta5kBuilder
 fun  Task.softFail(): Task =
     SoftFailTask(this)
@@ -35,16 +32,17 @@ fun  Task.softFail(): Task =
  *
  * @see softFail
  */
-class SoftFailTask(
+class SoftFailTask @Ta5kBuilder internal constructor(
     private val innerTask: Task,
 ) : Task() {
     override val name get() = "Soft fail protection for ${innerTask.name}"
+    override val isInvisible = true
 
-    override fun SafeContext.onStart() {
-        innerTask
-            .onSuccess { success(it) }
-            .execute(this@SoftFailTask)
-    }
+	override fun SafeContext.onStart() {
+		innerTask
+			.onSuccess { success(it) }
+            .start()
+	}
 
     override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
         if (subTask == innerTask) success(null)
diff --git a/src/main/kotlin/com/lambda/task/wrappers/RecoveryTasks.kt b/src/main/kotlin/com/lambda/task/wrappers/RecoveryTasks.kt
deleted file mode 100644
index 990e526f1..000000000
--- a/src/main/kotlin/com/lambda/task/wrappers/RecoveryTasks.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2026 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see .
- */
-
-package com.lambda.task.wrappers
-
-import com.lambda.context.SafeContext
-import com.lambda.task.Task
-import com.lambda.task.Task.Ta5kBuilder
-import com.lambda.threading.runSafe
-
-@Ta5kBuilder
-infix fun  Task.onFail(handler: SafeContext.(Throwable) -> Task) =
-	RecoveryTask(this, handler)
-
-@Ta5kBuilder
-infix fun  Task.onFailOrNull(handler: SafeContext.(Throwable) -> Task?) =
-	OptionalRecoveryTask(this, handler)
-
-/**
- * A task that performs a given task ([innerTask]) with a fallback to the task supplied by [recoveryTaskSupplier] in-case of failure.
- *
- * Useful for if you want to keep the task branch going even in the event an individual task fails.
- *
- * @see onFail
- */
-class RecoveryTask(
-	private val innerTask: Task,
-	private val recoveryTaskSupplier: SafeContext.(Throwable) -> Task,
-) : Task() {
-	override val name get() = "Recovery task in case of failure for ${innerTask.name}"
-
-	override fun SafeContext.onStart() {
-		innerTask
-			.onSuccess { success(null) }
-			.execute(this@RecoveryTask)
-	}
-
-	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
-		if (subTask == innerTask) {
-			runSafe {
-				recoveryTaskSupplier(this, cause)
-					.onSuccess { success(it) }
-					.execute(this@RecoveryTask)
-			}
-		} else super.onSubTaskFailure(subTask, cause)
-	}
-}
-
-/**
- * A task that performs a given task ([innerTask]) with a fallback to the nullable task supplied by [recoveryTaskSupplier] in-case of failure.
- *
- * Useful for if you want to keep the task branch going even in the event an individual task fails, but want to supply the recovery task based on a predicate.
- *
- * @see onFailOrNull
- */
-class OptionalRecoveryTask(
-	private val innerTask: Task,
-	private val recoveryTaskSupplier: SafeContext.(Throwable) -> Task?,
-) : Task() {
-	override val name get() = "Optional recovery task in case of failure for ${innerTask.name}"
-
-	override fun SafeContext.onStart() {
-		innerTask
-			.onSuccess { success(null) }
-			.execute(this@OptionalRecoveryTask)
-	}
-
-	override fun onSubTaskFailure(subTask: Task<*>, cause: Throwable) {
-		if (subTask == innerTask) {
-			runSafe {
-				recoveryTaskSupplier(this, cause)
-					?.onSuccess { success(it) }
-					?.execute(this@OptionalRecoveryTask)
-					?: super.onSubTaskFailure(subTask, cause)
-			}
-		} else super.onSubTaskFailure(subTask, cause)
-	}
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/threading/Threading.kt b/src/main/kotlin/com/lambda/threading/Threading.kt
index a03371fe0..d81cc0b5d 100644
--- a/src/main/kotlin/com/lambda/threading/Threading.kt
+++ b/src/main/kotlin/com/lambda/threading/Threading.kt
@@ -50,7 +50,7 @@ inline fun  runSafe(block: SafeContext.() -> T): T? =
 @JvmName("runSafeAutomated0")
 context(safeContext: SafeContext)
 inline fun  Automated.runSafeAutomated(automated: Automated = this, block: AutomatedSafeContext.() -> T): T =
-    AutomatedSafeContext(safeContext, automated).run(block)
+    AutomatedSafeContext.create(safeContext, automated).run(block)
 
 /**
  * Runs the [block] in an automated context.
@@ -59,7 +59,7 @@ inline fun  Automated.runSafeAutomated(automated: Automated = this, block: Au
  */
 @JvmName("runSafeAutomated1")
 inline fun  Automated.runSafeAutomated(block: AutomatedSafeContext.() -> T): T? {
-    return AutomatedSafeContext(SafeContext.create() ?: return null, this).run(block)
+    return AutomatedSafeContext.create(SafeContext.create() ?: return null, this).run(block)
 }
 
 /**
diff --git a/src/main/kotlin/com/lambda/util/BlockUtils.kt b/src/main/kotlin/com/lambda/util/BlockUtils.kt
index 5da12fb6b..fc42e7f3c 100644
--- a/src/main/kotlin/com/lambda/util/BlockUtils.kt
+++ b/src/main/kotlin/com/lambda/util/BlockUtils.kt
@@ -101,181 +101,187 @@ import net.minecraft.util.math.Vec3i
 
 @Suppress("unused")
 object BlockUtils {
-    val signs = setOf(
-        Blocks.OAK_SIGN,
-        Blocks.BIRCH_SIGN,
-        Blocks.ACACIA_SIGN,
-        Blocks.CHERRY_SIGN,
-        Blocks.JUNGLE_SIGN,
-        Blocks.DARK_OAK_SIGN,
-        Blocks.MANGROVE_SIGN,
-        Blocks.BAMBOO_SIGN,
-        Blocks.CRIMSON_SIGN,
-        Blocks.WARPED_SIGN,
-        Blocks.SPRUCE_SIGN
-    )
+    val SIGNS =
+        setOf(
+            Blocks.OAK_SIGN,
+            Blocks.BIRCH_SIGN,
+            Blocks.ACACIA_SIGN,
+            Blocks.CHERRY_SIGN,
+            Blocks.JUNGLE_SIGN,
+            Blocks.DARK_OAK_SIGN,
+            Blocks.MANGROVE_SIGN,
+            Blocks.BAMBOO_SIGN,
+            Blocks.CRIMSON_SIGN,
+            Blocks.WARPED_SIGN,
+            Blocks.SPRUCE_SIGN
+        )
 
-    val wallSigns = setOf(
-        Blocks.OAK_WALL_SIGN,
-        Blocks.BIRCH_WALL_SIGN,
-        Blocks.ACACIA_WALL_SIGN,
-        Blocks.CHERRY_WALL_SIGN,
-        Blocks.JUNGLE_WALL_SIGN,
-        Blocks.DARK_OAK_WALL_SIGN,
-        Blocks.MANGROVE_WALL_SIGN,
-        Blocks.BAMBOO_WALL_SIGN,
-        Blocks.CRIMSON_WALL_SIGN,
-        Blocks.WARPED_WALL_SIGN,
-        Blocks.SPRUCE_WALL_SIGN
-    )
+    val WALL_SIGNS =
+        setOf(
+            Blocks.OAK_WALL_SIGN,
+            Blocks.BIRCH_WALL_SIGN,
+            Blocks.ACACIA_WALL_SIGN,
+            Blocks.CHERRY_WALL_SIGN,
+            Blocks.JUNGLE_WALL_SIGN,
+            Blocks.DARK_OAK_WALL_SIGN,
+            Blocks.MANGROVE_WALL_SIGN,
+            Blocks.BAMBOO_WALL_SIGN,
+            Blocks.CRIMSON_WALL_SIGN,
+            Blocks.WARPED_WALL_SIGN,
+            Blocks.SPRUCE_WALL_SIGN
+        )
 
-    val hangingSigns = setOf(
-        Blocks.OAK_HANGING_SIGN,
-        Blocks.BIRCH_HANGING_SIGN,
-        Blocks.ACACIA_HANGING_SIGN,
-        Blocks.CHERRY_HANGING_SIGN,
-        Blocks.JUNGLE_HANGING_SIGN,
-        Blocks.DARK_OAK_HANGING_SIGN,
-        Blocks.MANGROVE_HANGING_SIGN,
-        Blocks.BAMBOO_HANGING_SIGN,
-        Blocks.CRIMSON_HANGING_SIGN,
-        Blocks.WARPED_HANGING_SIGN,
-        Blocks.SPRUCE_HANGING_SIGN
-    )
+    val HANGING_SIGNS =
+        setOf(
+            Blocks.OAK_HANGING_SIGN,
+            Blocks.BIRCH_HANGING_SIGN,
+            Blocks.ACACIA_HANGING_SIGN,
+            Blocks.CHERRY_HANGING_SIGN,
+            Blocks.JUNGLE_HANGING_SIGN,
+            Blocks.DARK_OAK_HANGING_SIGN,
+            Blocks.MANGROVE_HANGING_SIGN,
+            Blocks.BAMBOO_HANGING_SIGN,
+            Blocks.CRIMSON_HANGING_SIGN,
+            Blocks.WARPED_HANGING_SIGN,
+            Blocks.SPRUCE_HANGING_SIGN
+        )
 
-    val hangingWallSigns = setOf(
-        Blocks.OAK_WALL_HANGING_SIGN,
-        Blocks.BIRCH_WALL_HANGING_SIGN,
-        Blocks.ACACIA_WALL_HANGING_SIGN,
-        Blocks.CHERRY_WALL_HANGING_SIGN,
-        Blocks.JUNGLE_WALL_HANGING_SIGN,
-        Blocks.DARK_OAK_WALL_HANGING_SIGN,
-        Blocks.MANGROVE_WALL_HANGING_SIGN,
-        Blocks.BAMBOO_WALL_HANGING_SIGN,
-        Blocks.CRIMSON_WALL_HANGING_SIGN,
-        Blocks.WARPED_WALL_HANGING_SIGN,
-        Blocks.SPRUCE_WALL_HANGING_SIGN
-    )
+    val HANGING_WALL_SIGNS =
+        setOf(
+            Blocks.OAK_WALL_HANGING_SIGN,
+            Blocks.BIRCH_WALL_HANGING_SIGN,
+            Blocks.ACACIA_WALL_HANGING_SIGN,
+            Blocks.CHERRY_WALL_HANGING_SIGN,
+            Blocks.JUNGLE_WALL_HANGING_SIGN,
+            Blocks.DARK_OAK_WALL_HANGING_SIGN,
+            Blocks.MANGROVE_WALL_HANGING_SIGN,
+            Blocks.BAMBOO_WALL_HANGING_SIGN,
+            Blocks.CRIMSON_WALL_HANGING_SIGN,
+            Blocks.WARPED_WALL_HANGING_SIGN,
+            Blocks.SPRUCE_WALL_HANGING_SIGN
+        )
 
-    val allSigns = signs + wallSigns + hangingSigns + hangingWallSigns
+    val ALL_SIGNS = SIGNS + WALL_SIGNS + HANGING_SIGNS + HANGING_WALL_SIGNS
 
-    val pottedBlocks = setOf(
-        Blocks.POTTED_WARPED_FUNGUS,
-        Blocks.POTTED_AZALEA_BUSH,
-        Blocks.POTTED_CLOSED_EYEBLOSSOM,
-        Blocks.POTTED_CACTUS,
-        Blocks.POTTED_PINK_TULIP,
-        Blocks.POTTED_FLOWERING_AZALEA_BUSH,
-        Blocks.POTTED_RED_TULIP,
-        Blocks.POTTED_CORNFLOWER,
-        Blocks.POTTED_DANDELION,
-        Blocks.POTTED_SPRUCE_SAPLING,
-        Blocks.POTTED_WHITE_TULIP,
-        Blocks.POTTED_OAK_SAPLING,
-        Blocks.POTTED_WITHER_ROSE,
-        Blocks.POTTED_PALE_OAK_SAPLING,
-        Blocks.POTTED_ACACIA_SAPLING,
-        Blocks.POTTED_BIRCH_SAPLING,
-        Blocks.POTTED_ALLIUM,
-        Blocks.POTTED_CRIMSON_FUNGUS,
-        Blocks.POTTED_CRIMSON_ROOTS,
-        Blocks.POTTED_JUNGLE_SAPLING,
-        Blocks.POTTED_DEAD_BUSH,
-        Blocks.POTTED_TORCHFLOWER,
-        Blocks.POTTED_BLUE_ORCHID,
-        Blocks.POTTED_BROWN_MUSHROOM,
-        Blocks.POTTED_BAMBOO,
-        Blocks.POTTED_MANGROVE_PROPAGULE,
-        Blocks.POTTED_CHERRY_SAPLING,
-        Blocks.POTTED_AZURE_BLUET,
-        Blocks.POTTED_DARK_OAK_SAPLING,
-        Blocks.POTTED_RED_MUSHROOM,
-        Blocks.POTTED_WARPED_ROOTS,
-        Blocks.POTTED_OPEN_EYEBLOSSOM,
-        Blocks.POTTED_ORANGE_TULIP,
-        Blocks.POTTED_OXEYE_DAISY,
-        Blocks.POTTED_POPPY,
-        Blocks.POTTED_LILY_OF_THE_VALLEY,
-        Blocks.POTTED_FERN
-    )
+    val POTTED_BLOCKS =
+        setOf(
+            Blocks.POTTED_WARPED_FUNGUS,
+            Blocks.POTTED_AZALEA_BUSH,
+            Blocks.POTTED_CLOSED_EYEBLOSSOM,
+            Blocks.POTTED_CACTUS,
+            Blocks.POTTED_PINK_TULIP,
+            Blocks.POTTED_FLOWERING_AZALEA_BUSH,
+            Blocks.POTTED_RED_TULIP,
+            Blocks.POTTED_CORNFLOWER,
+            Blocks.POTTED_DANDELION,
+            Blocks.POTTED_SPRUCE_SAPLING,
+            Blocks.POTTED_WHITE_TULIP,
+            Blocks.POTTED_OAK_SAPLING,
+            Blocks.POTTED_WITHER_ROSE,
+            Blocks.POTTED_PALE_OAK_SAPLING,
+            Blocks.POTTED_ACACIA_SAPLING,
+            Blocks.POTTED_BIRCH_SAPLING,
+            Blocks.POTTED_ALLIUM,
+            Blocks.POTTED_CRIMSON_FUNGUS,
+            Blocks.POTTED_CRIMSON_ROOTS,
+            Blocks.POTTED_JUNGLE_SAPLING,
+            Blocks.POTTED_DEAD_BUSH,
+            Blocks.POTTED_TORCHFLOWER,
+            Blocks.POTTED_BLUE_ORCHID,
+            Blocks.POTTED_BROWN_MUSHROOM,
+            Blocks.POTTED_BAMBOO,
+            Blocks.POTTED_MANGROVE_PROPAGULE,
+            Blocks.POTTED_CHERRY_SAPLING,
+            Blocks.POTTED_AZURE_BLUET,
+            Blocks.POTTED_DARK_OAK_SAPLING,
+            Blocks.POTTED_RED_MUSHROOM,
+            Blocks.POTTED_WARPED_ROOTS,
+            Blocks.POTTED_OPEN_EYEBLOSSOM,
+            Blocks.POTTED_ORANGE_TULIP,
+            Blocks.POTTED_OXEYE_DAISY,
+            Blocks.POTTED_POPPY,
+            Blocks.POTTED_LILY_OF_THE_VALLEY,
+            Blocks.POTTED_FERN
+        )
 
-    val interactionBlocks = setOf(
-        AbstractCauldronBlock::class,
-        AbstractFurnaceBlock::class,
-        AbstractSignBlock::class,
-        AnvilBlock::class,
-        BarrelBlock::class,
-        BeaconBlock::class,
-        BedBlock::class,
-        BeehiveBlock::class,
-        BellBlock::class,
-        BrewingStandBlock::class,
-        ButtonBlock::class,
-        CakeBlock::class,
-        CampfireBlock::class,
-        CandleBlock::class,
-        CandleCakeBlock::class,
-        CartographyTableBlock::class,
-        CaveVinesBodyBlock::class,
-        CaveVinesHeadBlock::class,
-        ChestBlock::class,
-        ChiseledBookshelfBlock::class,
-        CommandBlock::class,
-        ComparatorBlock::class,
-        ComposterBlock::class,
-        CrafterBlock::class,
-        CraftingTableBlock::class,
-        DaylightDetectorBlock::class,
-        DecoratedPotBlock::class,
-        DispenserBlock::class,
-        DropperBlock::class,
-        DoorBlock::class,
-        DragonEggBlock::class,
-        EnchantingTableBlock::class,
-        EnderChestBlock::class,
-        FenceBlock::class,
-        FenceGateBlock::class,
-        FlowerPotBlock::class,
-        GrindstoneBlock::class,
-        HopperBlock::class,
-        JigsawBlock::class,
-        JukeboxBlock::class,
-        LecternBlock::class,
-        LeverBlock::class,
-        LightBlock::class,
-        LoomBlock::class,
-        NoteBlock::class,
-        PistonExtensionBlock::class,
-        PumpkinBlock::class,
-        RedstoneOreBlock::class,
-        RedstoneWireBlock::class,
-        RepeaterBlock::class,
-        RespawnAnchorBlock::class,
-        ShulkerBoxBlock::class,
-        SmithingTableBlock::class,
-        StonecutterBlock::class,
-        StructureBlock::class,
-        SweetBerryBushBlock::class,
-        TntBlock::class,
-        TrapdoorBlock::class
-    )
+    val INTERACTION_BLOCKS =
+        setOf(
+            AbstractCauldronBlock::class,
+            AbstractFurnaceBlock::class,
+            AbstractSignBlock::class,
+            AnvilBlock::class,
+            BarrelBlock::class,
+            BeaconBlock::class,
+            BedBlock::class,
+            BeehiveBlock::class,
+            BellBlock::class,
+            BrewingStandBlock::class,
+            ButtonBlock::class,
+            CakeBlock::class,
+            CampfireBlock::class,
+            CandleBlock::class,
+            CandleCakeBlock::class,
+            CartographyTableBlock::class,
+            CaveVinesBodyBlock::class,
+            CaveVinesHeadBlock::class,
+            ChestBlock::class,
+            ChiseledBookshelfBlock::class,
+            CommandBlock::class,
+            ComparatorBlock::class,
+            ComposterBlock::class,
+            CrafterBlock::class,
+            CraftingTableBlock::class,
+            DaylightDetectorBlock::class,
+            DecoratedPotBlock::class,
+            DispenserBlock::class,
+            DropperBlock::class,
+            DoorBlock::class,
+            DragonEggBlock::class,
+            EnchantingTableBlock::class,
+            EnderChestBlock::class,
+            FenceBlock::class,
+            FenceGateBlock::class,
+            FlowerPotBlock::class,
+            GrindstoneBlock::class,
+            HopperBlock::class,
+            JigsawBlock::class,
+            JukeboxBlock::class,
+            LecternBlock::class,
+            LeverBlock::class,
+            LightBlock::class,
+            LoomBlock::class,
+            NoteBlock::class,
+            PistonExtensionBlock::class,
+            PumpkinBlock::class,
+            RedstoneOreBlock::class,
+            RedstoneWireBlock::class,
+            RepeaterBlock::class,
+            RespawnAnchorBlock::class,
+            ShulkerBoxBlock::class,
+            SmithingTableBlock::class,
+            StonecutterBlock::class,
+            StructureBlock::class,
+            SweetBerryBushBlock::class,
+            TntBlock::class,
+            TrapdoorBlock::class
+        )
 
-    val fluids = listOf(
-        Fluids.LAVA,
-        Fluids.FLOWING_LAVA,
-        Fluids.WATER,
-        Fluids.FLOWING_WATER,
-        Fluids.EMPTY,
-    )
+    val FLUIDS =
+        listOf(
+            Fluids.LAVA,
+            Fluids.FLOWING_LAVA,
+            Fluids.WATER,
+            Fluids.FLOWING_WATER,
+            Fluids.EMPTY,
+        )
 
     fun SafeContext.blockState(pos: BlockPos): BlockState = world.getBlockState(pos)
     fun SafeContext.fluidState(pos: BlockPos): FluidState = world.getFluidState(pos)
     fun SafeContext.blockEntity(pos: BlockPos) = world.getBlockEntity(pos)
 
     fun BlockState.matches(state: BlockState, ignoredProperties: Collection> = emptySet()) =
-        this.block == state.block && this.properties.all {
-            this[it] == state[it] || it in ignoredProperties
-        }
+        this.block == state.block &&
+                this.properties.all { this[it] == state[it] || it in ignoredProperties }
 
     fun SafeContext.instantBreakable(blockState: BlockState, blockPos: BlockPos, breakThreshold: Float): Boolean {
         val ticksNeeded = 1 / (blockState.calcBlockBreakingDelta(player, world, blockPos) / breakThreshold)
diff --git a/src/main/kotlin/com/lambda/util/FolderRegistry.kt b/src/main/kotlin/com/lambda/util/FolderRegistry.kt
index d02d89b6f..2f52fe878 100644
--- a/src/main/kotlin/com/lambda/util/FolderRegistry.kt
+++ b/src/main/kotlin/com/lambda/util/FolderRegistry.kt
@@ -35,6 +35,7 @@ object FolderRegistry : Loadable {
     val replay: Path = lambda.resolve("replay")
     val cache: Path = lambda.resolve("cache")
     val capes: Path = cache.resolve("capes")
+    val containers: Path = lambda.resolve("containers")
     val structure: Path = lambda.resolve("structure")
     val maps: Path = lambda.resolve("maps")
     val fonts: Path = lambda.resolve("fonts")
@@ -42,7 +43,7 @@ object FolderRegistry : Loadable {
     val File.relativeMCPath: Path get() = minecraft.relativize(toPath())
 
     override fun load(): String {
-        val folders = listOf(lambda, config, packetLogs, replay, cache, capes, structure, maps, fonts)
+        val folders = listOf(lambda, config, packetLogs, replay, cache, capes, containers, structure, maps, fonts)
         val createdFolders = folders.mapNotNull {
             if (it.notExists()) {
                 it.createDirectories()
diff --git a/src/main/kotlin/com/lambda/util/PacketUtils.kt b/src/main/kotlin/com/lambda/util/PacketUtils.kt
index 22d95044a..2d8652302 100644
--- a/src/main/kotlin/com/lambda/util/PacketUtils.kt
+++ b/src/main/kotlin/com/lambda/util/PacketUtils.kt
@@ -27,7 +27,7 @@ object PacketUtils {
     /**
      * Sends a packet through the regular packet pipeline
      */
-    fun ClientPlayNetworkHandler.sendPacket(block: () -> Packet<*>) = connection.send(block())
+    fun ClientPlayNetworkHandler.sendPacket(packetSupplier: () -> Packet<*>) = connection.send(packetSupplier())
 
     /**
      * Sends a packet to the server without notifying the client.
diff --git a/src/main/kotlin/com/lambda/util/PlaceDirection.kt b/src/main/kotlin/com/lambda/util/PlaceDirection.kt
index b6e3b0484..bf0483a63 100644
--- a/src/main/kotlin/com/lambda/util/PlaceDirection.kt
+++ b/src/main/kotlin/com/lambda/util/PlaceDirection.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.util
 
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import net.minecraft.util.math.MathHelper
 import net.minecraft.util.math.Vec3i
 
diff --git a/src/main/kotlin/com/lambda/util/PlayerBuildLayerUtils.kt b/src/main/kotlin/com/lambda/util/PlayerBuildLayerUtils.kt
index 86d0375b3..96b029a8e 100644
--- a/src/main/kotlin/com/lambda/util/PlayerBuildLayerUtils.kt
+++ b/src/main/kotlin/com/lambda/util/PlayerBuildLayerUtils.kt
@@ -18,7 +18,7 @@
 package com.lambda.util
 
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.BaritoneHandler
+import com.lambda.interaction.handler.handlers.BaritoneHandler
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.BlockUtils.isNotEmpty
 import com.lambda.util.math.MathUtils.ceilToInt
diff --git a/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt b/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt
index 838b4f96d..fdf2636ee 100644
--- a/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt
+++ b/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt
@@ -80,6 +80,14 @@ class LimitedDecayQueue(
         return queue.removeIf { it.first == element }
     }
 
+    @Synchronized
+    fun refresh(element: E): Boolean {
+        cleanUp()
+        if (!queue.removeIf { it.first == element }) return false
+        queue.add(element to Instant.now())
+        return true
+    }
+
     @Synchronized
     override fun removeAll(elements: Collection): Boolean {
         cleanUp()
diff --git a/src/main/kotlin/com/lambda/util/combat/DamageUtils.kt b/src/main/kotlin/com/lambda/util/combat/DamageUtils.kt
index d6d890a09..f1b3abba8 100644
--- a/src/main/kotlin/com/lambda/util/combat/DamageUtils.kt
+++ b/src/main/kotlin/com/lambda/util/combat/DamageUtils.kt
@@ -20,7 +20,7 @@ package com.lambda.util.combat
 import com.lambda.context.SafeContext
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.extension.fullHealth
-import com.lambda.util.math.flooredBlockPos
+import com.lambda.util.math.blockPos
 import com.lambda.util.math.minus
 import com.lambda.util.player.prediction.buildPlayerPrediction
 import net.minecraft.block.BedBlock
@@ -76,7 +76,7 @@ object DamageUtils {
         val predictedPos = prediction.position
         val fallDistance = player.y - predictedPos.y + player.fallDistance
 
-        val state = blockState(predictedPos.flooredBlockPos)
+        val state = blockState(predictedPos.blockPos)
         val block = state.block
 
         val distance = fallDistance +
diff --git a/src/main/kotlin/com/lambda/util/extension/Entity.kt b/src/main/kotlin/com/lambda/util/extension/Entity.kt
index 50f761620..94ee5c188 100644
--- a/src/main/kotlin/com/lambda/util/extension/Entity.kt
+++ b/src/main/kotlin/com/lambda/util/extension/Entity.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.util.extension
 
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import net.minecraft.entity.Entity
 import net.minecraft.entity.LivingEntity
 import net.minecraft.util.math.Vec3d
diff --git a/src/main/kotlin/com/lambda/util/extension/World.kt b/src/main/kotlin/com/lambda/util/extension/World.kt
index 781b5c0b1..6703f6efa 100644
--- a/src/main/kotlin/com/lambda/util/extension/World.kt
+++ b/src/main/kotlin/com/lambda/util/extension/World.kt
@@ -21,6 +21,7 @@ package com.lambda.util.extension
 
 import com.lambda.Lambda.mc
 import com.lambda.context.SafeContext
+import com.lambda.util.world.Dimension
 import com.lambda.util.world.FastVector
 import com.lambda.util.world.toBlockPos
 import com.lambda.util.world.x
@@ -59,6 +60,13 @@ val SafeContext.worldName: String
 val World?.isOverworld: Boolean get() = this?.registryKey == World.OVERWORLD
 val World?.isNether: Boolean get() = this?.registryKey == World.NETHER
 val World?.isEnd: Boolean get() = this?.registryKey == World.END
+val World.dim: Dimension
+    get() = when {
+        isOverworld -> Dimension.Overworld
+        isNether -> Dimension.Nether
+        isEnd -> Dimension.End
+        else -> Dimension.Unknown
+    }
 val World?.dimensionName: String
     get() = when {
         isOverworld -> "Overworld"
diff --git a/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt b/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt
index 9e39a0cbd..cb50b9636 100644
--- a/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt
+++ b/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt
@@ -15,6 +15,8 @@
  * along with this program.  If not, see .
  */
 
+@file:Suppress("unused")
+
 package com.lambda.util.item
 
 import com.lambda.context.SafeContext
@@ -72,14 +74,30 @@ object ItemStackUtils {
         stack.getOrDefault(DataComponentTypes.ATTRIBUTE_MODIFIERS, AttributeModifiersComponent.DEFAULT)
             .filteredApply(attributeBaseValue(EntityAttributes.ATTACK_SPEED), EquipmentSlot.MAINHAND, EntityAttributes.ATTACK_SPEED)
 
+    const val NOTHING_MATCHED = -1
+
+    val Int.orZero get() = coerceAtLeast(0)
+
     val ItemStack.spaceLeft get() = maxCount - count
     val ItemStack.hasSpace get() = spaceLeft > 0
     val List.spaceLeft get() = sumOf { it.spaceLeft }
     val List.spaceLeft @JvmName("slotSpaceLeft") get() = sumOf { it.stack.spaceLeft }
-    val List.empty: Int get() = count { it.isEmpty }
-    val List.empty: Int @JvmName("slotEmpty") get() = count { it.stack.isEmpty }
-    val List.count: Int get() = if (isEmpty()) -1 else sumOf { it.count }
-    val List.count: Int @JvmName("slotCount") get() = if (isEmpty()) -1 else sumOf { it.stack.count }
+    val List.emptySpace: Int get() = count { it.isEmpty }
+    val List.emptySpace: Int @JvmName("emptySlotSpace") get() = count { it.stack.isEmpty }
+    val List.count: Int get() = if (isEmpty()) NOTHING_MATCHED else sumOf { it.count }
+    val List.count: Int @JvmName("slotCount") get() = if (isEmpty()) NOTHING_MATCHED else sumOf { it.stack.count }
+
+    fun ItemStack.count(countEmptyAsOne: Boolean): Int = if (isEmpty && countEmptyAsOne) 1 else count
+
+    fun List.count(countEmptyAsOne: Boolean): Int =
+        if (isEmpty()) NOTHING_MATCHED
+        else sumOf { it.count(countEmptyAsOne) }
+
+    @JvmName("slotCountMatching")
+    fun List.count(countEmptyAsOne: Boolean): Int =
+        if (isEmpty()) NOTHING_MATCHED
+        else sumOf { it.stack.count(countEmptyAsOne) }
+
     val List.copy: List get() = map { it.copy() }
 
     context(safeContext: SafeContext)
@@ -111,11 +129,11 @@ object ItemStackUtils {
         return listOf(copyWithCount(maxCount), copyWithCount(remainder))
     }
 
-    val ItemStack.shulkerBoxContents: List by cacheable { stack ->
+    val ItemStack.shulkerBoxStacks: List by cacheable { stack ->
         stack.components.get(DataComponentTypes.CONTAINER)?.stream()?.toList() ?: emptyList()
     }
 
-    val ItemStack.bundleContents: List by cacheable { stack ->
+    val ItemStack.bundleStacks: List by cacheable { stack ->
         stack.components.get(DataComponentTypes.BUNDLE_CONTENTS)?.stream()?.toList() ?: emptyList()
     }
 
diff --git a/src/main/kotlin/com/lambda/util/item/ItemUtils.kt b/src/main/kotlin/com/lambda/util/item/ItemUtils.kt
index 836aa1dba..361a26c8e 100644
--- a/src/main/kotlin/com/lambda/util/item/ItemUtils.kt
+++ b/src/main/kotlin/com/lambda/util/item/ItemUtils.kt
@@ -17,8 +17,7 @@
 
 package com.lambda.util.item
 
-import com.lambda.context.SafeContext
-import com.lambda.util.player.SlotUtils.armorSlots
+import com.lambda.interaction.container.containers.ArmorContainer
 import net.minecraft.block.Block
 import net.minecraft.component.DataComponentTypes
 import net.minecraft.item.BlockItem
@@ -27,130 +26,144 @@ import net.minecraft.item.ItemStack
 import net.minecraft.item.Items
 
 object ItemUtils {
-    val pickaxes = setOf(
-        Items.WOODEN_PICKAXE,
-        Items.STONE_PICKAXE,
-        Items.IRON_PICKAXE,
-        Items.GOLDEN_PICKAXE,
-        Items.DIAMOND_PICKAXE,
-        Items.NETHERITE_PICKAXE,
-    )
-
-    val shovels = setOf(
-        Items.WOODEN_SHOVEL,
-        Items.STONE_SHOVEL,
-        Items.IRON_SHOVEL,
-        Items.GOLDEN_SHOVEL,
-        Items.DIAMOND_SHOVEL,
-        Items.NETHERITE_SHOVEL,
-    )
-
-    val axes = setOf(
-        Items.WOODEN_AXE,
-        Items.STONE_AXE,
-        Items.IRON_AXE,
-        Items.GOLDEN_AXE,
-        Items.DIAMOND_AXE,
-        Items.NETHERITE_AXE,
-    )
-
-    val hoes = setOf(
-        Items.WOODEN_HOE,
-        Items.STONE_HOE,
-        Items.IRON_HOE,
-        Items.GOLDEN_HOE,
-        Items.DIAMOND_HOE,
-        Items.NETHERITE_HOE,
-    )
-
-    val swords = setOf(
-        Items.WOODEN_SWORD,
-        Items.STONE_SWORD,
-        Items.IRON_SWORD,
-        Items.GOLDEN_SWORD,
-        Items.DIAMOND_SWORD,
-        Items.NETHERITE_SWORD,
-    )
-
-    val misc = setOf(
-        Items.SHEARS,
-        Items.FLINT_AND_STEEL,
-    )
-
-    val tools = pickaxes + shovels + axes + hoes + swords + misc
-
-    val shulkerBoxes = setOf(
-        Items.SHULKER_BOX,
-        Items.WHITE_SHULKER_BOX,
-        Items.ORANGE_SHULKER_BOX,
-        Items.MAGENTA_SHULKER_BOX,
-        Items.LIGHT_BLUE_SHULKER_BOX,
-        Items.YELLOW_SHULKER_BOX,
-        Items.LIME_SHULKER_BOX,
-        Items.PINK_SHULKER_BOX,
-        Items.GRAY_SHULKER_BOX,
-        Items.LIGHT_GRAY_SHULKER_BOX,
-        Items.CYAN_SHULKER_BOX,
-        Items.PURPLE_SHULKER_BOX,
-        Items.BLUE_SHULKER_BOX,
-        Items.BROWN_SHULKER_BOX,
-        Items.GREEN_SHULKER_BOX,
-        Items.RED_SHULKER_BOX,
-        Items.BLACK_SHULKER_BOX,
-    )
-
-    val bundles = setOf(
-        Items.BUNDLE,
-        Items.WHITE_BUNDLE,
-        Items.ORANGE_BUNDLE,
-        Items.MAGENTA_BUNDLE,
-        Items.LIGHT_BLUE_BUNDLE,
-        Items.YELLOW_BUNDLE,
-        Items.LIME_BUNDLE,
-        Items.PINK_BUNDLE,
-        Items.GRAY_BUNDLE,
-        Items.LIGHT_GRAY_BUNDLE,
-        Items.CYAN_BUNDLE,
-        Items.PURPLE_BUNDLE,
-        Items.BLUE_BUNDLE,
-        Items.BROWN_BUNDLE,
-        Items.GREEN_BUNDLE,
-        Items.RED_BUNDLE,
-        Items.BLACK_BUNDLE
-    )
-
-    val chests = setOf(
-        Items.CHEST,
-        Items.TRAPPED_CHEST,
-        Items.ENDER_CHEST,
-        Items.BARREL,
-    )
-
-    val defaultDisposables = setOf(
-        Items.DIRT,
-        Items.GRASS_BLOCK,
-        Items.COBBLESTONE,
-        Items.GRANITE,
-        Items.DIORITE,
-        Items.ANDESITE,
-        Items.SANDSTONE,
-        Items.RED_SANDSTONE,
-        Items.NETHERRACK,
-        Items.END_STONE,
-        Items.STONE,
-        Items.BASALT,
-        Items.BLACKSTONE,
-        Items.COBBLED_DEEPSLATE
-    )
+    val PICKAXES =
+        setOf(
+           Items.WOODEN_PICKAXE,
+            Items.STONE_PICKAXE,
+            Items.COPPER_PICKAXE,
+            Items.IRON_PICKAXE,
+            Items.GOLDEN_PICKAXE,
+            Items.DIAMOND_PICKAXE,
+            Items.NETHERITE_PICKAXE,
+        )
+
+    val SHOVELS =
+        setOf(
+            Items.WOODEN_SHOVEL,
+            Items.STONE_SHOVEL,
+            Items.COPPER_SHOVEL,
+            Items.IRON_SHOVEL,
+            Items.GOLDEN_SHOVEL,
+            Items.DIAMOND_SHOVEL,
+            Items.NETHERITE_SHOVEL,
+        )
+
+    val AXES =
+        setOf(
+            Items.WOODEN_AXE,
+            Items.STONE_AXE,
+            Items.COPPER_AXE,
+            Items.IRON_AXE,
+            Items.GOLDEN_AXE,
+            Items.DIAMOND_AXE,
+            Items.NETHERITE_AXE,
+        )
+
+    val HOES =
+        setOf(
+            Items.WOODEN_HOE,
+            Items.STONE_HOE,
+            Items.COPPER_HOE,
+            Items.IRON_HOE,
+            Items.GOLDEN_HOE,
+            Items.DIAMOND_HOE,
+            Items.NETHERITE_HOE,
+        )
+
+    val SWORDS =
+        setOf(
+            Items.WOODEN_SWORD,
+            Items.STONE_SWORD,
+            Items.COPPER_SWORD,
+            Items.IRON_SWORD,
+            Items.GOLDEN_SWORD,
+            Items.DIAMOND_SWORD,
+            Items.NETHERITE_SWORD,
+        )
+
+    val MISC =
+        setOf(
+            Items.SHEARS,
+            Items.FLINT_AND_STEEL,
+        )
+
+    val TOOLS = PICKAXES + SHOVELS + AXES + HOES + SWORDS + MISC
+
+    val SHULKER_BOXES =
+        setOf(
+            Items.SHULKER_BOX,
+            Items.WHITE_SHULKER_BOX,
+            Items.ORANGE_SHULKER_BOX,
+            Items.MAGENTA_SHULKER_BOX,
+            Items.LIGHT_BLUE_SHULKER_BOX,
+            Items.YELLOW_SHULKER_BOX,
+            Items.LIME_SHULKER_BOX,
+            Items.PINK_SHULKER_BOX,
+            Items.GRAY_SHULKER_BOX,
+            Items.LIGHT_GRAY_SHULKER_BOX,
+            Items.CYAN_SHULKER_BOX,
+            Items.PURPLE_SHULKER_BOX,
+            Items.BLUE_SHULKER_BOX,
+            Items.BROWN_SHULKER_BOX,
+            Items.GREEN_SHULKER_BOX,
+            Items.RED_SHULKER_BOX,
+            Items.BLACK_SHULKER_BOX,
+        )
+
+    val BUNDLES =
+        setOf(
+            Items.BUNDLE,
+            Items.WHITE_BUNDLE,
+            Items.ORANGE_BUNDLE,
+            Items.MAGENTA_BUNDLE,
+            Items.LIGHT_BLUE_BUNDLE,
+            Items.YELLOW_BUNDLE,
+            Items.LIME_BUNDLE,
+            Items.PINK_BUNDLE,
+            Items.GRAY_BUNDLE,
+            Items.LIGHT_GRAY_BUNDLE,
+            Items.CYAN_BUNDLE,
+            Items.PURPLE_BUNDLE,
+            Items.BLUE_BUNDLE,
+            Items.BROWN_BUNDLE,
+            Items.GREEN_BUNDLE,
+            Items.RED_BUNDLE,
+            Items.BLACK_BUNDLE
+        )
+
+    val CHESTS =
+        setOf(
+            Items.CHEST,
+            Items.COPPER_CHEST,
+            Items.TRAPPED_CHEST,
+            Items.ENDER_CHEST
+        )
+
+    val DEFAULT_DISPOSABLES =
+        setOf(
+            Items.DIRT,
+            Items.GRASS_BLOCK,
+            Items.COBBLESTONE,
+            Items.GRANITE,
+            Items.DIORITE,
+            Items.ANDESITE,
+            Items.SANDSTONE,
+            Items.RED_SANDSTONE,
+            Items.NETHERRACK,
+            Items.END_STONE,
+            Items.STONE,
+            Items.BASALT,
+            Items.BLACKSTONE,
+            Items.COBBLED_DEEPSLATE
+        )
 
     val Item.block: Block get() = Block.getBlockFromItem(this)
     val ItemStack.blockItem get() = item as? BlockItem
 
     val Item.nutrition: Int get() = components.get(DataComponentTypes.FOOD)?.nutrition ?: 0
 
-    context(safeContext: SafeContext)
     val ItemStack.armorSlot get() =
-        safeContext.player.armorSlots.firstOrNull { it.canInsert(this) }
+        ArmorContainer.slots.firstOrNull { it.canInsert(this) }
 
     fun Int.toItemCount(): String {
         if (this < 0) {
diff --git a/src/main/kotlin/com/lambda/util/item/StackMovePlanner.kt b/src/main/kotlin/com/lambda/util/item/StackMovePlanner.kt
new file mode 100644
index 000000000..a237d626e
--- /dev/null
+++ b/src/main/kotlin/com/lambda/util/item/StackMovePlanner.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.util.item
+
+object StackMovePlanner {
+	enum class Target {
+		Source,
+		Destination
+	}
+
+	enum class Button(val id: Int) {
+		Left(0),
+		Right(1)
+	}
+
+	data class Click(val target: Target, val button: Button)
+
+	data class State(val cursor: Int, val source: Int, val destination: Int) {
+		val total get() = cursor + source + destination
+
+		operator fun get(target: Target) =
+			when (target) {
+				Target.Source -> source
+				Target.Destination -> destination
+			}
+
+		fun withSlot(target: Target, count: Int) =
+			when (target) {
+				Target.Source -> copy(source = count)
+				Target.Destination -> copy(destination = count)
+			}
+
+		fun fits(maxCount: Int) =
+			cursor in 0..maxCount && source in 0..maxCount && destination in 0..maxCount
+
+		fun settled(count: Int) = State(cursor = 0, source = total - count, destination = count)
+	}
+
+	data class Plan(val clicks: List, val destination: Int)
+
+	private data class Step(val previous: State, val click: Click)
+
+	private val CLICKS = Target.entries.flatMap { target -> Button.entries.map { Click(target, it) } }
+
+	fun planWithin(state: State, maxCount: Int, goal: Int, clickBudget: Int): Plan? {
+		if (!state.fits(maxCount)) return null
+		val steps = explore(state, maxCount, maxClicks = clickBudget)
+		val highest = minOf(goal, maxCount, state.total)
+		return (highest downTo state.destination + 1).firstNotNullOfOrNull { count ->
+			steps.pathTo(state.settled(count))?.let { Plan(it, count) }
+		}
+	}
+
+	fun plan(state: State, maxCount: Int, goal: Int): List? {
+		val end = state.settled(goal)
+		if (!state.fits(maxCount) || !end.fits(maxCount)) return null
+		return explore(state, maxCount, maxClicks = Int.MAX_VALUE).pathTo(end)
+	}
+
+	private fun explore(start: State, maxCount: Int, maxClicks: Int): Map {
+		val steps = hashMapOf(start to null)
+		var frontier = listOf(start)
+		var depth = 0
+		while (frontier.isNotEmpty() && depth < maxClicks) {
+			frontier =
+				frontier.flatMap { state ->
+					CLICKS.mapNotNull { click ->
+						apply(state, click, maxCount)
+							.takeUnless { it in steps }
+							?.also { steps[it] = Step(state, click) }
+					}
+				}
+			depth++
+		}
+		return steps
+	}
+
+	private fun Map.pathTo(end: State): List? {
+		if (end !in this) return null
+		return generateSequence(this[end]) { this[it.previous] }
+			.map { it.click }
+			.toList()
+			.asReversed()
+	}
+
+	fun apply(state: State, click: Click, maxCount: Int): State =
+		if (state.cursor == 0) state.pickUp(click) else state.place(click, maxCount)
+
+	private fun State.pickUp(click: Click): State {
+		val slot = this[click.target]
+		val taken =
+			when (click.button) {
+				Button.Left -> slot
+				Button.Right -> (slot + 1) / 2
+			}
+		return copy(cursor = taken).withSlot(click.target, slot - taken)
+	}
+
+	private fun State.place(click: Click, maxCount: Int): State {
+		val slot = this[click.target]
+		val offered =
+			when (click.button) {
+				Button.Left -> cursor
+				Button.Right -> 1
+			}
+		val placed = minOf(offered, maxCount - slot).coerceAtLeast(0)
+		return copy(cursor = cursor - placed).withSlot(click.target, slot + placed)
+	}
+}
diff --git a/src/main/kotlin/com/lambda/util/math/Linear.kt b/src/main/kotlin/com/lambda/util/math/Linear.kt
index fe0360966..de0ab8c2e 100644
--- a/src/main/kotlin/com/lambda/util/math/Linear.kt
+++ b/src/main/kotlin/com/lambda/util/math/Linear.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.util.math
 
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import net.minecraft.util.math.Box
 import net.minecraft.util.math.Vec3d
 import java.awt.Color
diff --git a/src/main/kotlin/com/lambda/util/math/Vectors.kt b/src/main/kotlin/com/lambda/util/math/Vectors.kt
index b43767cae..baaa02434 100644
--- a/src/main/kotlin/com/lambda/util/math/Vectors.kt
+++ b/src/main/kotlin/com/lambda/util/math/Vectors.kt
@@ -107,7 +107,7 @@ fun Vec3d.approximate(other: Vec3d, precision: Double = 2.0E-4): Boolean =
 val Vec3d.roundedBlockPos: BlockPos
     get() = BlockPos(x.roundToInt(), y.roundToInt(), z.roundToInt())
 
-val Vec3d.flooredBlockPos: BlockPos
+val Vec3d.blockPos: BlockPos
     get() = BlockPos(x.floorToInt(), y.floorToInt(), z.floorToInt())
 
 val Entity.netherCoord: Vec3d get() = pos.multiply(0.125, 1.0, 0.125)
@@ -205,7 +205,7 @@ infix fun Entity.distSq(other: Vec3i): Int = blockPos distSq other
 infix fun Entity.distSq(other: Entity): Double = squaredDistanceTo(other)
 
 context(safeContext: SafeContext)
-val Vec3d.isLoaded get() = flooredBlockPos.isLoaded
+val Vec3d.isLoaded get() = blockPos.isLoaded
 
 val UP = Vec3d(0.0, 1.0, 0.0)
 val DOWN = Vec3d(0.0, -1.0, 0.0)
diff --git a/src/main/kotlin/com/lambda/util/player/MovementUtils.kt b/src/main/kotlin/com/lambda/util/player/MovementUtils.kt
index 115a6cabc..b00b6cada 100644
--- a/src/main/kotlin/com/lambda/util/player/MovementUtils.kt
+++ b/src/main/kotlin/com/lambda/util/player/MovementUtils.kt
@@ -18,7 +18,7 @@
 package com.lambda.util.player
 
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.util.math.MathUtils.toDegree
 import com.lambda.util.math.MathUtils.toDouble
 import com.lambda.util.math.MathUtils.toRadian
diff --git a/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt b/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt
index f381daac1..574394b7a 100644
--- a/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt
+++ b/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt
@@ -19,7 +19,7 @@ package com.lambda.util.player
 
 import com.lambda.config.blocks.BuildConfig
 import com.lambda.context.SafeContext
-import com.lambda.interaction.handlers.GlideHandler
+import com.lambda.interaction.handler.handlers.GlideHandler
 import com.lambda.util.extension.getBlockState
 import com.lambda.util.player.MovementUtils.sneaking
 import com.lambda.util.world.fastEntitySearch
diff --git a/src/main/kotlin/com/lambda/util/player/RotationUtils.kt b/src/main/kotlin/com/lambda/util/player/RotationUtils.kt
index 840cf5670..cb0af20ec 100644
--- a/src/main/kotlin/com/lambda/util/player/RotationUtils.kt
+++ b/src/main/kotlin/com/lambda/util/player/RotationUtils.kt
@@ -23,9 +23,9 @@ import com.lambda.context.SafeContext
 import com.lambda.interaction.construction.simulation.processing.PreProcessingData
 import com.lambda.interaction.construction.verify.ScanMode
 import com.lambda.interaction.construction.verify.SurfaceScan
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo
-import com.lambda.interaction.managers.rotating.RotationManager
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.rotationTo
+import com.lambda.interaction.manager.managers.rotating.RotationManager
 import com.lambda.module.modules.client.Client
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.PlaceDirection
diff --git a/src/main/kotlin/com/lambda/util/player/SlotUtils.kt b/src/main/kotlin/com/lambda/util/player/SlotUtils.kt
index d09c0ae32..740520939 100644
--- a/src/main/kotlin/com/lambda/util/player/SlotUtils.kt
+++ b/src/main/kotlin/com/lambda/util/player/SlotUtils.kt
@@ -22,6 +22,8 @@ import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen
 import net.minecraft.client.network.ClientPlayerEntity
 import net.minecraft.entity.player.PlayerInventory
 import net.minecraft.item.ItemStack
+import net.minecraft.screen.ScreenHandler
+import net.minecraft.screen.ScreenHandlerType
 import net.minecraft.screen.slot.ArmorSlot
 import net.minecraft.screen.slot.Slot
 import net.minecraft.screen.slot.SlotActionType
@@ -29,29 +31,47 @@ import net.minecraft.screen.slot.SlotActionType
 object SlotUtils {
     val ClientPlayerEntity.allSlots get() = currentScreenHandler.slots.filter { it.inventory is PlayerInventory }
     val ClientPlayerEntity.armorSlots get() = allSlots.filterIsInstance()
-    val ClientPlayerEntity.inventorySlots: List get() =
-        if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) emptyList()
-        else if (currentScreenHandler === playerScreenHandler) allSlots.subList(4, 31)
-        else allSlots.subList(0, 27)
-    val ClientPlayerEntity.hotbarSlots: List get() =
-        if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) allSlots
-        else if (currentScreenHandler === playerScreenHandler) allSlots.subList(31, 40)
-        else allSlots.subList(27, 36)
-    val ClientPlayerEntity.hotbarAndInventorySlots: List get() =
-        if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) allSlots
-        else if (currentScreenHandler === playerScreenHandler) allSlots.subList(4, 40)
-        else allSlots.subList(0, 36)
+
+    val ClientPlayerEntity.inventorySlots: List
+        get() =
+            if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) emptyList()
+            else if (currentScreenHandler === playerScreenHandler) allSlots.subList(4, 31)
+            else allSlots.subList(0, 27)
+
+    val ClientPlayerEntity.hotbarSlots: List
+        get() =
+            if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) allSlots
+            else if (currentScreenHandler === playerScreenHandler) allSlots.subList(31, 40)
+            else allSlots.subList(27, 36)
+
+    val ClientPlayerEntity.hotbarAndInventorySlots: List
+        get() =
+            if (currentScreenHandler is CreativeInventoryScreen.CreativeScreenHandler) allSlots
+            else if (currentScreenHandler === playerScreenHandler) allSlots.subList(4, 40)
+            else allSlots.subList(0, 36)
+
     val ClientPlayerEntity.mainHandSlots: List get() = listOf(hotbarSlots[inventory.selectedSlot])
     val ClientPlayerEntity.offHandSlots: List get() = if (currentScreenHandler === playerScreenHandler) listOf(allSlots.last()) else emptyList()
 
     val ClientPlayerEntity.allStacks: List get() = hotbarAndInventoryStacks + equipmentStacks
+    val ClientPlayerEntity.armorStacks: List get() = inventory.equipment.map.filter { it.key.index in 1..4 }.map { it.value }
     val ClientPlayerEntity.equipmentStacks: List get() = inventory.equipment.map.map { it.value }
     val ClientPlayerEntity.hotbarStacks: List get() = inventory.mainStacks.slice(0..8)
     val ClientPlayerEntity.inventoryStacks: List get() = inventory.mainStacks.slice(9..35)
     val ClientPlayerEntity.hotbarAndInventoryStacks: List get() = inventory.mainStacks
 
+    val ScreenHandler.typeSafe: ScreenHandlerType<*>?
+        get() = try { this.type } catch (_: Throwable) { null }
+
     fun SafeContext.clickSlot(slotId: Int, button: Int, actionType: SlotActionType) {
         val syncId = player.currentScreenHandler?.syncId ?: return
         interaction.clickSlot(syncId, slotId, button, actionType, player)
     }
+
+    infix fun Slot.matches(slot: Slot) =
+        index == slot.index &&
+                inventory::class == slot.inventory::class &&
+                id == slot.id &&
+                x == slot.x &&
+                y == slot.y
 }
diff --git a/src/main/kotlin/com/lambda/util/player/prediction/PredictionEntity.kt b/src/main/kotlin/com/lambda/util/player/prediction/PredictionEntity.kt
index 17a9a4d7c..25a93a848 100644
--- a/src/main/kotlin/com/lambda/util/player/prediction/PredictionEntity.kt
+++ b/src/main/kotlin/com/lambda/util/player/prediction/PredictionEntity.kt
@@ -19,14 +19,14 @@ package com.lambda.util.player.prediction
 
 import com.lambda.Lambda.mc
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import com.lambda.module.modules.movement.SafeWalk.isNearLedge
 import com.lambda.threading.runSafe
 import com.lambda.util.BlockUtils.blockState
 import com.lambda.util.math.DOWN
 import com.lambda.util.math.MathUtils.toIntSign
 import com.lambda.util.math.MathUtils.toRadian
-import com.lambda.util.math.flooredBlockPos
+import com.lambda.util.math.blockPos
 import com.lambda.util.math.plus
 import com.lambda.util.math.times
 import com.lambda.util.player.MovementUtils.forward
@@ -85,7 +85,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
 
     // Other shit
     private var jumpingCooldown = player.jumpingCooldown
-    private var velocityAffectingPos = player.supportingBlockPos.orElse((position + DOWN * 0.001).flooredBlockPos)
+    private var velocityAffectingPos = player.supportingBlockPos.orElse((position + DOWN * 0.001).blockPos)
 
     private var horizontalCollision = player.horizontalCollision
     private var verticalCollision = player.verticalCollision
@@ -208,7 +208,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
         }
 
         val velocityMultiplier = run {
-            val f = blockState(position.flooredBlockPos).block.velocityMultiplier.toDouble()
+            val f = blockState(position.blockPos).block.velocityMultiplier.toDouble()
             val g = blockState(velocityAffectingPos).block.velocityMultiplier.toDouble()
             if (f == 1.0) g else f
         }
@@ -220,7 +220,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
             boundingBox = normalized.offset(position)
         }
 
-        velocityAffectingPos = (position + DOWN * 0.001).flooredBlockPos
+        velocityAffectingPos = (position + DOWN * 0.001).blockPos
     }
 
     /** @see net.minecraft.entity.LivingEntity.jump */
@@ -232,7 +232,7 @@ class PredictionEntity(val player: ClientPlayerEntity) {
 
         /** @see net.minecraft.entity.Entity.getJumpVelocityMultiplier */
         val jumpHeight = run {
-            val f = blockState(position.flooredBlockPos).block.jumpVelocityMultiplier.toDouble()
+            val f = blockState(position.blockPos).block.jumpVelocityMultiplier.toDouble()
             val g = blockState(velocityAffectingPos).block.jumpVelocityMultiplier.toDouble()
             if (f == 1.0) g else f
         } * 0.42 + player.jumpBoostVelocityModifier
diff --git a/src/main/kotlin/com/lambda/util/player/prediction/PredictionTick.kt b/src/main/kotlin/com/lambda/util/player/prediction/PredictionTick.kt
index 074a6c69d..3d05d802c 100644
--- a/src/main/kotlin/com/lambda/util/player/prediction/PredictionTick.kt
+++ b/src/main/kotlin/com/lambda/util/player/prediction/PredictionTick.kt
@@ -17,7 +17,7 @@
 
 package com.lambda.util.player.prediction
 
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import net.minecraft.util.math.Box
 import net.minecraft.util.math.Vec3d
 
diff --git a/src/main/kotlin/com/lambda/util/world/Dimension.kt b/src/main/kotlin/com/lambda/util/world/Dimension.kt
new file mode 100644
index 000000000..d7fb95e62
--- /dev/null
+++ b/src/main/kotlin/com/lambda/util/world/Dimension.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+package com.lambda.util.world
+
+enum class Dimension {
+	Overworld,
+	Nether,
+	End,
+	Unknown
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/util/world/WorldUtils.kt b/src/main/kotlin/com/lambda/util/world/WorldUtils.kt
index e9b3e4fe7..a13fa4684 100644
--- a/src/main/kotlin/com/lambda/util/world/WorldUtils.kt
+++ b/src/main/kotlin/com/lambda/util/world/WorldUtils.kt
@@ -171,13 +171,13 @@ object WorldUtils {
         range: FastVector,
         step: FastVector,
     ) = sequence {
-        for (x in -range.x..range.x step step.x) {
-            for (y in -range.y..range.y step step.y) {
-                for (z in -range.z..range.z step step.z) {
-                    yield(pos plus fastVectorOf(x, y, z))
-                }
-            }
-        }
+	    (-range.x..range.x step step.x).forEach { x ->
+		    (-range.y..range.y step step.y).forEach { y ->
+			    (-range.z..range.z step step.z).forEach { z ->
+				    yield(pos plus fastVectorOf(x, y, z))
+			    }
+		    }
+	    }
     }
 }
 
diff --git a/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt b/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt
index 343b0e8ca..91ac2263e 100644
--- a/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt
+++ b/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt
@@ -19,7 +19,7 @@ package com.lambda.util.world.raycast
 
 import com.lambda.Lambda.mc
 import com.lambda.context.SafeContext
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import com.lambda.util.math.distSq
 import net.minecraft.client.network.ClientPlayerEntity
 import net.minecraft.entity.Entity
diff --git a/src/test/kotlin/InventoryLedgerTest.kt b/src/test/kotlin/InventoryLedgerTest.kt
new file mode 100644
index 000000000..a1142ff95
--- /dev/null
+++ b/src/test/kotlin/InventoryLedgerTest.kt
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+import com.lambda.interaction.manager.managers.inventory.InventoryLedger
+import com.lambda.interaction.manager.managers.inventory.InventoryLedger.SingleDecision.Apply
+import com.lambda.interaction.manager.managers.inventory.InventoryLedger.SingleDecision.Drop
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Instant
+
+/**
+ * Challenges [InventoryLedger] with the exact failure modes of the old value-matching cache:
+ * delayed echoes after rapid actions, stale full snapshots clobbering newer slots, repeated
+ * identical values, orphaned entries eating real corrections, and server rejections.
+ *
+ * Stacks are plain data (no Minecraft classes) so these run as fast JVM unit tests.
+ */
+class InventoryLedgerTest {
+
+    private data class TestStack(val item: String, val count: Int)
+
+    private fun stone(count: Int) = TestStack("stone", count)
+    private fun dirt(count: Int) = TestStack("dirt", count)
+
+    private class ManualClock(var now: Instant = Instant.fromEpochMilliseconds(0)) {
+        fun advance(ms: Long) {
+            now = now.plus(ms.milliseconds)
+        }
+    }
+
+    private fun ledger(clock: ManualClock, maxAgeMs: Long = 60_000L) =
+        InventoryLedger({ a, b -> a == b }, maxAgeMs = maxAgeMs, clock = { clock.now })
+
+    @Test
+    fun `twelve rapid placements with delayed echoes keep the predicted count`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+        val slot = 36
+
+        // Client predicts 64 -> 52 while the server answers are still in flight.
+        var count = 64
+        repeat(12) {
+            ledger.record(0, slot, stone(count), stone(count - 1))
+            count--
+        }
+        assertEquals(52, count)
+        assertEquals(12, ledger.pendingCount(0, slot))
+
+        // The 12 echoes arrive late, after all predictions. Every one must be dropped.
+        repeat(12) { i ->
+            assertEquals(Drop, ledger.decideSingle(0, slot, stone(63 - i)), "echo ${63 - i} must be dropped")
+        }
+        assertEquals(0, ledger.totalPending())
+
+        // Something genuinely new (blocks removed by something else) must still apply.
+        assertEquals(Apply, ledger.decideSingle(0, slot, stone(48)))
+    }
+
+    @Test
+    fun `stale full snapshot does not clobber slots changed after it was sent`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // Slot 0 was decremented, then slot 1 changed after the server snapshot was taken.
+        ledger.record(0, 0, stone(64), stone(63))
+        ledger.record(0, 1, dirt(5), dirt(4))
+
+        val client = listOf(stone(63), dirt(4), stone(10))
+        // Server snapshot predates both changes; slot 2 has an unrelated external change.
+        val incoming = listOf(stone(64), dirt(5), stone(11))
+
+        assertEquals(listOf(false, false, true), ledger.decideFull(0, incoming, client))
+
+        // Nothing was consumed for the stale slots, so the real echo still drops afterwards.
+        assertEquals(1, ledger.pendingCount(0, 0))
+        assertEquals(Drop, ledger.decideSingle(0, 0, stone(63)))
+    }
+
+    @Test
+    fun `repeated identical values consume oldest first without orphans`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // Place (64->63), pick up (63->64), place again (64->63).
+        ledger.record(0, 36, stone(64), stone(63))
+        ledger.record(0, 36, stone(63), stone(64))
+        ledger.record(0, 36, stone(64), stone(63))
+
+        assertEquals(Drop, ledger.decideSingle(0, 36, stone(63)))
+        assertEquals(Drop, ledger.decideSingle(0, 36, stone(64)))
+        assertEquals(Drop, ledger.decideSingle(0, 36, stone(63)))
+        assertEquals(0, ledger.totalPending())
+
+        // A later genuine correction must not be eaten by a leftover entry.
+        assertEquals(Apply, ledger.decideSingle(0, 36, stone(60)))
+    }
+
+    @Test
+    fun `click records once and a later correction still applies`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // A click moves the stack; the forced full update echoes it back.
+        // (Slot 0, because full-packet list positions map to slot ids.)
+        ledger.record(0, 0, TestStack("sword", 1), TestStack("shield", 1))
+
+        val apply = ledger.decideFull(0, listOf(TestStack("shield", 1)), listOf(TestStack("shield", 1)))
+        assertEquals(listOf(true), apply)
+        assertEquals(0, ledger.totalPending(), "single record must be fully consumed, no orphan may remain")
+
+        // A duplicate delivery of the same value applies harmlessly (same value either way).
+        assertEquals(Apply, ledger.decideSingle(0, 0, TestStack("shield", 1)))
+
+        // A real change afterwards is new information.
+        ledger.record(0, 0, TestStack("shield", 1), dirt(2))
+        assertEquals(Apply, ledger.decideSingle(0, 0, stone(30)))
+        assertEquals(0, ledger.pendingCount(0, 0))
+    }
+
+    @Test
+    fun `server rejection of a prediction reverts to the old value`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // Client predicted a placement the server refused: the old count comes back.
+        ledger.record(0, 36, stone(64), stone(63))
+        assertEquals(Apply, ledger.decideSingle(0, 36, stone(64)))
+        assertEquals(0, ledger.pendingCount(0, 36))
+    }
+
+    @Test
+    fun `external value in a full snapshot applies and clears stale predictions`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(0, 0, stone(64), stone(63))
+        val apply = ledger.decideFull(0, listOf(stone(70)), listOf(stone(63)))
+        assertEquals(listOf(true), apply)
+        assertEquals(0, ledger.pendingCount(0, 0))
+    }
+
+    @Test
+    fun `expired predictions no longer filter echoes`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock, maxAgeMs = 100L)
+
+        ledger.record(0, 36, stone(64), stone(63))
+        clock.advance(500L)
+        // The recording has decayed, so the late echo is treated as new and applied.
+        assertEquals(Apply, ledger.decideSingle(0, 36, stone(63)))
+        assertEquals(0, ledger.totalPending())
+    }
+
+    @Test
+    fun `screens do not share predictions`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(5, 3, stone(10), stone(9))
+        assertEquals(Apply, ledger.decideSingle(0, 3, stone(9)), "other screens must not consume this")
+        assertEquals(Drop, ledger.decideSingle(5, 3, stone(9)))
+        ledger.record(5, 3, stone(9), stone(8))
+        ledger.clear(5)
+        assertEquals(Apply, ledger.decideSingle(5, 3, stone(8)))
+    }
+
+    @Test
+    fun `full snapshot with mismatched sizes applies everything`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(0, 0, stone(64), stone(63))
+        val apply = ledger.decideFull(0, listOf(stone(1), stone(2)), listOf(stone(63)))
+        assertEquals(listOf(true, true), apply)
+    }
+
+    @Test
+    fun `recording an unchanged slot stores nothing`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(0, 0, stone(64), stone(64))
+        assertEquals(0, ledger.totalPending())
+        assertEquals(Apply, ledger.decideSingle(0, 0, stone(64)))
+    }
+
+    @Test
+    fun `random predictions with in-order echoes are all dropped`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+        val random = Random(1234)
+        val slots = listOf(36, 37, 38)
+        val expected = mutableMapOf>()
+
+        repeat(200) {
+            val slot = slots.random(random)
+            val current = expected[slot]?.lastOrNull() ?: stone(64)
+            val next = current.copy(count = (current.count + random.nextInt(-3, 2)).coerceIn(0, 64))
+            if (next != current) {
+                ledger.record(0, slot, current, next)
+                expected.getOrPut(slot) { ArrayDeque() }.addLast(next)
+            }
+            clock.advance(1L)
+        }
+
+        // Server echoes arrive in order per slot; each must be recognised as our own action.
+        expected.forEach { (slot, echoes) ->
+            echoes.forEach { echo ->
+                assertEquals(Drop, ledger.decideSingle(0, slot, echo), "echo $echo on slot $slot")
+            }
+        }
+        assertEquals(0, ledger.totalPending())
+
+        // And the ledger still accepts genuinely new values afterwards.
+        assertTrue(ledger.decideFull(0, listOf(dirt(1)), listOf(dirt(1))) == listOf(true))
+    }
+
+    // ---- cursor: every click is sent with a mismatched revision, so the server answers each one
+    // with a full packet carrying the cursor. With rapid grab/place pairs that cursor is routinely
+    // stale on arrival and must be merged like a slot, or a resurrected cursor breaks the tasks
+    // that read it every tick. ------------------------------------------------------------------
+
+    private val cursor = InventoryLedger.CURSOR_SLOT_ID
+    private val empty = TestStack("air", 0)
+    private fun shulker(contents: String) = TestStack("shulker[$contents]", 1)
+
+    @Test
+    fun `stale cursor from an earlier click is dropped`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // Click 1 grabs the shulker, click 2 puts it into the destination slot.
+        ledger.record(0, cursor, empty, shulker("gear"))
+        ledger.record(0, 10, stone(5), shulker("gear"))
+        ledger.record(0, cursor, shulker("gear"), empty)
+
+        // The first full (cursor still holding the shulker) arrives after the second click.
+        assertEquals(Drop, ledger.decideCursor(0, shulker("gear")))
+        // The destination slot in the same stale full is merged the same way.
+        assertEquals(
+            listOf(true, true, true, true, true, true, true, true, true, true, false),
+            ledger.decideFull(0, List(10) { stone(5) } + stone(5), List(10) { stone(5) } + shulker("gear"))
+        )
+    }
+
+    @Test
+    fun `two-click grab and place consumes everything once both echoes arrive`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(0, cursor, empty, shulker("gear"))
+        ledger.record(0, 3, stone(5), shulker("gear"))
+        ledger.record(0, cursor, shulker("gear"), empty)
+
+        // First full arrives late: cursor grab is stale, destination already holds the shulker.
+        assertEquals(Drop, ledger.decideCursor(0, shulker("gear")))
+        assertEquals(listOf(true, true, true, false), ledger.decideFull(0, listOf(empty, empty, empty, stone(5)), listOf(empty, empty, empty, shulker("gear"))))
+
+        // Second full (everything current) drains the rest; nothing is left to false-match later.
+        assertEquals(Drop, ledger.decideCursor(0, empty))
+        assertEquals(listOf(true, true, true, true), ledger.decideFull(0, listOf(empty, empty, empty, shulker("gear")), listOf(empty, empty, empty, shulker("gear"))))
+        assertEquals(0, ledger.totalPending())
+
+        // A later genuine cursor correction still applies.
+        ledger.record(0, cursor, empty, dirt(2))
+        assertEquals(Apply, ledger.decideCursor(0, stone(30)))
+        assertEquals(0, ledger.pendingCount(0, cursor))
+    }
+
+    @Test
+    fun `novel server cursor applies and clears stale predictions`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(0, cursor, empty, shulker("gear"))
+        // Something genuinely new that matches neither end of the prediction.
+        assertEquals(Apply, ledger.decideCursor(0, dirt(2)))
+        assertEquals(0, ledger.pendingCount(0, cursor))
+    }
+
+    @Test
+    fun `redundant full keeps the newer prediction without consuming it`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        // Grab, then place: two predictions. A no-op click in between makes the server send
+        // a full carrying the intermediate cursor, which must neither apply nor consume.
+        ledger.record(0, cursor, empty, shulker("gear"))
+        ledger.record(0, cursor, shulker("gear"), empty)
+        assertEquals(Drop, ledger.decideCursor(0, shulker("gear")))
+        assertEquals(1, ledger.pendingCount(0, cursor))
+        assertEquals(Drop, ledger.decideCursor(0, shulker("gear")))
+        assertEquals(1, ledger.pendingCount(0, cursor))
+        assertEquals(Drop, ledger.decideCursor(0, empty))
+        assertEquals(0, ledger.pendingCount(0, cursor))
+    }
+
+    @Test
+    fun `cursor predictions are isolated per screen`() {
+        val clock = ManualClock()
+        val ledger = ledger(clock)
+
+        ledger.record(5, cursor, empty, shulker("gear"))
+        assertEquals(Apply, ledger.decideCursor(0, shulker("gear")))
+        assertEquals(Drop, ledger.decideSingle(5, cursor, shulker("gear")))
+    }
+}
diff --git a/src/test/kotlin/InventorySyncFilterTest.kt b/src/test/kotlin/InventorySyncFilterTest.kt
new file mode 100644
index 000000000..12cefb885
--- /dev/null
+++ b/src/test/kotlin/InventorySyncFilterTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+import com.lambda.interaction.manager.managers.inventory.InventoryLedger
+import com.lambda.interaction.manager.managers.inventory.InventorySyncFilter
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.time.Instant
+
+/**
+ * End-to-end tests for [InventorySyncFilter]: snapshot + ledger wiring, not just the ledger rules.
+ *
+ * The headline scenario is the nested-shulker failure: pick up a shulker box, move it into the
+ * ender chest on another screen, then pick up an *identical* shulker box into the *same* slot.
+ * The second pickup is new information from the server and must be applied. Previously the
+ * applied first pickup was re-recorded as a phantom prediction on the next diff (the snapshot was
+ * never refreshed), and that phantom matched the identical second shulker, so the update was
+ * dropped and the shulker never appeared client-side.
+ *
+ * Stacks are plain data (no Minecraft classes) so these run as fast JVM unit tests.
+ */
+class InventorySyncFilterTest {
+    private data class TestStack(val item: String, val count: Int)
+
+    private val empty = TestStack("air", 0)
+    private fun shulker(contents: String) = TestStack("shulker[$contents]", 1)
+
+    private class ManualClock(var now: Instant = Instant.fromEpochMilliseconds(0))
+
+    private fun filter(clock: ManualClock, maxAgeMs: Long = 60_000L) =
+        InventorySyncFilter({ a, b -> a == b }, { it.copy() }, maxAgeMs = maxAgeMs, clock = { clock.now })
+
+    /** Player-screen slots: everything empty except [slot]. */
+    private fun playerSlots(slot: Int, stack: TestStack, size: Int = 46): Map =
+        (0 until size).associateWith { empty } + (slot to stack)
+
+    @Test
+    fun `identical shulker picked into the same slot applies`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+        val slot = 36
+        val first = shulker("ender_chest")
+        val second = shulker("ender_chest")
+        assertEquals(first, second, "test setup: the two shulkers must be identical")
+
+        // Baseline: the player screen as the client sees it.
+        filter.observeLocal(0, playerSlots(slot, empty), empty)
+
+        // The first shulker is picked up from the ground. Nothing predicted it, so it applies.
+        assertTrue(filter.onSingle(0, slot, first, empty))
+
+        // A tick passes with no local change. The applied pickup must NOT be re-recorded as
+        // something we did: there must be no phantom prediction left behind.
+        filter.observeLocal(0, playerSlots(slot, first), empty)
+        assertEquals(0, filter.totalPending(), "applied server values must refresh the snapshot")
+
+        // The first shulker moves into the ender chest on its screen (container-local ids,
+        // so these recordings live under that screen only).
+        filter.snapshot(5, mapOf(2 to empty, 40 to first), empty)
+        filter.observeLocal(5, mapOf(2 to first, 40 to empty), empty)
+        assertEquals(0, filter.pendingCount(0, slot))
+
+        // The second, identical shulker is picked up into the same slot. This is new
+        // information and must be applied, not mistaken for an echo of the first pickup.
+        assertTrue(
+            filter.onSingle(0, slot, second, empty),
+            "identical shulker into the same slot must be applied"
+        )
+        assertEquals(0, filter.pendingCount(0, slot))
+    }
+
+    @Test
+    fun `identical shulker picked into a different slot applies`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+
+        filter.observeLocal(0, playerSlots(36, empty), empty)
+        assertTrue(filter.onSingle(0, 36, shulker("ender_chest"), empty))
+        filter.observeLocal(0, playerSlots(36, shulker("ender_chest")), empty)
+
+        assertTrue(filter.onSingle(0, 37, shulker("ender_chest"), empty))
+    }
+
+    @Test
+    fun `local predictions still filter their echoes`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+        val slot = 36
+
+        filter.observeLocal(0, playerSlots(slot, TestStack("stone", 64)), empty)
+
+        // The client predicts placing a block; the late echo must be dropped.
+        filter.observeLocal(0, playerSlots(slot, TestStack("stone", 63)), empty)
+        assertEquals(1, filter.pendingCount(0, slot))
+        assertEquals(false, filter.onSingle(0, slot, TestStack("stone", 63), TestStack("stone", 63)))
+        assertEquals(0, filter.pendingCount(0, slot))
+
+        // And a correction straight afterwards still applies.
+        assertTrue(filter.onSingle(0, slot, TestStack("stone", 64), TestStack("stone", 63)))
+    }
+
+    @Test
+    fun `snapshot overwrites and per-screen state is isolated`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+
+        filter.observeLocal(0, playerSlots(36, TestStack("stone", 64)), empty)
+        filter.observeLocal(0, playerSlots(36, TestStack("stone", 63)), empty)
+        assertEquals(1, filter.pendingCount(0, 36))
+
+        // Re-snapshotting (screen reopened) does not clear predictions, but a different screen
+        // never sees them.
+        filter.snapshot(5, mapOf(0 to TestStack("stone", 1)), empty)
+        assertEquals(1, filter.pendingCount(0, 36))
+        assertTrue(filter.onSingle(5, 0, TestStack("stone", 1), TestStack("stone", 1)))
+
+        filter.clear(0)
+        assertEquals(0, filter.totalPending())
+    }
+
+    @Test
+    fun `stale cursor-only update is dropped`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+        val cursor = InventoryLedger.CURSOR_SLOT_ID
+
+        filter.observeLocal(0, playerSlots(0, empty), empty)
+        filter.observeLocal(0, playerSlots(0, empty), shulker("gear"))
+        filter.observeLocal(0, playerSlots(0, empty), empty)
+
+        // A cursor echo from after the grab arrives after the place: stale, drop it.
+        assertEquals(false, filter.onCursor(0, shulker("gear"), empty))
+        assertEquals(false, filter.onCursor(0, empty, empty))
+        assertEquals(0, filter.pendingCount(0, cursor))
+    }
+
+    @Test
+    fun `novel cursor applies and refreshes the snapshot`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+
+        filter.observeLocal(0, playerSlots(0, empty), empty)
+        filter.observeLocal(0, playerSlots(0, empty), shulker("gear"))
+
+        // The server rejected the grab and reports something unpredicted: apply it...
+        assertEquals(true, filter.onCursor(0, TestStack("dirt", 1), shulker("gear")))
+        // ...and the snapshot followed, so the next diff records no phantom.
+        filter.observeLocal(0, playerSlots(0, empty), TestStack("dirt", 1))
+        assertEquals(0, filter.totalPending())
+    }
+
+    @Test
+    fun `slot change times track local changes only`() {
+        val clock = ManualClock()
+        val filter = filter(clock)
+
+        // Baselines establish truth without stamping anything.
+        filter.observeLocal(0, playerSlots(5, empty), empty)
+        filter.snapshot(5, mapOf(2 to empty), empty)
+        assertEquals(null, filter.lastChange(0, 5))
+        assertEquals(null, filter.lastChange(5, 2))
+
+        // A local change stamps exactly that slot at the recording time.
+        clock.now = Instant.fromEpochMilliseconds(1000)
+        filter.observeLocal(0, playerSlots(5, shulker("gear")), empty)
+        assertEquals(Instant.fromEpochMilliseconds(1000), filter.lastChange(0, 5))
+        assertEquals(null, filter.lastChange(0, 6))
+        assertEquals(null, filter.lastChange(5, 2))
+
+        // Unchanged slots re-diffed later keep their original stamp.
+        clock.now = Instant.fromEpochMilliseconds(2000)
+        filter.observeLocal(0, playerSlots(5, shulker("gear")), empty)
+        assertEquals(Instant.fromEpochMilliseconds(1000), filter.lastChange(0, 5))
+    }
+
+    @Test
+    fun `slot change times expire and clear with the screen`() {
+        val clock = ManualClock()
+        val filter = filter(clock, maxAgeMs = 1500L)
+
+        filter.observeLocal(0, playerSlots(5, empty), empty)
+        clock.now = Instant.fromEpochMilliseconds(1000)
+        filter.observeLocal(0, playerSlots(5, shulker("gear")), empty)
+        assertEquals(Instant.fromEpochMilliseconds(1000), filter.lastChange(0, 5))
+
+        // Older than maxAgeMs: unknown again, so lookups keep their old order.
+        clock.now = Instant.fromEpochMilliseconds(3000)
+        assertEquals(null, filter.lastChange(0, 5))
+
+        clock.now = Instant.fromEpochMilliseconds(4000)
+        filter.observeLocal(0, playerSlots(5, shulker("other")), empty)
+        filter.clear(0)
+        assertEquals(null, filter.lastChange(0, 5))
+    }
+}
diff --git a/src/test/kotlin/PlaceDirectionTest.kt b/src/test/kotlin/PlaceDirectionTest.kt
index 02b5fb6e3..15c94436e 100644
--- a/src/test/kotlin/PlaceDirectionTest.kt
+++ b/src/test/kotlin/PlaceDirectionTest.kt
@@ -15,7 +15,7 @@
  * along with this program.  If not, see .
  */
 
-import com.lambda.interaction.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation
 import com.lambda.util.PlaceDirection
 import kotlin.test.Test
 import kotlin.test.assertEquals
diff --git a/src/test/kotlin/RotationTest.kt b/src/test/kotlin/RotationTest.kt
index f57a3c33e..7c182d8c1 100644
--- a/src/test/kotlin/RotationTest.kt
+++ b/src/test/kotlin/RotationTest.kt
@@ -15,12 +15,12 @@
  * along with this program.  If not, see .
  */
 
-import com.lambda.interaction.managers.rotating.Rotation
-import com.lambda.interaction.managers.rotating.Rotation.Companion.angleDifference
-import com.lambda.interaction.managers.rotating.Rotation.Companion.dist
-import com.lambda.interaction.managers.rotating.Rotation.Companion.lerp
-import com.lambda.interaction.managers.rotating.Rotation.Companion.slerp
-import com.lambda.interaction.managers.rotating.Rotation.Companion.wrap
+import com.lambda.interaction.manager.managers.rotating.Rotation
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.angleDifference
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.dist
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.lerp
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.slerp
+import com.lambda.interaction.manager.managers.rotating.Rotation.Companion.wrap
 import kotlin.math.abs
 import kotlin.math.hypot
 import kotlin.test.Test
diff --git a/src/test/kotlin/StackMovePlannerTest.kt b/src/test/kotlin/StackMovePlannerTest.kt
new file mode 100644
index 000000000..86bd50cc5
--- /dev/null
+++ b/src/test/kotlin/StackMovePlannerTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+import com.lambda.util.item.StackMovePlanner
+import com.lambda.util.item.StackMovePlanner.Button
+import com.lambda.util.item.StackMovePlanner.Click
+import com.lambda.util.item.StackMovePlanner.State
+import com.lambda.util.item.StackMovePlanner.Target
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class StackMovePlannerTest {
+	private fun simulate(start: State, clicks: List, maxCount: Int) =
+		clicks.fold(start) { state, click -> StackMovePlanner.apply(state, click, maxCount) }
+
+	private fun assertSettles(start: State, clicks: List, maxCount: Int, destination: Int) {
+		val end = simulate(start, clicks, maxCount)
+		assertEquals(destination, end.destination, "destination after $clicks from $start")
+		assertEquals(0, end.cursor, "cursor after $clicks from $start")
+		assertEquals(start.total, end.total, "items must be conserved")
+	}
+
+	private fun assertReaches(start: State, maxCount: Int, goal: Int): List {
+		val plan = assertNotNull(StackMovePlanner.plan(start, maxCount, goal), "no plan for $start -> $goal (max $maxCount)")
+		assertSettles(start, plan, maxCount, goal)
+		return plan
+	}
+
+	/** A random state within the model and a goal that is reachable from it. */
+	private fun Random.reachableCase(maxCount: Int): Pair? {
+		val source = nextInt(0, maxCount + 1)
+		val destination = nextInt(0, maxCount + 1)
+		val total = source + destination
+		val low = (total - maxCount).coerceAtLeast(0)
+		val high = minOf(maxCount, total)
+		if (low > high) return null
+		return State(0, source, destination) to nextInt(low, high + 1)
+	}
+
+	@Test
+	fun wholeStackIntoEmptySlotIsTwoClicks() {
+		val plan = assertReaches(State(0, 64, 0), 64, 64)
+		assertEquals(listOf(Click(Target.Source, Button.Left), Click(Target.Destination, Button.Left)), plan)
+	}
+
+	@Test
+	fun singleItemIsThreeClicks() {
+		val plan = assertReaches(State(0, 64, 0), 64, 1)
+		assertEquals(3, plan.size)
+	}
+
+	@Test
+	fun halfStackUsesRightClickPickup() {
+		val plan = assertReaches(State(0, 64, 0), 64, 32)
+		assertEquals(listOf(Click(Target.Source, Button.Right), Click(Target.Destination, Button.Left)), plan)
+	}
+
+	@Test
+	fun partialCountBeatsPlacingOneAtATime() {
+		val plan = assertReaches(State(0, 64, 0), 64, 10)
+		assertTrue(plan.size < 12, "expected a smarter plan than 12 single placements, got ${plan.size}")
+	}
+
+	@Test
+	fun mergesIntoPartialDestination() {
+		// The whole source fits: pick up, place.
+		assertEquals(2, assertReaches(State(0, 14, 50), 64, 64).size)
+		// Only 14 of 20 fit, so the surplus has to go back: pick up, place, return.
+		assertEquals(3, assertReaches(State(0, 20, 50), 64, 64).size)
+	}
+
+	@Test
+	fun continuesFromItemsAlreadyOnCursor() {
+		assertReaches(State(5, 10, 0), 64, 15)
+		assertReaches(State(30, 0, 10), 64, 20)
+	}
+
+	@Test
+	fun alreadySatisfiedNeedsNoClicks() {
+		assertEquals(emptyList(), StackMovePlanner.plan(State(0, 10, 7), 64, 7))
+	}
+
+	@Test
+	fun rejectsUnreachableGoals() {
+		assertNull(StackMovePlanner.plan(State(0, 10, 0), 64, 11))
+		assertNull(StackMovePlanner.plan(State(0, 64, 64), 64, 65))
+		assertNull(StackMovePlanner.plan(State(0, 64, 64), 64, 10), "source cannot hold the surplus")
+		assertNull(StackMovePlanner.plan(State(0, 5, 0), 0, 1))
+	}
+
+	@Test
+	fun rejectsStatesOutsideTheModel() {
+		// Counts above the stack size cannot be represented, so planning declines instead of guessing.
+		assertNull(StackMovePlanner.plan(State(70, 0, 0), 64, 64))
+		assertNull(StackMovePlanner.plan(State(0, 20, 0), 16, 16))
+		assertNull(StackMovePlanner.planWithin(State(70, 0, 0), 64, 64, clickBudget = 100))
+	}
+
+	@Test
+	fun unstackableItemsMoveWhole() {
+		val plan = assertReaches(State(0, 1, 0), 1, 1)
+		assertEquals(2, plan.size)
+	}
+
+	@Test
+	fun everyReachableGoalHasAValidPlan() {
+		val random = Random(1337)
+		for (maxCount in listOf(1, 16, 64)) {
+			repeat(300) {
+				val (start, goal) = random.reachableCase(maxCount) ?: return@repeat
+				assertReaches(start, maxCount, goal)
+			}
+		}
+	}
+
+	@Test
+	fun planWithinTakesTheGoalWhenItsPlanFitsTheBudget() {
+		val plan = assertNotNull(StackMovePlanner.planWithin(State(0, 64, 0), 64, goal = 32, clickBudget = 2))
+		assertEquals(32, plan.destination)
+		assertEquals(listOf(Click(Target.Source, Button.Right), Click(Target.Destination, Button.Left)), plan.clicks)
+	}
+
+	@Test
+	fun planWithinSettlesForFewerItemsWhenTheBudgetIsTight() {
+		// 20 items need more than three clicks; a single item needs exactly three (pick up, place one, put back).
+		val plan = assertNotNull(StackMovePlanner.planWithin(State(0, 64, 0), 64, goal = 20, clickBudget = 3))
+		assertEquals(1, plan.destination)
+		assertEquals(3, plan.clicks.size)
+		assertSettles(State(0, 64, 0), plan.clicks, 64, 1)
+	}
+
+	@Test
+	fun planWithinNeverLeavesItemsOnTheCursorForABiggerMove() {
+		// Picking up and placing one item is two clicks but would strand 63 items on the cursor.
+		assertNull(StackMovePlanner.planWithin(State(0, 64, 0), 64, goal = 20, clickBudget = 2))
+		assertNull(StackMovePlanner.planWithin(State(0, 64, 0), 64, goal = 20, clickBudget = 0))
+	}
+
+	@Test
+	fun planWithinClampsTheGoalToWhatCanBeHeld() {
+		val plan = assertNotNull(StackMovePlanner.planWithin(State(0, 14, 50), 64, goal = 70, clickBudget = 10))
+		assertEquals(64, plan.destination)
+		assertNull(StackMovePlanner.planWithin(State(0, 10, 10), 64, goal = 10, clickBudget = 10), "nothing to add")
+	}
+
+	@Test
+	fun planWithinIsAsGoodAsTheExactPlanAndNeverOvershoots() {
+		val random = Random(42)
+		for (maxCount in listOf(1, 16, 64)) {
+			repeat(300) {
+				val (start, goal) = random.reachableCase(maxCount) ?: return@repeat
+				if (goal <= start.destination) return@repeat
+				val budget = random.nextInt(0, 9)
+				val exact = assertNotNull(StackMovePlanner.plan(start, maxCount, goal))
+				val plan = StackMovePlanner.planWithin(start, maxCount, goal, budget)
+				if (exact.size <= budget) {
+					assertEquals(goal, assertNotNull(plan, "exact plan fits $budget clicks").destination)
+				}
+				if (plan == null) return@repeat
+				assertTrue(plan.clicks.size <= budget, "plan ${plan.clicks} exceeds the budget of $budget")
+				assertTrue(plan.destination in (start.destination + 1)..goal, "destination ${plan.destination} outside (${start.destination}, $goal]")
+				assertSettles(start, plan.clicks, maxCount, plan.destination)
+				assertEquals(
+					plan.clicks.size,
+					assertNotNull(StackMovePlanner.plan(start, maxCount, plan.destination)).size,
+					"a budgeted plan must still be a shortest plan"
+				)
+			}
+		}
+	}
+}
diff --git a/src/test/kotlin/TaskTreeTest.kt b/src/test/kotlin/TaskTreeTest.kt
new file mode 100644
index 000000000..e958c66b9
--- /dev/null
+++ b/src/test/kotlin/TaskTreeTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see .
+ */
+
+import com.lambda.task.Task
+import com.lambda.task.tasks.wrappers.then
+import com.lambda.task.tasks.wrappers.thenAction
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class TaskTreeTest {
+    private class TestTask(
+        override val name: String,
+        override val isInvisible: Boolean = false,
+    ) : Task()
+
+    @Test
+    fun `invisible tasks are hidden and their children are promoted`() {
+        val root = TestTask("Root")
+        val wrapper = TestTask("Wrapper", isInvisible = true)
+        val child = TestTask("Child")
+        root.subTasks += wrapper
+        wrapper.subTasks += child
+
+        val tree = root.taskTree()
+
+        assertFalse(tree.contains("Wrapper"))
+        assertTrue(tree.lineSequence().any { it.startsWith("    Child") })
+    }
+
+    @Test
+    fun `invisible tasks are shown when requested`() {
+        val root = TestTask("Root")
+        val wrapper = TestTask("Wrapper", isInvisible = true)
+        val child = TestTask("Child")
+        root.subTasks += wrapper
+        wrapper.subTasks += child
+
+        val tree = root.taskTree(showInvisible = true)
+
+        assertTrue(tree.lineSequence().any { it.startsWith("    Wrapper") })
+        assertTrue(tree.lineSequence().any { it.startsWith("        Child") })
+    }
+
+    @Test
+    fun `chaining utilities are invisible`() {
+        val leaf = TestTask("Leaf")
+
+        assertTrue((leaf then { TestTask("Next") }).isInvisible)
+        assertTrue(leaf.thenAction { }.isInvisible)
+    }
+}