mirror of
https://github.com/GTNewHorizons/AE2FluidCraft-Rework.git
synced 2026-11-15 17:44:46 +00:00
Merge remote-tracking branch 'michaeldoylecs/creative-like-pick-block' into dev
This commit is contained in:
6 files changed
+416
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* This packet handles the entire withdrawal flow including:
|
||||
* <ul>
|
||||
* <li>Scanning the player's inventory for existing stacks of the target block</li>
|
||||
* <li>Consolidating partial stacks to maximize inventory efficiency</li>
|
||||
* <li>Extracting the appropriate amount from the ME network to fill or create a full stack</li>
|
||||
* <li>Setting the target stack as the player's active hotbar slot for immediate use</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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<CPacketPickBlockWithdraw, IMessage> {
|
||||
|
||||
@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<IAEItemStack> 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<Integer> 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<IAEItemStack> getWirelessInventory(EntityPlayerMP player) {
|
||||
ImmutablePair<Integer, ItemStack> 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<IAEItemStack> 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<IAEItemStack>) Util
|
||||
.getWirelessInv(terminalItemStack, player, StorageChannel.ITEMS);
|
||||
}
|
||||
|
||||
return inventoryHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,32 @@ public final class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static ImmutablePair<Integer, ItemStack> 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<Integer, ItemStack> getUltraWirelessTerm(EntityPlayer player) {
|
||||
int invSize = player.inventory.getSizeInventory();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user