Volver al índice
src/main/java/ar/com/companeros/PlayerOperationTests.java
package ar.com.companeros;
import ar.com.companeros.action.ToolResult;
import ar.com.companeros.body.PlayerSessions;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.identity.AgentState;
import ar.com.companeros.world.PlayerCrafting;
import ar.com.companeros.world.PlayerOperations;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.gametest.framework.BeforeBatch;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;
/** Cada batch usa una única sesión real, respetando el límite del servidor GameTest. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class PlayerOperationTests {
// Los datos de protección se reinician exclusivamente en este mundo de fixtures.
@BeforeBatch(batch = "playerops_place") public static void resetPlace(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_store") public static void resetStore(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_store_veto") public static void resetStoreVeto(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_crafting") public static void resetCrafting(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_remainders") public static void resetRemainders(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_mining") public static void resetMining(ServerLevel level) { reset(level); }
@BeforeBatch(batch = "playerops_mining_veto") public static void resetMiningVeto(ServerLevel level) { reset(level); }
private static void reset(ServerLevel level) { level.getDataStorage().set("companeros_zonas", new WorkArea()); }
@GameTest(template = "empty", batch = "playerops_place")
public void registeredPlayerPlacementConsumesMaterialAndHonorsVeto(GameTestHelper helper) {
Fixture fixture = create(helper);
BlockPos target = helper.absolutePos(new BlockPos(4, 2, 3));
try {
fixture.player.getInventory().setItem(0, new ItemStack(Items.OAK_PLANKS, 2));
PlayerOperations.Operation operation = PlayerOperations.place(fixture.player, UUID.randomUUID(), target, Items.OAK_PLANKS);
ToolResult result = operation.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(result.succeeded() && helper.getLevel().getBlockState(target).is(Blocks.OAK_PLANKS)
&& fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 1, "Coloca un bloque real pagando una tabla");
operation.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 1, "El resultado terminado no repite el consumo");
helper.getLevel().setBlock(target, Blocks.AIR.defaultBlockState(), 3);
PlacementVeto veto = new PlacementVeto(fixture.id, target);
MinecraftForge.EVENT_BUS.register(veto);
try {
ToolResult blocked = PlayerOperations.place(fixture.player, UUID.randomUUID(), target, Items.OAK_PLANKS)
.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(veto.events.get() > 0 && !blocked.succeeded()
&& helper.getLevel().getBlockState(target).isAir()
&& fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 1,
"Un veto Forge revierte la colocación y conserva el material");
} finally { MinecraftForge.EVENT_BUS.unregister(veto); }
helper.succeed();
} finally { fixture.close(); }
}
@GameTest(template = "empty", batch = "playerops_store")
public void fullAndPartialChestTransfersConserveItems(GameTestHelper helper) {
Fixture fixture = create(helper);
BlockPos target = helper.absolutePos(new BlockPos(4, 2, 3));
try {
helper.getLevel().setBlock(target, Blocks.CHEST.defaultBlockState(), 3);
ChestBlockEntity chest = (ChestBlockEntity) helper.getLevel().getBlockEntity(target);
for (int slot = 0; slot < chest.getContainerSize(); slot++) chest.setItem(slot, new ItemStack(Items.STONE, 64));
fixture.player.getInventory().setItem(0, new ItemStack(Items.STONE, 5));
int before = chestCount(chest);
ToolResult full = PlayerOperations.store(fixture.player, UUID.randomUUID(), target, Items.STONE, 5)
.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(!full.succeeded() && full.reason() == ToolResult.Reason.INVENTORY_FULL
&& fixture.player.getInventory().countItem(Items.STONE) == 5 && chestCount(chest) == before,
"Un cofre lleno no consume ni duplica los materiales");
chest.setItem(0, new ItemStack(Items.STONE, 60));
before = chestCount(chest);
ToolResult partial = PlayerOperations.store(fixture.player, UUID.randomUUID(), target, Items.STONE, 5)
.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(partial.succeeded() && fixture.player.getInventory().countItem(Items.STONE) == 1
&& chestCount(chest) == before + 4 && partial.inventoryDelta().getOrDefault("minecraft:stone", 0) == -4,
"Una transferencia parcial cobra exactamente lo que el cofre aceptó");
helper.succeed();
} finally { fixture.close(); }
}
@GameTest(template = "empty", batch = "playerops_store_veto")
public void containerInteractionVetoPreventsDirectCapabilityDeposit(GameTestHelper helper) {
Fixture fixture = create(helper);
BlockPos target = helper.absolutePos(new BlockPos(4, 2, 3));
try {
helper.getLevel().setBlock(target, Blocks.CHEST.defaultBlockState(), 3);
ChestBlockEntity chest = (ChestBlockEntity) helper.getLevel().getBlockEntity(target);
fixture.player.getInventory().setItem(0, new ItemStack(Items.STONE, 5));
InteractionVeto veto = new InteractionVeto(fixture.id, target);
MinecraftForge.EVENT_BUS.register(veto);
try {
ToolResult result = PlayerOperations.store(fixture.player, UUID.randomUUID(), target, Items.STONE, 5)
.tick(helper.getLevel().getServer().getTickCount());
helper.assertTrue(veto.events.get() > 0 && !result.succeeded()
&& fixture.player.getInventory().countItem(Items.STONE) == 5 && chestCount(chest) == 0,
"La capacidad del cofre no permite eludir un veto de interacción");
} finally { MinecraftForge.EVENT_BUS.unregister(veto); }
helper.succeed();
} finally { fixture.close(); }
}
@GameTest(template = "empty", batch = "playerops_crafting")
public void canonicalCraftingConsumesIngredientsAndRejectsFullInventory(GameTestHelper helper) {
Fixture fixture = create(helper);
try {
fixture.player.getInventory().setItem(0, new ItemStack(Items.OAK_LOG));
ToolResult made = PlayerCrafting.craft(fixture.player, UUID.randomUUID(), Items.OAK_PLANKS);
helper.assertTrue(made.succeeded() && fixture.player.getInventory().countItem(Items.OAK_LOG) == 0
&& fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 4,
"La receta real consume un tronco y produce cuatro tablas en el inventario del jugador");
fixture.player.getInventory().clearContent();
for (int slot = 0; slot < 36; slot++) fixture.player.getInventory().setItem(slot, new ItemStack(Items.STONE, 64));
fixture.player.getInventory().setItem(0, new ItemStack(Items.OAK_LOG, 64));
ToolResult full = PlayerCrafting.craft(fixture.player, UUID.randomUUID(), Items.OAK_PLANKS);
helper.assertTrue(!full.succeeded() && full.reason() == ToolResult.Reason.INVENTORY_FULL
&& fixture.player.getInventory().countItem(Items.OAK_LOG) == 64
&& fixture.player.getInventory().countItem(Items.STONE) == 35 * 64
&& fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 0,
"Si no cabe el resultado, la transacción conserva todos los ingredientes");
helper.succeed();
} finally { fixture.close(); }
}
@GameTest(template = "empty", batch = "playerops_remainders")
public void craftingNeedsWorkbenchAndReturnsRealContainers(GameTestHelper helper) {
Fixture fixture = create(helper);
try {
fixture.player.getInventory().setItem(0, new ItemStack(Items.OAK_PLANKS, 3));
fixture.player.getInventory().setItem(1, new ItemStack(Items.STICK, 2));
ToolResult missing = PlayerCrafting.craft(fixture.player, UUID.randomUUID(), Items.WOODEN_PICKAXE);
helper.assertTrue(!missing.succeeded() && fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 3
&& fixture.player.getInventory().countItem(Items.STICK) == 2, "Una receta 3×3 no aparece sin mesa");
helper.setBlock(new BlockPos(4, 2, 3), Blocks.CRAFTING_TABLE);
ToolResult pickaxe = PlayerCrafting.craft(fixture.player, UUID.randomUUID(), Items.WOODEN_PICKAXE);
helper.assertTrue(pickaxe.succeeded() && fixture.player.getInventory().countItem(Items.WOODEN_PICKAXE) == 1
&& fixture.player.getInventory().countItem(Items.OAK_PLANKS) == 0
&& fixture.player.getInventory().countItem(Items.STICK) == 0, "Usa una mesa alcanzable y materiales reales");
fixture.player.getInventory().clearContent();
for (int slot = 0; slot < 3; slot++) fixture.player.getInventory().setItem(slot, new ItemStack(Items.MILK_BUCKET));
fixture.player.getInventory().setItem(3, new ItemStack(Items.SUGAR, 2));
fixture.player.getInventory().setItem(4, new ItemStack(Items.EGG));
fixture.player.getInventory().setItem(5, new ItemStack(Items.WHEAT, 3));
ToolResult cake = PlayerCrafting.craft(fixture.player, UUID.randomUUID(), Items.CAKE);
helper.assertTrue(cake.succeeded() && fixture.player.getInventory().countItem(Items.CAKE) == 1
&& fixture.player.getInventory().countItem(Items.MILK_BUCKET) == 0
&& fixture.player.getInventory().countItem(Items.BUCKET) == 3, "La receta devuelve los tres recipientes sin perderlos ni duplicarlos");
helper.succeed();
} finally { fixture.close(); }
}
@GameTest(template = "empty", batch = "playerops_mining", timeoutTicks = 100)
public void canonicalMiningProducesOneDropAndToolWear(GameTestHelper helper) {
Fixture fixture = create(helper);
BlockPos target = helper.absolutePos(new BlockPos(4, 2, 3));
helper.getLevel().setBlock(target, Blocks.STONE.defaultBlockState(), 3);
fixture.player.getInventory().setItem(0, new ItemStack(Items.WOODEN_PICKAXE));
PlayerOperations.Operation operation = PlayerOperations.mine(fixture.player, UUID.randomUUID(), target);
ToolResult[] result = { null };
helper.onEachTick(() -> {
try { result[0] = operation.tick(helper.getLevel().getServer().getTickCount()); }
catch (RuntimeException failure) { operation.cancel(); fixture.close(); throw failure; }
});
helper.runAfterDelay(60, () -> {
try {
helper.assertTrue(result[0] != null && result[0].succeeded() && helper.getLevel().getBlockState(target).isAir(),
"El GameMode del jugador termina una rotura real");
int dropped = fixture.player.getInventory().countItem(Items.COBBLESTONE);
for (ItemEntity item : helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(target).inflate(2)))
if (item.getItem().is(Items.COBBLESTONE)) dropped += item.getItem().getCount();
helper.assertTrue(dropped == 1 && fixture.player.getInventory().getItem(0).getDamageValue() > 0,
"El loot vanilla aparece una sola vez y desgasta el pico real");
helper.succeed();
} finally { operation.cancel(); fixture.close(); }
});
}
@GameTest(template = "empty", batch = "playerops_mining_veto", timeoutTicks = 100)
public void lateBreakEventVetoLeavesBlockAndToolIntact(GameTestHelper helper) {
Fixture fixture = create(helper);
BlockPos target = helper.absolutePos(new BlockPos(4, 2, 3));
helper.getLevel().setBlock(target, Blocks.STONE.defaultBlockState(), 3);
fixture.player.getInventory().setItem(0, new ItemStack(Items.WOODEN_PICKAXE));
PlayerOperations.Operation operation = PlayerOperations.mine(fixture.player, UUID.randomUUID(), target);
operation.tick(helper.getLevel().getServer().getTickCount());
BreakVeto veto = new BreakVeto(fixture.id, target);
// Se registra después del recibo; ambos listeners tienen LOWEST.
MinecraftForge.EVENT_BUS.register(veto);
ToolResult[] result = { null };
helper.onEachTick(() -> {
try { result[0] = operation.tick(helper.getLevel().getServer().getTickCount()); }
catch (RuntimeException failure) {
MinecraftForge.EVENT_BUS.unregister(veto); operation.cancel(); fixture.close(); throw failure;
}
});
helper.runAfterDelay(60, () -> {
try {
helper.assertTrue(veto.events.get() > 0 && result[0] != null && !result[0].succeeded()
&& result[0].reason() == ToolResult.Reason.PERMISSION_DENIED
&& helper.getLevel().getBlockState(target).is(Blocks.STONE), "Respeta la cancelación final del evento Forge");
helper.assertTrue(fixture.player.getInventory().countItem(Items.COBBLESTONE) == 0
&& fixture.player.getInventory().getItem(0).getDamageValue() == 0, "La rotura vetada no genera loot ni cobra durabilidad");
helper.succeed();
} finally { MinecraftForge.EVENT_BUS.unregister(veto); operation.cancel(); fixture.close(); }
});
}
// Fixtures y observación: nunca alteran recursos fuera de la arena de prueba.
private static Fixture create(GameTestHelper helper) {
for (int x = 0; x <= 13; x++) for (int z = 0; z <= 13; z++) helper.setBlock(new BlockPos(x, 1, z), Blocks.DIRT);
UUID id = UUID.randomUUID();
String name = "Ops_" + id.toString().substring(0, 8);
var server = helper.getLevel().getServer();
AgentRegistry registry = AgentRegistry.get(server);
BlockPos pos = helper.absolutePos(new BlockPos(3, 2, 3));
registry.register(id, name, null, GlobalPos.of(helper.getLevel().dimension(), pos), "quieto");
registry.markState(id, AgentState.PAUSED);
try {
ServerPlayer player = PlayerSessions.create(server, id, name, helper.getLevel(), pos);
player.setOnGround(true);
return new Fixture(helper, id, player);
} catch (RuntimeException failure) { registry.markState(id, AgentState.RETIRED); throw failure; }
}
private static int chestCount(ChestBlockEntity chest) {
int total = 0;
for (int slot = 0; slot < chest.getContainerSize(); slot++) total += chest.getItem(slot).getCount();
return total;
}
private record Fixture(GameTestHelper helper, UUID id, ServerPlayer player) {
void close() {
PlayerSessions.remove(helper.getLevel().getServer(), id);
AgentRegistry.get(helper.getLevel().getServer()).markState(id, AgentState.RETIRED);
}
}
public static final class PlacementVeto {
private final UUID actor; private final BlockPos pos; final AtomicInteger events = new AtomicInteger();
PlacementVeto(UUID actor, BlockPos pos) { this.actor = actor; this.pos = pos; }
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void veto(BlockEvent.EntityPlaceEvent event) {
if (event.getEntity() != null && event.getEntity().getUUID().equals(actor) && event.getPos().equals(pos)) {
events.incrementAndGet(); event.setCanceled(true);
}
}
}
public static final class InteractionVeto {
private final UUID actor; private final BlockPos pos; final AtomicInteger events = new AtomicInteger();
InteractionVeto(UUID actor, BlockPos pos) { this.actor = actor; this.pos = pos; }
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void veto(PlayerInteractEvent.RightClickBlock event) {
if (event.getEntity().getUUID().equals(actor) && event.getPos().equals(pos)) {
events.incrementAndGet(); event.setCanceled(true);
}
}
}
public static final class BreakVeto {
private final UUID actor; private final BlockPos pos; final AtomicInteger events = new AtomicInteger();
BreakVeto(UUID actor, BlockPos pos) { this.actor = actor; this.pos = pos; }
@SubscribeEvent(priority = EventPriority.LOWEST)
public void veto(BlockEvent.BreakEvent event) {
if (event.getPlayer().getUUID().equals(actor) && event.getPos().equals(pos)) {
events.incrementAndGet(); event.setCanceled(true);
}
}
}
}