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