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 POSITION_FIELDS = Set.of("x", "y", "z"); private static final Set 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 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 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 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 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 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 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 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)); } }