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