Volver al índice

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

package ar.com.companeros.world;

import ar.com.companeros.action.ToolResult;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.level.ServerPlayer;
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.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.event.ForgeEventFactory;

/** Fabricación sobre inventario canónico, con asignación de ingredientes y eventos del jugador. */
public final class PlayerCrafting {
    private PlayerCrafting() { }

    // Entrada: solo recetas reales de mesa y matrices 2×2 o 3×3.
    public static ToolResult craft(ServerPlayer player, UUID actionId, Item wanted) {
        int gridSize = workbench(player) ? 3 : 2;
        var inventory = player.getInventory();
        var before = counts(player);
        boolean known = false;
        boolean full = false;
        for (CraftingRecipe recipe : player.serverLevel().getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) {
            if (!recipe.getResultItem(player.level().registryAccess()).is(wanted) || !recipe.canCraftInDimensions(gridSize, gridSize)) continue;
            known = true;
            if (recipe.getIngredients().isEmpty()) continue; // Recetas especiales necesitan su adaptador.
            SimpleContainer scratch = new SimpleContainer(36);
            for (int i = 0; i < 36; i++) scratch.setItem(i, inventory.getItem(i).copy());
            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; }
            }, gridSize, gridSize);
            int recipeWidth = recipe instanceof ShapedRecipe shaped ? shaped.getWidth() : gridSize;
            int[] budget = {4096};
            if (!assign(recipe, scratch, matrix, recipeWidth, gridSize, 0, budget)) continue;
            if (!recipe.matches(matrix, player.level())) continue;
            ItemStack result = recipe.assemble(matrix, player.level().registryAccess());
            if (result.isEmpty() || !result.is(wanted)) continue;
            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()) { full = true; continue; }
            boolean fits = true;
            for (ItemStack stack : remaining) if (!stack.isEmpty() && !scratch.addItem(stack.copy()).isEmpty()) { fits = false; break; }
            if (!fits) { full = true; continue; }

            // Commit único; los remanentes y resultado ya caben sin eliminar recursos.
            for (int i = 0; i < 36; i++) inventory.setItem(i, scratch.getItem(i).copy());
            inventory.setChanged();
            result.onCraftedBy(player.level(), player, result.getCount());
            ForgeEventFactory.firePlayerCraftingEvent(player, result.copy(), matrix);
            player.awardRecipes(List.of(recipe));
            player.containerMenu.broadcastChanges();
            var delta = delta(before, counts(player));
            return ToolResult.immediate(actionId, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE,
                GlobalPos.of(player.level().dimension(), player.blockPosition()), delta, List.of("crafted:" + recipe.getId()));
        }
        return ToolResult.immediate(actionId, ToolResult.Status.BLOCKED,
            full ? ToolResult.Reason.INVENTORY_FULL : known ? ToolResult.Reason.RESOURCE_EXHAUSTED : ToolResult.Reason.RECIPE_UNSUPPORTED,
            GlobalPos.of(player.level().dimension(), player.blockPosition()), Map.of(), List.of());
    }

    // Backtracking acotado evita rechazar recetas por etiquetas de ingredientes solapadas.
    private static boolean assign(CraftingRecipe recipe, SimpleContainer bag, TransientCraftingContainer matrix,
                                  int width, int gridWidth, int ingredientIndex, int[] budget) {
        if (ingredientIndex == recipe.getIngredients().size()) return true;
        if (budget[0] <= 0) return false;
        var ingredient = recipe.getIngredients().get(ingredientIndex);
        int matrixSlot = ingredientIndex / width * gridWidth + ingredientIndex % width;
        if (matrixSlot >= matrix.getContainerSize()) return false;
        if (ingredient.isEmpty()) return assign(recipe, bag, matrix, width, gridWidth, ingredientIndex + 1, budget);
        List<ItemStack> tried = new ArrayList<>();
        for (int slot = 0; slot < bag.getContainerSize(); slot++) {
            ItemStack stack = bag.getItem(slot);
            if (stack.isEmpty() || !ingredient.test(stack) || tried.stream().anyMatch(t -> ItemStack.isSameItemSameTags(t, stack))) continue;
            tried.add(stack.copy());
            if (--budget[0] < 0) return false;
            ItemStack one = bag.removeItem(slot, 1);
            matrix.setItem(matrixSlot, one);
            if (assign(recipe, bag, matrix, width, gridWidth, ingredientIndex + 1, budget)) return true;
            matrix.setItem(matrixSlot, ItemStack.EMPTY);
            ItemStack restored = bag.getItem(slot);
            if (restored.isEmpty()) bag.setItem(slot, one); else restored.grow(1);
        }
        return false;
    }

    private static boolean workbench(ServerPlayer p) {
        for (BlockPos pos : BlockPos.betweenClosed(p.blockPosition().offset(-3, -2, -3), p.blockPosition().offset(3, 2, 3))) {
            if (!p.serverLevel().hasChunkAt(pos) || !p.level().getBlockState(pos).is(Blocks.CRAFTING_TABLE)
                || !PlayerOperations.reach(p, pos) || !p.serverLevel().mayInteract(p, pos)) continue;
            var hit = p.level().clip(new net.minecraft.world.level.ClipContext(p.getEyePosition(), net.minecraft.world.phys.Vec3.atCenterOf(pos),
                net.minecraft.world.level.ClipContext.Block.COLLIDER, net.minecraft.world.level.ClipContext.Fluid.NONE, p));
            if (hit.getBlockPos().equals(pos)) return true;
        }
        return false;
    }
    public static Map<String, Integer> counts(ServerPlayer p) {
        Map<String, Integer> result = new HashMap<>();
        for (ItemStack stack : p.getInventory().items) if (!stack.isEmpty())
            result.merge(net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(), stack.getCount(), Integer::sum);
        return result;
    }
    public static Map<String, Integer> delta(Map<String, Integer> before, Map<String, Integer> after) {
        Map<String, Integer> result = new HashMap<>();
        Set<String> keys = new HashSet<>(before.keySet()); keys.addAll(after.keySet());
        for (String key : keys) { int difference = after.getOrDefault(key, 0) - before.getOrDefault(key, 0); if (difference != 0) result.put(key, difference); }
        return result;
    }
}