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: + *

+ * + *

+ * 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