diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 1105b7a52..ecb23100b 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -296,6 +296,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, // Some client updates this.playerConnection.sendPacket(getPropertiesPacket()); // Send default properties + triggerStatus((byte) (24 + permissionLevel)); // Set permission level refreshHealth(); // Heal and send health packet refreshAbilities(); // Send abilities packet @@ -424,6 +425,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, RespawnPacket respawnPacket = new RespawnPacket(getDimensionType(), getDimensionType().getName().asString(), 0, gameMode, gameMode, false, levelFlat, true); getPlayerConnection().sendPacket(respawnPacket); + PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(this); EventDispatcher.call(respawnEvent); triggerStatus((byte) (24 + permissionLevel)); // Set permission level @@ -433,6 +435,16 @@ public class Player extends LivingEntity implements CommandSender, Localizable, teleport(respawnEvent.getRespawnPosition()).thenRun(this::refreshAfterTeleport); } + /** + * Sends necessary packets to synchronize player data after a {@link RespawnPacket} + */ + private void refreshClientStateAfterRespawn() { + this.playerConnection.sendPacket(new UpdateHealthPacket(this.getHealth(), food, foodSaturation)); + this.playerConnection.sendPacket(new SetExperiencePacket(exp, level, 0)); + triggerStatus((byte) (24 + permissionLevel)); // Set permission level + refreshAbilities(); + } + /** * Refreshes the command list for this player. This checks the * {@link net.minestom.server.command.builder.condition.CommandCondition}s @@ -901,6 +913,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, playerConnection.sendPacket(destroyEntitiesPacket); playerConnection.sendPacket(respawnPacket); playerConnection.sendPacket(addPlayerPacket); + refreshClientStateAfterRespawn(); { // Remove player @@ -1207,7 +1220,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } /** - * Changes the player {@link GameMode}. + * Changes the player {@link GameMode} * * @param gameMode the new player GameMode */ @@ -1219,6 +1232,27 @@ public class Player extends LivingEntity implements CommandSender, Localizable, sendPacketToViewersAndSelf(new PlayerInfoPacket(PlayerInfoPacket.Action.UPDATE_GAMEMODE, new PlayerInfoPacket.UpdateGameMode(getUuid(), gameMode))); } + + // The client updates their abilities based on the GameMode as follows + switch (gameMode) { + case CREATIVE -> { + this.allowFlying = true; + this.instantBreak = true; + this.invulnerable = true; + } + case SPECTATOR -> { + this.allowFlying = true; + this.instantBreak = false; + this.invulnerable = true; + this.flying = true; + } + default -> { + this.allowFlying = false; + this.instantBreak = false; + this.invulnerable = false; + this.flying = false; + } + } } /** @@ -1242,6 +1276,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.dimensionType = dimensionType; sendPacket(new RespawnPacket(dimensionType, dimensionType.getName().asString(), 0, gameMode, gameMode, false, levelFlat, true)); + refreshClientStateAfterRespawn(); } /** @@ -1465,10 +1500,13 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.permissionLevel = permissionLevel; - // Magic values: https://wiki.vg/Entity_statuses#Player - // TODO remove magic values - final byte permissionLevelStatus = (byte) (24 + permissionLevel); - triggerStatus(permissionLevelStatus); + // Condition to prevent sending the packets before spawning the player + if (isActive()) { + // Magic values: https://wiki.vg/Entity_statuses#Player + // TODO remove magic values + final byte permissionLevelStatus = (byte) (24 + permissionLevel); + triggerStatus(permissionLevelStatus); + } } /** diff --git a/src/test/java/net/minestom/server/entity/EntityInstanceIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityInstanceIntegrationTest.java index 2ed715210..3e09644aa 100644 --- a/src/test/java/net/minestom/server/entity/EntityInstanceIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityInstanceIntegrationTest.java @@ -32,20 +32,6 @@ public class EntityInstanceIntegrationTest { assertEquals(instance, player.getInstance()); } - @Test - public void playerJoinPacket(Env env) { - var instance = env.createFlatInstance(); - var connection = env.createConnection(); - var tracker = connection.trackIncoming(JoinGamePacket.class); - var tracker2 = connection.trackIncoming(ServerPacket.class); - var player = connection.connect(instance, new Pos(0, 40, 0)).join(); - assertEquals(instance, player.getInstance()); - assertEquals(new Pos(0, 40, 0), player.getPosition()); - - assertEquals(1, tracker.collect().size()); - assertTrue(tracker2.collect().size() > 1); - } - @Test public void playerSwitch(Env env) { var instance = env.createFlatInstance(); diff --git a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java new file mode 100644 index 000000000..81015cd84 --- /dev/null +++ b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java @@ -0,0 +1,122 @@ +package net.minestom.server.entity.player; + +import net.minestom.server.api.Env; +import net.minestom.server.api.EnvTest; +import net.minestom.server.api.TestConnection; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.play.*; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.world.DimensionType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@EnvTest +public class PlayerIntegrationTest { + + /** + * Test to see whether player abilities are updated correctly when changing gamemodes + */ + @Test + public void gamemodeTest(Env env) { + var instance = env.createFlatInstance(); + var connection = env.createConnection(); + var player = connection.connect(instance, new Pos(0, 42, 0)).join(); + assertEquals(instance, player.getInstance()); + + player.setGameMode(GameMode.CREATIVE); + assertAbilities(player, true, false, true, true); + player.setGameMode(GameMode.SPECTATOR); + assertAbilities(player, true, true, true, false); + player.setGameMode(GameMode.CREATIVE); + assertAbilities(player, true, true, true, true); + player.setGameMode(GameMode.ADVENTURE); + assertAbilities(player, false, false, false, false); + player.setGameMode(GameMode.SURVIVAL); + assertAbilities(player, false, false, false, false); + } + + private void assertAbilities(Player player, boolean isInvulnerable, boolean isFlying, boolean isAllowFlying, + boolean isInstantBreak) { + assertEquals(isInvulnerable, player.isInvulnerable()); + assertEquals(isFlying, player.isFlying()); + assertEquals(isAllowFlying, player.isAllowFlying()); + assertEquals(isInstantBreak, player.isInstantBreak()); + } + + @Test + public void playerJoinPackets(Env env) { + var instance = env.createFlatInstance(); + var connection = env.createConnection(); + + final var packets = List.of( + JoinGamePacket.class, ServerDifficultyPacket.class, SpawnPositionPacket.class, + DeclareCommandsPacket.class, EntityPropertiesPacket.class, EntityStatusPacket.class, + UpdateHealthPacket.class, PlayerAbilitiesPacket.class + ); + final List> trackers = new ArrayList<>(); + for (var packet : packets) { + trackers.add(connection.trackIncoming(packet)); + } + + var trackerAll = connection.trackIncoming(ServerPacket.class); + + var player = connection.connect(instance, new Pos(0, 40, 0)).join(); + assertEquals(instance, player.getInstance()); + assertEquals(new Pos(0, 40, 0), player.getPosition()); + + for (var tracker : trackers) { + assertEquals(1, tracker.collect().size()); + } + assertTrue(trackerAll.collect().size() > packets.size()); + } + + /** + * Test to see whether the packets from Player#refreshPlayer are sent + * when changing dimensions + */ + @Test + public void refreshPlayerTest(Env env) { + final int TEST_PERMISSION_LEVEL = 2; + final var testDimension = DimensionType.builder(NamespaceID.from("minestom:test_dimension")).build(); + env.process().dimension().addDimension(testDimension); + + var instance = env.createFlatInstance(); + var instance2 = env.process().instance().createInstanceContainer(testDimension); + + var connection = env.createConnection(); + var player = connection.connect(instance, new Pos(0, 42, 0)).join(); + assertEquals(instance, player.getInstance()); + + var tracker1 = connection.trackIncoming(UpdateHealthPacket.class); + var tracker2 = connection.trackIncoming(SetExperiencePacket.class); + var trackerStatus = connection.trackIncoming(EntityStatusPacket.class); + var tracker4 = connection.trackIncoming(PlayerAbilitiesPacket.class); + + player.setPermissionLevel(TEST_PERMISSION_LEVEL); + + // #join may cause the thread to hang as scheduled for the next tick when initially in a pool + Assertions.assertTimeout(Duration.ofSeconds(2), () -> player.setInstance(instance2).join()); + assertEquals(instance2, player.getInstance()); + + assertEquals(1, tracker1.collect().size()); + assertEquals(1, tracker2.collect().size()); + assertEquals(2, trackerStatus.collect().size()); + assertEquals(1, tracker4.collect().size()); + + // Ensure that the player was sent the permission levels + for (var statusPacket : trackerStatus.collect()) { + assertEquals(player.getEntityId(), statusPacket.entityId()); + assertEquals(24 + TEST_PERMISSION_LEVEL, statusPacket.status()); // TODO: Remove magic value of 24 + } + } + +}