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));
}
}