diff --git a/src/main/java/com/glodblock/github/loader/ChannelLoader.java b/src/main/java/com/glodblock/github/loader/ChannelLoader.java
index 62c4652c..3ca2d647 100644
--- a/src/main/java/com/glodblock/github/loader/ChannelLoader.java
+++ b/src/main/java/com/glodblock/github/loader/ChannelLoader.java
@@ -6,6 +6,10 @@ import com.glodblock.github.network.CPacketDumpTank;
import com.glodblock.github.network.CPacketFluidPatternTermBtns;
import com.glodblock.github.network.CPacketLevelMaintainer;
import com.glodblock.github.network.CPacketLevelTerminalCommands;
+import com.glodblock.github.network.CPacketPatternMultiSet;
+import com.glodblock.github.network.CPacketPatternValueSet;
+import com.glodblock.github.network.CPacketPickBlockWithdraw;
+import com.glodblock.github.network.CPacketRenamer;
import com.glodblock.github.network.CPacketSwitchGuis;
import com.glodblock.github.network.CPacketValueConfig;
import com.glodblock.github.network.SPacketCustomButtonUpdate;
@@ -57,6 +61,11 @@ public class ChannelLoader implements Runnable {
SPacketLevelMaintainerGuiUpdate.class,
id++,
Side.CLIENT);
+ netHandler.registerMessage(
+ new CPacketPickBlockWithdraw.Handler(),
+ CPacketPickBlockWithdraw.class,
+ id++,
+ Side.SERVER);
netHandler.registerMessage(
new SPacketCustomButtonUpdate.Handler(),
SPacketCustomButtonUpdate.class,
diff --git a/src/main/java/com/glodblock/github/loader/KeybindLoader.java b/src/main/java/com/glodblock/github/loader/KeybindLoader.java
index ae30e7f4..bb25bd1f 100644
--- a/src/main/java/com/glodblock/github/loader/KeybindLoader.java
+++ b/src/main/java/com/glodblock/github/loader/KeybindLoader.java
@@ -4,11 +4,14 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityClientPlayerMP;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.item.ItemStack;
+import net.minecraft.util.MovingObjectPosition;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.lwjgl.input.Keyboard;
import com.glodblock.github.FluidCraft;
+import com.glodblock.github.common.item.ItemWirelessUltraTerminal;
+import com.glodblock.github.network.CPacketPickBlockWithdraw;
import com.glodblock.github.network.CPacketSwitchGuis;
import com.glodblock.github.network.CPacketValueConfig;
import com.glodblock.github.util.Util;
@@ -25,13 +28,16 @@ public class KeybindLoader implements Runnable {
public static KeyBinding openTerminal;
public static KeyBinding restock;
+ public static KeyBinding pickBlock;
@Override
public void run() {
openTerminal = new KeyBinding(FluidCraft.MODID + ".key.OpenTerminal", Keyboard.CHAR_NONE, "itemGroup.ae2fc");
restock = new KeyBinding(FluidCraft.MODID + ".key.Restock", Keyboard.CHAR_NONE, "itemGroup.ae2fc");
+ pickBlock = new KeyBinding(FluidCraft.MODID + ".key.PickBlock", Keyboard.CHAR_NONE, "itemGroup.ae2fc");
ClientRegistry.registerKeyBinding(openTerminal);
ClientRegistry.registerKeyBinding(restock);
+ ClientRegistry.registerKeyBinding(pickBlock);
FMLCommonHandler.instance().bus().register(this);
}
@@ -42,6 +48,8 @@ public class KeybindLoader implements Runnable {
handleOpenTerminalKey();
} else if (restock.isPressed()) {
handleRestockKey();
+ } else if (pickBlock.isPressed()) {
+ handlePickBlockKey();
}
}
@@ -52,6 +60,8 @@ public class KeybindLoader implements Runnable {
handleOpenTerminalKey();
} else if (restock.isPressed()) {
handleRestockKey();
+ } else if (pickBlock.isPressed()) {
+ handlePickBlockKey();
}
}
@@ -75,4 +85,22 @@ public class KeybindLoader implements Runnable {
FluidCraft.proxy.netHandler.sendToServer(new CPacketValueConfig(0, 1));
}
}
+
+ private void handlePickBlockKey() {
+ Minecraft minecraft = Minecraft.getMinecraft();
+ if (minecraft.currentScreen != null) return; // Don't act if a GUI is open
+
+ EntityClientPlayerMP player = minecraft.thePlayer;
+ if (player == null) return;
+
+ // Get the block the player is currently looking at
+ MovingObjectPosition movingObject = minecraft.objectMouseOver;
+ if (movingObject == null || movingObject.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK) {
+ return;
+ }
+
+ // Send packet to server with block coordinates
+ FluidCraft.proxy.netHandler.sendToServer(
+ new CPacketPickBlockWithdraw(movingObject.blockX, movingObject.blockY, movingObject.blockZ));
+ }
}
diff --git a/src/main/java/com/glodblock/github/network/CPacketPickBlockWithdraw.java b/src/main/java/com/glodblock/github/network/CPacketPickBlockWithdraw.java
new file mode 100644
index 00000000..fbff6d12
--- /dev/null
+++ b/src/main/java/com/glodblock/github/network/CPacketPickBlockWithdraw.java
@@ -0,0 +1,247 @@
+package com.glodblock.github.network;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.minecraft.block.Block;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.world.World;
+import net.p455w0rd.wirelesscraftingterminal.common.container.ContainerWirelessCraftingTerminal;
+import net.p455w0rd.wirelesscraftingterminal.common.utils.RandomUtils;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+
+import com.glodblock.github.util.PlayerInventoryUtil;
+import com.glodblock.github.util.Util;
+
+import appeng.api.AEApi;
+import appeng.api.config.Actionable;
+import appeng.api.networking.security.PlayerSource;
+import appeng.api.storage.IMEInventoryHandler;
+import appeng.api.storage.StorageChannel;
+import appeng.api.storage.data.IAEItemStack;
+import appeng.api.storage.data.IAEStack;
+import cpw.mods.fml.common.network.simpleimpl.IMessage;
+import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
+import cpw.mods.fml.common.network.simpleimpl.MessageContext;
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Network packet sent from client to server when a player looks at a block and requests to withdraw that block type
+ * from their wireless terminal's ME network.
+ *
+ *
+ * This packet handles the entire withdrawal flow including:
+ *
+ * - Scanning the player's inventory for existing stacks of the target block
+ * - Consolidating partial stacks to maximize inventory efficiency
+ * - Extracting the appropriate amount from the ME network to fill or create a full stack
+ * - Setting the target stack as the player's active hotbar slot for immediate use
+ *
+ *
+ *
+ * The packet carries the coordinates of the block the player is looking at, which determines what item type to withdraw
+ * from the ME network.
+ */
+public class CPacketPickBlockWithdraw implements IMessage {
+
+ private int blockX;
+ private int blockY;
+ private int blockZ;
+
+ public CPacketPickBlockWithdraw() {
+ // Required for FML
+ }
+
+ public CPacketPickBlockWithdraw(int x, int y, int z) {
+ this.blockX = x;
+ this.blockY = y;
+ this.blockZ = z;
+ }
+
+ @Override
+ public void fromBytes(ByteBuf buf) {
+ this.blockX = buf.readInt();
+ this.blockY = buf.readInt();
+ this.blockZ = buf.readInt();
+ }
+
+ @Override
+ public void toBytes(ByteBuf buf) {
+ buf.writeInt(this.blockX);
+ buf.writeInt(this.blockY);
+ buf.writeInt(this.blockZ);
+ }
+
+ public static class Handler implements IMessageHandler {
+
+ @Override
+ public IMessage onMessage(CPacketPickBlockWithdraw message, MessageContext ctx) {
+ EntityPlayerMP player = ctx.getServerHandler().playerEntity;
+ if (player == null || player.inventory == null) {
+ return null;
+ }
+ World world = player.worldObj;
+
+ // Ensure the player has a wireless terminal
+ IMEInventoryHandler terminalInventory = getWirelessInventory(player);
+ if (terminalInventory == null) {
+ player.addChatMessage(new ChatComponentText("Could not access AE2 network."));
+ return null;
+ }
+
+ // Get the target block
+ Block targetBlock = world.getBlock(message.blockX, message.blockY, message.blockZ);
+ if (targetBlock == Blocks.air) {
+ return null; // Don't try to withdraw air
+ }
+
+ Item targetItem = Item.getItemFromBlock(targetBlock);
+ if (targetItem == null) {
+ return null; // Should not happen for non-air blocks
+ }
+
+ // Check player inventory for existing stacks to determine how much to withdraw.
+ ItemStack itemToFind = new ItemStack(
+ targetItem,
+ 1,
+ world.getBlockMetadata(message.blockX, message.blockY, message.blockZ));
+
+ // 1. Scan through the player's main inventory to categorize existing stacks of the target block:
+ // - If a full stack (stackSize >= maxStackSize) is found, record its slot and stop searching.
+ // This indicates the player already has the maximum possible stack, so no withdrawal is needed.
+ // - Otherwise, collect all partial stack slots (stacks that match the item but aren't full).
+ // Partial stacks will be consolidated in a later step.
+ int fullStackSlot = -1;
+ List partialStackSlotsList = new ArrayList<>();
+ for (int i = 0; i < player.inventory.mainInventory.length; i++) {
+ ItemStack stackInSlot = player.inventory.mainInventory[i];
+ if (stackInSlot != null && stackInSlot.isItemEqual(itemToFind)
+ && ItemStack.areItemStackTagsEqual(stackInSlot, itemToFind)) {
+ if (stackInSlot.stackSize >= stackInSlot.getMaxStackSize()) {
+ fullStackSlot = i;
+ break; // Found a full stack, no need to do anything.
+ }
+ partialStackSlotsList.add(i);
+ }
+ }
+
+ // 2. If a full stack already exists, put in active slot and return.
+ if (fullStackSlot >= 0) {
+ PlayerInventoryUtil.setSlotAsActiveSlot(player, fullStackSlot);
+ return null;
+ }
+
+ // 3. If there are no partial stacks and the player's inventory is full,
+ // then return since we cannot add a retrieved stack to a full inventory
+ int nextEmptySlot = PlayerInventoryUtil.getFirstEmptyStackReverse(player.inventory);
+ if (partialStackSlotsList.isEmpty() && nextEmptySlot == -1) {
+ return null;
+ }
+
+ // 4. Consolidate all partial stacks of target block into 1 ItemStack.
+ // If a full stack is obtained, set it as the active slot and return.
+ ItemStack consolidatedStack = null;
+ int consolidatedStackSlot = -1;
+ for (Integer partialStackSlot : partialStackSlotsList) {
+ if (consolidatedStack == null) {
+ consolidatedStack = player.inventory.getStackInSlot(partialStackSlot);
+ consolidatedStackSlot = partialStackSlot;
+ } else {
+ PlayerInventoryUtil
+ .consolidateItemStacks(player.inventory, partialStackSlot, consolidatedStackSlot);
+ }
+
+ // Check if we created a full stack of items
+ if (consolidatedStack.stackSize == consolidatedStack.getMaxStackSize()) {
+ PlayerInventoryUtil.setSlotAsActiveSlot(player, consolidatedStackSlot);
+ return null;
+ }
+ }
+
+ // 5. Calculate withdrawal amount
+ int amountToWithdraw = consolidatedStack == null ? itemToFind.getMaxStackSize()
+ : itemToFind.getMaxStackSize() - consolidatedStack.stackSize;
+ if (amountToWithdraw <= 0) {
+ return null;
+ }
+
+ // Create an IAEItemStack for the target block with the calculated amount
+ ItemStack targetItemStack = itemToFind.copy();
+ targetItemStack.stackSize = amountToWithdraw;
+ IAEItemStack targetAeItemStack = AEApi.instance().storage().createItemStack(targetItemStack);
+ if (targetAeItemStack == null) {
+ return null;
+ }
+
+ // 6. Extract items from the network
+ PlayerSource source = new PlayerSource(player, null);
+ IAEStack> extractedStack = terminalInventory.extractItems(targetAeItemStack, Actionable.MODULATE, source);
+ if (extractedStack instanceof IAEItemStack extractedAeItemStack && extractedStack.getStackSize() > 0) {
+ ItemStack itemsToGive = extractedAeItemStack.getItemStack();
+ // Update the player's inventory with the withdrawn items
+ if (itemsToGive != null && itemsToGive.stackSize > 0) {
+ if (consolidatedStack == null) {
+ player.inventory.setInventorySlotContents(nextEmptySlot, itemsToGive);
+ } else {
+ consolidatedStack.stackSize += itemsToGive.stackSize;
+ }
+ player.inventory.markDirty();
+ }
+ }
+
+ // If the target stack is already in the player's hotbar, set that as the active slot.
+ // Otherwise, move the target stack to the active slot.
+ // The slot to swap will have either been a consolidated stack of partial ItemStacks,
+ // or it will have been a newly created ItemStack in the next empty slot.
+ int slotToSwap = consolidatedStack == null ? nextEmptySlot : consolidatedStackSlot;
+ PlayerInventoryUtil.setSlotAsActiveSlot(player, slotToSwap);
+
+ return null; // No reply packet needed
+ }
+
+ @SuppressWarnings("unchecked")
+ private IMEInventoryHandler getWirelessInventory(EntityPlayerMP player) {
+ ImmutablePair terminalAndInventorySlot = Util.getWirelessTerminal(player);
+ ItemStack wctTerminal = null;
+
+ if (terminalAndInventorySlot == null) {
+ // Secondary check, does the player have a WirelessCraftingTerminal terminal
+ // Why is this mod still in GTNH ;-;
+ wctTerminal = RandomUtils.getWirelessTerm(player.inventory);
+ if (wctTerminal == null) {
+ return null;
+ }
+ }
+
+ IMEInventoryHandler inventoryHandler;
+ if (wctTerminal != null) {
+ inventoryHandler = ContainerWirelessCraftingTerminal.getGuiObject(
+ wctTerminal,
+ player,
+ player.worldObj,
+ (int) player.posX,
+ (int) player.posY,
+ (int) player.posZ).getItemInventory();
+ } else {
+ ItemStack terminalItemStack = terminalAndInventorySlot.getRight();
+ if (terminalItemStack == null) {
+ return null;
+ }
+
+ // Obtain the inventory handler for the AE2 wireless terminal.
+ //
+ // We can safely suppress this warning, as we are the one providing the storage type in
+ // the function call.
+ inventoryHandler = (IMEInventoryHandler) Util
+ .getWirelessInv(terminalItemStack, player, StorageChannel.ITEMS);
+ }
+
+ return inventoryHandler;
+ }
+ }
+}
diff --git a/src/main/java/com/glodblock/github/util/PlayerInventoryUtil.java b/src/main/java/com/glodblock/github/util/PlayerInventoryUtil.java
new file mode 100644
index 00000000..e1910d7f
--- /dev/null
+++ b/src/main/java/com/glodblock/github/util/PlayerInventoryUtil.java
@@ -0,0 +1,105 @@
+package com.glodblock.github.util;
+
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.network.play.server.S09PacketHeldItemChange;
+
+public class PlayerInventoryUtil {
+
+ /**
+ * Finds the first empty slot in the player's inventory, searching in reverse order (from the last slot to the
+ * first). This prioritizes filling the inventory from the end, which helps keep commonly accessed hotbar slots
+ * free.
+ *
+ * @param inventory The player's inventory to search.
+ * @return The index of the first empty slot found (searching backwards), or -1 if the inventory is full.
+ */
+ public static int getFirstEmptyStackReverse(InventoryPlayer inventory) {
+ for (int i = inventory.mainInventory.length - 1; i >= 0; --i) {
+ if (inventory.mainInventory[i] == null) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Sets the specified inventory slot as the player's active (held) slot. If the target slot is in the hotbar (slots
+ * 0-8), it directly selects that hotbar slot. If the target slot is outside the hotbar, it swaps the contents of
+ * that slot with the current active slot.
+ *
+ * @param player The player whose active slot should be changed.
+ * @param slot The inventory slot index to make active (0-8 for hotbar, 9+ for main inventory).
+ */
+ public static void setSlotAsActiveSlot(EntityPlayerMP player, int slot) {
+ if (slot >= 0 && slot <= 8) {
+ player.inventory.currentItem = slot;
+ player.playerNetServerHandler.sendPacket(new S09PacketHeldItemChange(slot));
+ } else {
+ swapInventorySlots(player.inventory, player.inventory.currentItem, slot);
+ }
+ player.inventory.markDirty();
+ }
+
+ /**
+ * Moves an ItemStack from a source slot to a destination slot in the player's inventory. If the destination slot is
+ * occupied, the items are swapped.
+ *
+ * @param inventory The player's inventory.
+ * @param slot1 The index of the first ItemStack to move.
+ * @param slot2 The index of the second ItemStack to move.
+ */
+ public static void swapInventorySlots(InventoryPlayer inventory, int slot1, int slot2) {
+ if (slot1 == slot2) {
+ return;
+ }
+
+ // Get the stacks from both slots
+ ItemStack sourceStack = inventory.getStackInSlot(slot1);
+ ItemStack destinationStack = inventory.getStackInSlot(slot2);
+
+ // Set the destination slot with the source stack (even if it's null)
+ inventory.setInventorySlotContents(slot2, sourceStack);
+
+ // Set the source slot with the original destination stack
+ inventory.setInventorySlotContents(slot1, destinationStack);
+
+ // Mark the inventory as dirty to ensure changes are saved and synced
+ inventory.markDirty();
+ }
+
+ /**
+ * Consolidate ItemStacks from a source slot to a destination slot in the player's inventory.
+ *
+ * @param inventory The player's inventory.
+ * @param sourceSlot The index of the slot to move the item from.
+ * @param destinationSlot The index of the slot to move the item to.
+ */
+ public static void consolidateItemStacks(InventoryPlayer inventory, int sourceSlot, int destinationSlot) {
+ ItemStack sourceStack = inventory.getStackInSlot(sourceSlot);
+ ItemStack destinationStack = inventory.getStackInSlot(destinationSlot);
+
+ if (!sourceStack.isItemEqual(destinationStack)
+ || !ItemStack.areItemStackTagsEqual(sourceStack, destinationStack)) {
+ return;
+ }
+
+ int missingQuantity = destinationStack.getMaxStackSize() - destinationStack.stackSize;
+ if (missingQuantity >= sourceStack.stackSize) {
+ destinationStack.stackSize = destinationStack.stackSize + sourceStack.stackSize;
+ sourceStack = null;
+ } else {
+ sourceStack.stackSize -= missingQuantity;
+ destinationStack.stackSize += missingQuantity;
+ }
+
+ // Update the inventory stacks
+ inventory.setInventorySlotContents(destinationSlot, destinationStack);
+ inventory.setInventorySlotContents(sourceSlot, sourceStack);
+
+ // Mark the inventory as dirty to ensure changes are saved and synced
+ inventory.markDirty();
+ }
+}
diff --git a/src/main/java/com/glodblock/github/util/Util.java b/src/main/java/com/glodblock/github/util/Util.java
index 118a94a7..3deb1fa0 100644
--- a/src/main/java/com/glodblock/github/util/Util.java
+++ b/src/main/java/com/glodblock/github/util/Util.java
@@ -202,6 +202,32 @@ public final class Util {
}
}
+ public static ImmutablePair getWirelessTerminal(EntityPlayer player) {
+ // Check player inventory for the wireless terminal.
+ int inventorySize = player.inventory.getSizeInventory();
+ for (int i = 0; i < inventorySize; ++i) {
+ ItemStack is = player.inventory.getStackInSlot(i);
+ if (is != null && is.getItem() instanceof ToolWirelessTerminal) {
+ return new ImmutablePair<>(GuiHelper.encodeType(i, GuiHelper.InvType.PLAYER_BAUBLES), is);
+ }
+ }
+
+ if (isBaublesLoaded) {
+ IInventory handler = BaublesApi.getBaubles(player);
+ if (handler != null) {
+ int baubleSlotCount = handler.getSizeInventory();
+ for (int i = 0; i < baubleSlotCount; ++i) {
+ ItemStack is = handler.getStackInSlot(i);
+ if (is != null && is.getItem() instanceof ToolWirelessTerminal) {
+ return new ImmutablePair<>(GuiHelper.encodeType(i, GuiHelper.InvType.PLAYER_BAUBLES), is);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
public static ImmutablePair getUltraWirelessTerm(EntityPlayer player) {
int invSize = player.inventory.getSizeInventory();
diff --git a/src/main/resources/assets/ae2fc/lang/en_US.lang b/src/main/resources/assets/ae2fc/lang/en_US.lang
index e86b999f..345339fc 100644
--- a/src/main/resources/assets/ae2fc/lang/en_US.lang
+++ b/src/main/resources/assets/ae2fc/lang/en_US.lang
@@ -222,6 +222,7 @@ error.unknown=Unknown
ae2fc.key.OpenTerminal=Open Wireless Universal Terminal
ae2fc.key.Restock=Toggle Restock
+ae2fc.key.PickBlock=AE2 Pick Block
itemGroup.ae2fc=Fluid Craft For AE2