Volver al índice

src/main/java/ar/com/companeros/flags/BannerActions.java

package ar.com.companeros.flags;

import ar.com.companeros.action.ToolResult;
import ar.com.companeros.world.PlayerCrafting;
import ar.com.companeros.world.PlayerOperations;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.TransientCraftingContainer;
import net.minecraft.world.item.BannerItem;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.DyeItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.BannerBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BannerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.registries.ForgeRegistries;

/** Fabricación transaccional y colocación vanilla de banderas del inventario canónico. */
public final class BannerActions {
    public static final String METADATA_TAG = "RapsFlag";
    public static final int SCHEMA_VERSION = 1;
    private static final Set<String> POSITION_FIELDS = Set.of("x", "y", "z");
    private static final Set<String> METADATA_FIELDS = Set.of("Schema", "Author", "Action", "Design");
    private BannerActions() { }

    // Primero se valida y simula todo; el inventario sólo cambia en el commit final.
    public static ToolResult create(ServerPlayer player, UUID actionId, JsonObject input) {
        if (!available(player)) return blocked(player, actionId, ToolResult.Reason.CONDITIONS_CHANGED);
        BannerDesign design;
        try { design = BannerDesign.parse(input); }
        catch (IllegalArgumentException failure) { return blocked(player, actionId, ToolResult.Reason.INVALID_ARGUMENT); }
        BlockPos table = station(player, Blocks.CRAFTING_TABLE);
        BlockPos loom = station(player, Blocks.LOOM);
        if (table == null || loom == null) return blocked(player, actionId, ToolResult.Reason.WRONG_TOOL);
        if (!canUseStation(player, table) || !canUseStation(player, loom))
            return blocked(player, actionId, ToolResult.Reason.PERMISSION_DENIED);

        SimpleContainer scratch = new SimpleContainer(36);
        for (int slot = 0; slot < 36; slot++) scratch.setItem(slot, player.getInventory().getItem(slot).copy());
        Item wool = ForgeRegistries.ITEMS.getValue(new ResourceLocation("minecraft", design.baseColor().getName() + "_wool"));
        if (wool == null || wool == Items.AIR) return blocked(player, actionId, ToolResult.Reason.RECIPE_UNSUPPORTED);
        var matrix = new TransientCraftingContainer(new AbstractContainerMenu(null, -1) {
            @Override public ItemStack quickMoveStack(Player p, int slot) { return ItemStack.EMPTY; }
            @Override public boolean stillValid(Player p) { return false; }
        }, 3, 3);
        for (int slot = 0; slot < 6; slot++) {
            ItemStack material = consume(scratch, wool);
            if (material.isEmpty()) return blocked(player, actionId, ToolResult.Reason.RESOURCE_EXHAUSTED);
            matrix.setItem(slot, material);
        }
        ItemStack stick = consume(scratch, Items.STICK);
        if (stick.isEmpty() || consume(scratch, DyeItem.byColor(design.circleColor())).isEmpty()
                || design.symbol() != BannerDesign.Symbol.NONE && consume(scratch, DyeItem.byColor(design.symbolColor())).isEmpty())
            return blocked(player, actionId, ToolResult.Reason.RESOURCE_EXHAUSTED);
        matrix.setItem(7, stick);
        var recipe = player.serverLevel().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, matrix, player.level()).orElse(null);
        if (recipe == null) return blocked(player, actionId, ToolResult.Reason.RECIPE_UNSUPPORTED);
        ItemStack result = recipe.assemble(matrix, player.level().registryAccess());
        if (!result.is(BannerBlock.byColor(design.baseColor()).asItem()) || result.getCount() != 1)
            return blocked(player, actionId, ToolResult.Reason.RECIPE_UNSUPPORTED);
        CompoundTag blockData = new CompoundTag();
        blockData.put("Patterns", design.patterns());
        BlockItem.setBlockEntityData(result, BlockEntityType.BANNER, blockData);
        CompoundTag metadata = new CompoundTag();
        metadata.putInt("Schema", SCHEMA_VERSION);
        metadata.putUUID("Author", player.getUUID());
        metadata.putUUID("Action", actionId);
        metadata.put("Design", design.toTag());
        result.getOrCreateTag().put(METADATA_TAG, metadata);

        Player previous = ForgeHooks.getCraftingPlayer();
        net.minecraft.core.NonNullList<ItemStack> remaining;
        try { ForgeHooks.setCraftingPlayer(player); remaining = recipe.getRemainingItems(matrix); }
        finally { ForgeHooks.setCraftingPlayer(previous); }
        if (!scratch.addItem(result.copy()).isEmpty()) return blocked(player, actionId, ToolResult.Reason.INVENTORY_FULL);
        for (ItemStack stack : remaining) if (!stack.isEmpty() && !scratch.addItem(stack.copy()).isEmpty())
            return blocked(player, actionId, ToolResult.Reason.INVENTORY_FULL);

        Map<String, Integer> before = PlayerCrafting.counts(player);
        for (int slot = 0; slot < 36; slot++) player.getInventory().setItem(slot, scratch.getItem(slot).copy());
        player.getInventory().setChanged();
        result.onCraftedBy(player.level(), player, 1);
        ForgeEventFactory.firePlayerCraftingEvent(player, result.copy(), matrix);
        player.awardRecipes(List.of(recipe));
        player.containerMenu.broadcastChanges();
        return finished(player, actionId, before, "crafted_banner:" + recipe.getId());
    }

    // La misma pila con patrones llega a GameMode/useOn y a los eventos Forge de colocación.
    public static ToolResult place(ServerPlayer player, UUID actionId, JsonObject input) {
        if (!available(player)) return blocked(player, actionId, ToolResult.Reason.CONDITIONS_CHANGED);
        BlockPos target;
        try {
            if (input == null || !input.keySet().equals(POSITION_FIELDS)) throw new IllegalArgumentException();
            target = new BlockPos(integer(input, "x"), integer(input, "y"), integer(input, "z"));
        } catch (IllegalArgumentException failure) { return blocked(player, actionId, ToolResult.Reason.INVALID_ARGUMENT); }
        if (!PlayerOperations.editable(player, target)) return blocked(player, actionId, ToolResult.Reason.PERMISSION_DENIED);
        if (!withinReach(player, target)) return blocked(player, actionId, ToolResult.Reason.OUT_OF_REACH);
        if (!PlayerOperations.visible(player, target)) return blocked(player, actionId, ToolResult.Reason.NO_LINE_OF_SIGHT);
        if (!player.level().getBlockState(target).isAir()) return blocked(player, actionId, ToolResult.Reason.CONDITIONS_CHANGED);
        int slot = PlayerOperations.find(player, stack -> designOf(stack).isPresent());
        if (slot < 0) return blocked(player, actionId, ToolResult.Reason.RESOURCE_EXHAUSTED);
        ItemStack offered = player.getInventory().getItem(slot).copy();
        BannerDesign design = designOf(offered).orElseThrow();
        Map<String, Integer> before = PlayerCrafting.counts(player);
        PlayerOperations.select(player, slot);
        for (Direction face : Direction.values()) {
            BlockPos support = target.relative(face.getOpposite());
            if (!PlayerOperations.loaded(player, support) || !player.serverLevel().mayInteract(player, support)
                    || player.level().getBlockState(support).isAir() || !withinReach(player, support)
                    || !PlayerOperations.visible(player, support)) continue;
            Vec3 point = Vec3.atCenterOf(support).add(Vec3.atLowerCornerOf(face.getNormal()).scale(.5));
            var hit = new BlockHitResult(point, face, support, false);
            var used = player.gameMode.useItemOn(player, player.level(), player.getMainHandItem(), InteractionHand.MAIN_HAND, hit);
            if (used.consumesAction() && player.level().getBlockEntity(target) instanceof BannerBlockEntity entity
                    && entity.getBaseColor() == design.baseColor() && design.patterns().equals(BannerBlockEntity.getItemPatterns(entity.getItem()))
                    && PlayerCrafting.delta(before, PlayerCrafting.counts(player)).getOrDefault(itemKey(offered), 0) == -1) {
                entity.getPersistentData().put(METADATA_TAG, offered.getTag().getCompound(METADATA_TAG).copy());
                entity.setChanged();
                player.swing(InteractionHand.MAIN_HAND);
                player.containerMenu.broadcastChanges();
                return finished(player, actionId, before, "placed_banner:" + target.toShortString());
            }
            if (!player.level().getBlockState(target).isAir()) break;
        }
        return ToolResult.immediate(actionId, ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED,
            GlobalPos.of(player.level().dimension(), player.blockPosition()), PlayerCrafting.delta(before, PlayerCrafting.counts(player)), List.of());
    }

    // Lectura segura para percepción y selección: el NBT tiene que describir exactamente sus patrones.
    public static Optional<BannerDesign> designOf(ItemStack stack) {
        if (!(stack.getItem() instanceof BannerItem banner) || !stack.hasTag() || !stack.getTag().contains(METADATA_TAG, 10)) return Optional.empty();
        CompoundTag metadata = stack.getTag().getCompound(METADATA_TAG);
        Optional<CompoundTag> validated = validatedMetadata(metadata);
        if (validated.isEmpty()) return Optional.empty();
        try {
            BannerDesign design = BannerDesign.fromTag(validated.get().getCompound("Design"));
            return design.baseColor() == banner.getColor() && design.patterns().equals(BannerBlockEntity.getItemPatterns(stack))
                ? Optional.of(design) : Optional.empty();
        } catch (IllegalArgumentException failure) { return Optional.empty(); }
    }

    // Se reconstruyen sólo los campos permitidos, sin transferir NBT ajeno a la autoría.
    static Optional<CompoundTag> validatedMetadata(CompoundTag metadata) {
        if (!metadata.getAllKeys().equals(METADATA_FIELDS) || !metadata.contains("Schema", 3)
                || metadata.getInt("Schema") != SCHEMA_VERSION || !metadata.hasUUID("Author")
                || !metadata.hasUUID("Action") || !metadata.contains("Design", 10)) return Optional.empty();
        try {
            BannerDesign design = BannerDesign.fromTag(metadata.getCompound("Design"));
            CompoundTag clean = new CompoundTag();
            clean.putInt("Schema", SCHEMA_VERSION);
            clean.putUUID("Author", metadata.getUUID("Author"));
            clean.putUUID("Action", metadata.getUUID("Action"));
            clean.put("Design", design.toTag());
            return Optional.of(clean);
        } catch (IllegalArgumentException failure) { return Optional.empty(); }
    }

    private static boolean available(ServerPlayer p) {
        return p.isAlive() && !p.isRemoved() && !p.hasDisconnected() && p.gameMode.getGameModeForPlayer() == GameType.SURVIVAL
            && !p.getAbilities().instabuild;
    }
    private static ItemStack consume(SimpleContainer bag, Item item) {
        for (int slot = 0; slot < bag.getContainerSize(); slot++) if (bag.getItem(slot).is(item)) return bag.removeItem(slot, 1);
        return ItemStack.EMPTY;
    }
    private static BlockPos station(ServerPlayer p, Block block) {
        for (BlockPos pos : BlockPos.betweenClosed(p.blockPosition().offset(-3, -2, -3), p.blockPosition().offset(3, 2, 3)))
            if (PlayerOperations.loaded(p, pos) && p.level().getBlockState(pos).is(block) && withinReach(p, pos)
                    && PlayerOperations.visible(p, pos) && p.serverLevel().mayInteract(p, pos)) return pos.immutable();
        return null;
    }
    private static boolean canUseStation(ServerPlayer p, BlockPos pos) {
        var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false);
        var event = ForgeHooks.onRightClickBlock(p, InteractionHand.MAIN_HAND, pos, hit);
        return !event.isCanceled() && event.getUseBlock() != Event.Result.DENY;
    }
    private static boolean withinReach(ServerPlayer p, BlockPos pos) { return p.getEyePosition().distanceToSqr(Vec3.atCenterOf(pos)) <= 16; }
    private static int integer(JsonObject input, String key) {
        var value = input.get(key);
        if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) throw new IllegalArgumentException();
        try { return value.getAsBigDecimal().intValueExact(); }
        catch (ArithmeticException failure) { throw new IllegalArgumentException(failure); }
    }
    private static String itemKey(ItemStack stack) { return ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(); }
    private static ToolResult blocked(ServerPlayer p, UUID id, ToolResult.Reason reason) {
        return ToolResult.immediate(id, ToolResult.Status.BLOCKED, reason, GlobalPos.of(p.level().dimension(), p.blockPosition()), Map.of(), List.of());
    }
    private static ToolResult finished(ServerPlayer p, UUID id, Map<String, Integer> before, String effect) {
        return ToolResult.immediate(id, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, GlobalPos.of(p.level().dimension(), p.blockPosition()),
            PlayerCrafting.delta(before, PlayerCrafting.counts(p)), List.of(effect));
    }
}