package ar.com.companeros.world; import ar.com.companeros.WorkArea; import ar.com.companeros.action.ToolResult; import java.util.*; import net.minecraft.core.*; import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.*; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.*; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.eventbus.api.*; /** Acciones sobre el jugador canónico. Un resultado de éxito exige un efecto observado. */ public final class PlayerOperations { private PlayerOperations() { } public interface Operation { ToolResult tick(long gameTick); void cancel(); } // Resultados y vida de una operación, sin inventarios alternativos. private abstract static class Base implements Operation { final ServerPlayer player; final UUID id; final Map before; final long start = System.currentTimeMillis(); ToolResult result; Base(ServerPlayer player, UUID id) { this.player = player; this.id = id; before = PlayerCrafting.counts(player); } final ToolResult finish(ToolResult.Status status, ToolResult.Reason reason, String... effects) { if (result == null) { result = new ToolResult(id, status, reason, PlayerCrafting.delta(before, PlayerCrafting.counts(player)), List.of(effects), List.of(), GlobalPos.of(player.level().dimension(), player.blockPosition()), start, System.currentTimeMillis()); cleanup(); } return result; } final boolean unavailable() { return !player.isAlive() || player.isRemoved() || player.hasDisconnected(); } final ToolResult running() { return new ToolResult(id, ToolResult.Status.RUNNING, ToolResult.Reason.NONE, Map.of(), List.of(), List.of(), GlobalPos.of(player.level().dimension(), player.blockPosition()), start, System.currentTimeMillis()); } void cleanup() { } @Override public void cancel() { finish(ToolResult.Status.CANCELLED, ToolResult.Reason.CANCELLED); } } public static Operation mine(ServerPlayer player, UUID id, BlockPos target) { if (!loaded(player, target)) return rejected(player, id, ToolResult.Reason.CONDITIONS_CHANGED); return new Base(player, id) { final BlockPos pos = target.immutable(); final BlockState original = player.level().getBlockState(pos); final BreakReceipt receipt = new BreakReceipt(player.getUUID(), pos); float progress; boolean started; long firstTick; @Override public ToolResult tick(long tick) { if (result != null) return result; if (unavailable()) return finish(ToolResult.Status.CANCELLED, ToolResult.Reason.CONDITIONS_CHANGED); if (!editable(player, pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); if (!reach(player, pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.OUT_OF_REACH); if (!started) { if (original.isAir()) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.RESOURCE_EXHAUSTED); if (!player.level().getBlockState(pos).equals(original)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.CONDITIONS_CHANGED); if (!visible(player, pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_LINE_OF_SIGHT); selectBestTool(player, original); if (original.requiresCorrectToolForDrops() && !player.getMainHandItem().isCorrectToolForDrops(original)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.WRONG_TOOL); started = true; firstTick = tick; MinecraftForge.EVENT_BUS.register(receipt); player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, Direction.UP, player.level().getMaxBuildHeight(), 0); } if (receipt.denied()) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); if (!player.level().getBlockState(pos).equals(original)) return receipt.authorized() ? finish(ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, "mined:" + pos.toShortString()) : finish(ToolResult.Status.BLOCKED, ToolResult.Reason.CONDITIONS_CHANGED); if (!visible(player, pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_LINE_OF_SIGHT); if (tick - firstTick > 1200) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.TIMEOUT); progress += original.getDestroyProgress(player, player.level(), pos); player.swing(InteractionHand.MAIN_HAND); if (progress >= 1) { player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, Direction.UP, player.level().getMaxBuildHeight(), 0); if (!player.level().getBlockState(pos).equals(original) && receipt.authorized()) return finish(ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, "mined:" + pos.toShortString()); if (receipt.denied()) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); } return running(); } @Override void cleanup() { if (started) { MinecraftForge.EVENT_BUS.unregister(receipt); if (result.status() != ToolResult.Status.SUCCEEDED && !player.isRemoved()) player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK, Direction.UP, player.level().getMaxBuildHeight(), 0); } } }; } public static Operation place(ServerPlayer player, UUID id, BlockPos target, Item item) { return new Base(player, id) { @Override public ToolResult tick(long tick) { if (result != null) return result; if (unavailable()) return finish(ToolResult.Status.CANCELLED, ToolResult.Reason.CONDITIONS_CHANGED); if (!(item instanceof BlockItem block)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.UNSUPPORTED); if (!editable(player, target)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); if (!reach(player, target)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.OUT_OF_REACH); if (!player.level().getBlockState(target).isAir()) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.CONDITIONS_CHANGED); int slot = find(player, stack -> stack.is(item)); if (slot < 0) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.RESOURCE_EXHAUSTED); select(player, slot); for (Direction face : Direction.values()) { BlockPos support = target.relative(face.getOpposite()); if (!player.serverLevel().hasChunkAt(support) || player.level().getBlockState(support).isAir() || !visible(player, support)) continue; Vec3 point = Vec3.atCenterOf(support).add(Vec3.atLowerCornerOf(face.getNormal()).scale(.5)); var hit = new BlockHitResult(point, face, support, false); InteractionResult used = player.gameMode.useItemOn(player, player.level(), player.getMainHandItem(), InteractionHand.MAIN_HAND, hit); if (used.consumesAction() && player.level().getBlockState(target).is(block.getBlock())) { player.swing(InteractionHand.MAIN_HAND); return finish(ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, "placed:" + target.toShortString()); } if (!player.level().getBlockState(target).isAir()) break; } return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); } }; } public static Operation eat(ServerPlayer player, UUID id) { return new Base(player, id) { boolean started; long firstTick; @Override public ToolResult tick(long tick) { if (result != null) return result; if (unavailable()) return finish(ToolResult.Status.CANCELLED, ToolResult.Reason.CONDITIONS_CHANGED); if (!started) { if (!player.canEat(false)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE); int slot = find(player, stack -> stack.isEdible() && !stack.is(Items.ROTTEN_FLESH) && !stack.is(Items.POISONOUS_POTATO) && !stack.is(Items.PUFFERFISH) && !stack.is(Items.SPIDER_EYE)); if (slot < 0) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.RESOURCE_EXHAUSTED); select(player, slot); firstTick = tick; started = true; player.gameMode.useItem(player, player.level(), player.getMainHandItem(), InteractionHand.MAIN_HAND); } if (!player.isUsingItem() && tick > firstTick) { if (PlayerCrafting.delta(before, PlayerCrafting.counts(player)).values().stream().anyMatch(value -> value < 0)) return finish(ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, "ate"); return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE); } if (tick - firstTick > 200) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.TIMEOUT); return running(); } @Override void cleanup() { if (player.isUsingItem()) player.stopUsingItem(); } }; } public static Operation attack(ServerPlayer player, UUID id, UUID targetId) { return new Base(player, id) { @Override public ToolResult tick(long tick) { if (result != null) return result; Entity target = player.serverLevel().getEntity(targetId); if (unavailable() || target == null || !target.isAlive()) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.CONDITIONS_CHANGED); if (player.distanceToSqr(target) > 9) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.OUT_OF_REACH); if (!player.hasLineOfSight(target)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_LINE_OF_SIGHT); if (target instanceof Player other && (!player.server.isPvpAllowed() || !player.canHarmPlayer(other))) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); if (player.getAttackStrengthScale(0) < .95) return running(); float health = target instanceof LivingEntity living ? living.getHealth() + living.getAbsorptionAmount() : 0; player.attack(target); player.swing(InteractionHand.MAIN_HAND); boolean affected = target instanceof LivingEntity living && (living.getHealth() + living.getAbsorptionAmount() < health || !living.isAlive()); return affected ? finish(ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, "attack:" + targetId) : finish(ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE); } }; } public static Operation store(ServerPlayer player, UUID id, BlockPos pos, Item item, int wanted) { return new Base(player, id) { boolean opened; @Override public ToolResult tick(long tick) { if (result != null) return result; if (unavailable()) return finish(ToolResult.Status.CANCELLED, ToolResult.Reason.CONDITIONS_CHANGED); if (!loaded(player, pos) || !player.serverLevel().mayInteract(player, pos) || WorkArea.get(player.serverLevel()).contains(pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); if (!reach(player, pos) || !visible(player, pos)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.OUT_OF_REACH); var entity = player.level().getBlockEntity(pos); if (!(entity instanceof net.minecraft.world.level.block.entity.BaseContainerBlockEntity)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.UNSUPPORTED); var previousMenu = player.containerMenu; var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false); // Una operación de inventario no puede usar ni colocar el objeto de la mano. player.gameMode.useItemOn(player, player.level(), ItemStack.EMPTY, InteractionHand.MAIN_HAND, hit); opened = player.containerMenu != previousMenu; if (player.containerMenu == previousMenu || !player.containerMenu.stillValid(player)) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED); var handler = entity.getCapability(ForgeCapabilities.ITEM_HANDLER).orElse(null); if (handler == null) return finish(ToolResult.Status.BLOCKED, ToolResult.Reason.UNSUPPORTED); int left = Math.max(1, Math.min(64, wanted)); int deposited = 0; for (int slot = 0; slot < 36 && left > 0; slot++) { ItemStack source = player.getInventory().getItem(slot); if (!source.is(item)) continue; for (int destination = 0; destination < handler.getSlots() && left > 0 && !source.isEmpty(); destination++) { ItemStack offered = source.copy(); offered.setCount(Math.min(left, source.getCount())); int offeredCount = offered.getCount(); ItemStack remainder = handler.insertItem(destination, offered, false); int accepted = offeredCount - remainder.getCount(); if (accepted < 0 || accepted > offeredCount) return finish(ToolResult.Status.FAILED, ToolResult.Reason.INTERNAL_ERROR); source.shrink(accepted); left -= accepted; deposited += accepted; } } player.getInventory().setChanged(); return finish(deposited > 0 ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, deposited > 0 ? ToolResult.Reason.NONE : ToolResult.Reason.INVENTORY_FULL, "stored:" + deposited + ":" + pos.toShortString()); } @Override void cleanup() { if (opened) { player.closeContainer(); opened = false; } } }; } // Permisos conservadores: las máquinas se usan por capacidades específicas, no por minería. public static boolean loaded(ServerPlayer player, BlockPos pos) { return player.serverLevel().hasChunkAt(pos) && player.level().getWorldBorder().isWithinBounds(pos); } public static boolean editable(ServerPlayer player, BlockPos pos) { return loaded(player, pos) && player.serverLevel().mayInteract(player, pos) && !WorkArea.get(player.serverLevel()).contains(pos) && player.level().getBlockEntity(pos) == null; } public static boolean reach(ServerPlayer player, BlockPos pos) { return player.getEyePosition().distanceToSqr(Vec3.atCenterOf(pos)) <= 20.25; } public static boolean visible(ServerPlayer player, BlockPos pos) { var hit = player.level().clip(new net.minecraft.world.level.ClipContext(player.getEyePosition(), Vec3.atCenterOf(pos), net.minecraft.world.level.ClipContext.Block.COLLIDER, net.minecraft.world.level.ClipContext.Fluid.NONE, player)); return hit.getType() == HitResult.Type.MISS || hit.getBlockPos().equals(pos); } public static int find(ServerPlayer player, java.util.function.Predicate predicate) { for (int slot = 0; slot < 36; slot++) if (!player.getInventory().getItem(slot).isEmpty() && predicate.test(player.getInventory().getItem(slot))) return slot; return -1; } public static void select(ServerPlayer player, int slot) { var inventory = player.getInventory(); if (slot < 0 || slot >= 36) throw new IllegalArgumentException("Slot de inventario inválido"); if (slot < 9) inventory.selected = slot; else { int hand = inventory.selected; ItemStack previous = inventory.getItem(hand); inventory.setItem(hand, inventory.getItem(slot)); inventory.setItem(slot, previous); } inventory.setChanged(); } private static void selectBestTool(ServerPlayer player, BlockState state) { int best = player.getInventory().selected; float score = player.getInventory().getItem(best).getDestroySpeed(state); for (int slot = 0; slot < 36; slot++) { ItemStack stack = player.getInventory().getItem(slot); if (state.requiresCorrectToolForDrops() && !stack.isCorrectToolForDrops(state)) continue; if (stack.getDestroySpeed(state) > score || state.requiresCorrectToolForDrops() && !player.getInventory().getItem(best).isCorrectToolForDrops(state)) { best = slot; score = stack.getDestroySpeed(state); } } select(player, best); } public static final class BreakReceipt { final UUID actor; final BlockPos pos; BlockEvent.BreakEvent observed; BreakReceipt(UUID actor, BlockPos pos) { this.actor = actor; this.pos = pos; } @SubscribeEvent(priority = EventPriority.LOWEST, receiveCanceled = true) public void breaking(BlockEvent.BreakEvent event) { if (event.getPlayer().getUUID().equals(actor) && event.getPos().equals(pos)) { observed = event; } } boolean denied() { return observed != null && observed.isCanceled(); } boolean authorized() { return observed != null && !observed.isCanceled(); } } private static Operation rejected(ServerPlayer player, UUID id, ToolResult.Reason reason) { return new Base(player, id) { @Override public ToolResult tick(long tick) { return finish(ToolResult.Status.BLOCKED, reason); } }; } }