Volver al índice

src/main/java/ar/com/companeros/world/PlayerOperations.java

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<String, Integer> 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<ItemStack> 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); }
        };
    }
}