Volver al índice

src/main/java/ar/com/companeros/body/PlayerSessionTests.java

package ar.com.companeros.body;

import ar.com.companeros.Companeros;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;

/** Pruebas del ciclo de jugador registrado, separadas de las del cuerpo NPC legado. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class PlayerSessionTests {
    private static final Set<UUID> FIXTURE_IDS = new HashSet<>();
    private static String name(UUID id) { return "Ag_" + id.toString().substring(0, 8); }

    private static ServerPlayer create(GameTestHelper helper, UUID id) {
        PlayerSessions.install();
        var server = helper.getLevel().getServer();
        // GameTestPlayerList admite un solo jugador; sólo se limpian UUID creados por este fixture.
        for (UUID previous : Set.copyOf(FIXTURE_IDS)) removeFixture(server, previous);
        for (int x = 0; x <= 13; x++) for (int z = 0; z <= 13; z++)
            helper.setBlock(new BlockPos(x, 1, z), Blocks.DIRT);
        ServerPlayer player = PlayerSessions.create(server, id, name(id), helper.getLevel(),
            helper.absolutePos(new BlockPos(3, 2, 3)));
        FIXTURE_IDS.add(id);
        return player;
    }

    private static void removeFixture(net.minecraft.server.MinecraftServer server, UUID id) {
        if (PlayerSessions.isAgent(server, id)) PlayerSessions.setMovementIntent(server, id, 0, 0, false);
        PlayerSessions.remove(server, id);
        FIXTURE_IDS.remove(id);
    }

    @GameTest(batch = "body_01_registration", template = "empty")
    public void playerIsRegisteredAndHasNoInventedResources(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        try {
            ServerPlayer player = create(helper, id);
            helper.assertTrue(!(player instanceof FakePlayer), "El cuerpo no es un proxy FakePlayer");
            helper.assertTrue(server.getPlayerList().getPlayer(id) == player
                && server.getPlayerList().getPlayers().contains(player)
                && player.serverLevel().players().contains(player), "PlayerList y el nivel registran al jugador");
            helper.assertTrue(player.gameMode.getGameModeForPlayer() == GameType.SURVIVAL
                && !player.getAbilities().instabuild && !player.getAbilities().flying, "Es un jugador supervivencia");
            helper.assertTrue(player.getInventory().isEmpty(), "Comienza sin regalarle recursos");
            helper.assertTrue(player.connection.connection.isConnected()
                && PlayerSessions.deliveredPackets(server, id) > 0, "El canal local recibe la inicialización del jugador");
            long before = PlayerSessions.completedPlayerTicks(server, id);
            PlayerSessions.tick(server);
            long once = PlayerSessions.completedPlayerTicks(server, id);
            PlayerSessions.tick(server);
            helper.assertTrue(once == before + 1 && PlayerSessions.completedPlayerTicks(server, id) == once,
                "Dos llamadas del scheduler no duplican el tick físico");
            removeFixture(server, id);
            helper.assertTrue(server.getPlayerList().getPlayer(id) == null && !PlayerSessions.isAgent(server, id),
                "La salida elimina la sesión y la presencia en PlayerList");
            helper.succeed();
        } finally { removeFixture(server, id); }
    }

    @GameTest(batch = "body_02_movement", template = "empty", timeoutTicks = 100)
    public void registeredPlayerMovesAndProcessesHungerWithoutHuman(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        ServerPlayer player = create(helper, id);
        double initialZ = player.getZ();
        player.getFoodData().setSaturation(0);
        player.getFoodData().addExhaustion(12);
        PlayerSessions.setMovementIntent(server, id, 0, 1, false);
        helper.onEachTick(() -> {
            if (PlayerSessions.find(server, id) != null) PlayerSessions.setMovementIntent(server, id, 0, 1, false);
        });
        helper.runAfterDelay(30, () -> {
            try {
                ServerPlayer current = PlayerSessions.find(server, id);
                helper.assertTrue(current != null && current.getZ() > initialZ + .5,
                    "El jugador se mueve mediante física local sin un cliente humano");
                helper.assertTrue(current.getFoodData().getFoodLevel() < 20,
                    "FoodData procesa el agotamiento vanilla");
                helper.assertTrue(PlayerSessions.completedPlayerTicks(server, id) >= 29,
                    "El scheduler mantiene el tick de jugador");
                helper.succeed();
            } finally { removeFixture(server, id); }
        });
    }

    @GameTest(batch = "body_03_playerdata", template = "empty")
    public void playerDataPersistsAcrossSessionReplacement(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        try {
            ServerPlayer first = create(helper, id);
            first.getInventory().setItem(0, new ItemStack(Items.STONE, 3));
            first.getFoodData().setFoodLevel(12);
            first.setHealth(16);
            removeFixture(server, id);
            ServerPlayer loaded = create(helper, id);
            helper.assertTrue(loaded != first && loaded.getUUID().equals(id), "La identidad sobrevive al reemplazo de cuerpo");
            helper.assertTrue(loaded.getInventory().countItem(Items.STONE) == 3
                && loaded.getFoodData().getFoodLevel() == 12 && loaded.getHealth() == 16,
                "Carga inventario, hambre y salud desde playerdata sin duplicarlos");
            helper.succeed();
        } finally { removeFixture(server, id); }
    }

    @GameTest(batch = "body_04_respawn", template = "empty")
    public void vanillaRespawnKeepsIdentityAndSingleRegisteredBody(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        try {
            ServerPlayer dead = create(helper, id);
            dead.hurt(dead.damageSources().genericKill(), 1000);
            helper.assertTrue(dead.isDeadOrDying(), "La muerte real afecta al jugador");
            ServerPlayer respawned = PlayerSessions.respawn(server, id);
            helper.assertTrue(respawned != dead && respawned.getUUID().equals(id) && respawned.isAlive(),
                "El respawn vanilla sustituye el cuerpo y conserva UUID");
            helper.assertTrue(PlayerSessions.find(server, id) == respawned
                && server.getPlayerList().getPlayer(id) == respawned
                && server.getPlayerList().getPlayers().stream().filter(player -> player.getUUID().equals(id)).count() == 1,
                "Existe un único cuerpo registrado después de reaparecer");
            helper.assertTrue(respawned.connection.connection.isConnected(), "El nuevo cuerpo conserva el transporte local");
            helper.succeed();
        } finally { removeFixture(server, id); }
    }

    @GameTest(batch = "body_05_lease", template = "empty", timeoutTicks = 80)
    public void movementLeaseStopsAnAbandonedController(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        ServerPlayer player = create(helper, id);
        PlayerSessions.setMovementIntent(server, id, 0, 1, false);
        helper.runAfterDelay(20, () -> {
            ServerPlayer stopped = PlayerSessions.find(server, id);
            if (stopped == null) {
                helper.fail("La sesión se perdió antes de vencer la intención");
                return;
            }
            double stoppedZ = stopped.getZ();
            helper.runAfterDelay(10, () -> {
                try {
                    ServerPlayer current = PlayerSessions.find(server, id);
                    helper.assertTrue(current != null && Math.abs(current.getZ() - stoppedZ) < .05
                        && current.zza == 0 && current.xxa == 0,
                        "La intención caducada deja de producir movimiento tras disiparse la inercia");
                    helper.succeed();
                } finally { removeFixture(server, id); }
            });
        });
    }

    @GameTest(batch = "body_06_dimension", template = "empty")
    public void dimensionTransferPreservesTheRegisteredSession(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        try {
            ServerPlayer player = create(helper, id);
            var originalLevel = player.serverLevel();
            var originalPosition = player.position();
            var nether = server.getLevel(Level.NETHER);
            helper.assertTrue(nether != null, "El servidor de prueba debe tener dimensión Nether");
            // Teleport de fixture: prueba la transición técnica, no proporciona navegación al agente.
            player.teleportTo(nether, .5, 96, .5, 0, 0);
            helper.assertTrue(player.serverLevel() == nether && nether.players().contains(player)
                && !originalLevel.players().contains(player), "La presencia cambia de nivel sin dejar otro cuerpo");
            helper.assertTrue(server.getPlayerList().getPlayer(id) == player && PlayerSessions.find(server, id) == player
                && !player.isChangingDimension(), "UUID, sesión y confirmación de posición siguen sincronizados");
            player.teleportTo(originalLevel, originalPosition.x, originalPosition.y, originalPosition.z, 0, 0);
            helper.assertTrue(player.serverLevel() == originalLevel && !nether.players().contains(player),
                "Puede volver y retirar su presencia de la dimensión anterior");
            helper.succeed();
        } finally { removeFixture(server, id); }
    }

    @GameTest(batch = "body_07_fall", template = "empty", timeoutTicks = 160)
    public void localPhysicsAppliesRealFallDamage(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        UUID id = UUID.randomUUID();
        ServerPlayer player = create(helper, id);
        player.getFoodData().setFoodLevel(10); // Evita que la regeneración oculte el daño de la prueba.
        player.getFoodData().setSaturation(0);
        helper.runAfterDelay(65, () -> {
            ServerPlayer current = PlayerSessions.find(server, id);
            if (current == null) {
                helper.fail("La sesión se perdió antes de probar la caída");
                return;
            }
            // Se espera la protección normal de aparición antes de elevar el fixture ocho bloques.
            current.teleportTo(current.getX(), current.getY() + 8, current.getZ());
            current.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
        });
        helper.runAfterDelay(110, () -> {
            try {
                ServerPlayer landed = PlayerSessions.find(server, id);
                helper.assertTrue(landed != null && landed.isAlive() && landed.onGround()
                    && landed.getHealth() < landed.getMaxHealth(),
                    "La gravedad, colisión y caída producen daño real al jugador local");
                helper.succeed();
            } finally { removeFixture(server, id); }
        });
    }

    @GameTest(batch = "body_08_safe_spawn", template = "empty")
    public void newSpawnRejectsVoidAndUnsupportedAirBeforeJoining(GameTestHelper helper) {
        var server = helper.getLevel().getServer();
        var level = helper.getLevel();
        PlayerSessions.install();
        UUID id = UUID.randomUUID();
        BlockPos safe = helper.absolutePos(new BlockPos(3, 2, 3));
        helper.setBlock(new BlockPos(3, 1, 3), Blocks.DIRT);
        int playersBefore = server.getPlayerList().getPlayers().size();
        int sessionsBefore = PlayerSessions.players(server).size();
        try {
            assertRejectedSpawn(helper, id, new BlockPos(safe.getX(), level.getMinBuildHeight(), safe.getZ()));
            assertRejectedSpawn(helper, id, safe.above(6));
            helper.assertTrue(server.getPlayerList().getPlayers().size() == playersBefore
                && PlayerSessions.players(server).size() == sessionsBefore
                && server.getPlayerList().getPlayer(id) == null && PlayerSessions.find(server, id) == null,
                "Una aparición insegura se rechaza antes de registrar un cuerpo o cambiar participantes");
            ServerPlayer player = PlayerSessions.create(server, id, name(id), level, safe);
            FIXTURE_IDS.add(id);
            helper.assertTrue(player.getInventory().isEmpty() && player.blockPosition().equals(safe)
                && level.getBlockState(safe.below()).is(Blocks.DIRT),
                "El mismo UUID puede entrar sobre piso existente sin inventar objetos ni una plataforma");
            player.teleportTo(player.getX(), player.getY() + 6, player.getZ());
            var savedPosition = player.position();
            PlayerSessions.remove(server, id);
            ServerPlayer restored = PlayerSessions.create(server, id, name(id), level, safe);
            helper.assertTrue(restored != player && restored.position().distanceToSqr(savedPosition) < .0001
                && restored.getInventory().isEmpty(),
                "Un playerdata existente conserva una posición legítima en el aire sin recolocarla sobre el piso");
            helper.succeed();
        } finally { removeFixture(server, id); }
    }

    private static void assertRejectedSpawn(GameTestHelper helper, UUID id, BlockPos position) {
        boolean rejected = false;
        try { PlayerSessions.create(helper.getLevel().getServer(), id, name(id), helper.getLevel(), position); }
        catch (IllegalArgumentException expected) { rejected = true; }
        helper.assertTrue(rejected, "La identidad nueva necesita altura válida y apoyo sólido antes de ingresar");
    }
}