diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index a315431bb..a3fa1daa4 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -52,8 +52,10 @@ import net.minestom.server.utils.chunk.ChunkCache; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.player.PlayerUtils; +import net.minestom.server.utils.position.PositionUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; +import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -305,16 +307,20 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev * @param chunks the chunk indexes to load before teleporting the entity, * indexes are from {@link ChunkUtils#getChunkIndex(int, int)}, * can be null or empty to only load the chunk at {@code position} + * @param flags flags used to teleport the entity relatively rather than absolutely + * use {@link RelativeFlags} to see available flags * @throws IllegalStateException if you try to teleport an entity before settings its instance */ - public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks) { + public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks, + @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!"); + final Pos globalPosition = PositionUtils.getPositionWithRelativeFlags(this.position, position, flags); final Runnable endCallback = () -> { this.previousPosition = this.position; - this.position = position; - refreshCoordinate(position); - synchronizePosition(true); - sendPacketToViewers(new EntityHeadLookPacket(getEntityId(), position.yaw())); + this.position = globalPosition; + refreshCoordinate(globalPosition); + if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, flags); + else synchronizePosition(); }; if (chunks != null && chunks.length > 0) { @@ -322,9 +328,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return ChunkUtils.optionalLoadAll(instance, chunks, null).thenRun(endCallback); } final Pos currentPosition = this.position; - if (!currentPosition.sameChunk(position)) { + if (!currentPosition.sameChunk(globalPosition)) { // Ensure that the chunk is loaded - return instance.loadOptionalChunk(position).thenRun(endCallback); + return instance.loadOptionalChunk(globalPosition).thenRun(endCallback); } else { // Position is in the same chunk, keep it sync endCallback.run(); @@ -333,7 +339,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } public @NotNull CompletableFuture teleport(@NotNull Pos position) { - return teleport(position, null); + return teleport(position, null, RelativeFlags.NONE); } /** @@ -572,7 +578,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } // Scheduled synchronization if (ticks >= nextSynchronizationTick) { - synchronizePosition(false); + synchronizePosition(); } } @@ -962,7 +968,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev sendPacketToViewersAndSelf(getPassengersPacket()); // Updates the position of the new passenger, and then teleports the passenger updatePassengerPosition(position, entity); - entity.synchronizePosition(false); + entity.synchronizePosition(); } /** @@ -977,7 +983,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev if (!passengers.remove(entity)) return; entity.vehicle = null; sendPacketToViewersAndSelf(getPassengersPacket()); - entity.synchronizePosition(false); + entity.synchronizePosition(); } /** @@ -1345,7 +1351,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev * * @param newPosition the new position */ - private void refreshCoordinate(Point newPosition) { + @ApiStatus.Internal + protected void refreshCoordinate(Point newPosition) { // Passengers update final Set passengers = getPassengers(); if (!passengers.isEmpty()) { @@ -1579,15 +1586,10 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev /** * Used to synchronize entity position with viewers by sending an - * {@link EntityTeleportPacket} to viewers, in case of a player this is - * overridden in order to send an additional {@link PlayerPositionAndLookPacket} - * to itself. - * - * @param includeSelf if {@code true} and this is a {@link Player} an additional {@link PlayerPositionAndLookPacket} - * will be sent to the player itself + * {@link EntityTeleportPacket} and {@link EntityHeadLookPacket} to viewers. */ @ApiStatus.Internal - protected void synchronizePosition(boolean includeSelf) { + protected void synchronizePosition() { final Pos posCache = this.position; PacketUtils.prepareViewablePacket(currentChunk, new EntityTeleportPacket(getEntityId(), posCache, isOnGround()), this); if (posCache.yaw() != lastSyncedPosition.yaw()) { diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index b50adccb6..a6dbccdb0 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -736,7 +736,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, ChunkUtils.forChunksInRange(spawnPosition, MinecraftServer.getChunkViewDistance(), chunkAdder); } - synchronizePosition(true); // So the player doesn't get stuck + synchronizePositionAfterTeleport(spawnPosition, 0); // So the player doesn't get stuck if (dimensionChange) { sendPacket(new SpawnPositionPacket(spawnPosition, 0)); @@ -1822,15 +1822,17 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } /** - * @see Entity#synchronizePosition(boolean) + * Used to synchronize player position with viewers on spawn or after {@link Entity#teleport(Pos, long[], RelativeFlags...)} + * in cases where a {@link PlayerPositionAndLookPacket} is required + * + * @param position the position used by {@link PlayerPositionAndLookPacket} + * this may not be the same as the {@link Entity#position} + * @param relativeFlags byte flags used by {@link PlayerPositionAndLookPacket} */ - @Override @ApiStatus.Internal - protected void synchronizePosition(boolean includeSelf) { - if (includeSelf) { - sendPacket(new PlayerPositionAndLookPacket(position, (byte) 0x00, getNextTeleportId())); - } - super.synchronizePosition(includeSelf); + void synchronizePositionAfterTeleport(@NotNull Pos position, int relativeFlags) { + sendPacket(new PlayerPositionAndLookPacket(position, (byte) relativeFlags, getNextTeleportId())); + super.synchronizePosition(); } /** @@ -2370,10 +2372,13 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } } + /** + * @see #teleport(Pos, long[], int) + */ @Override - public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks) { + public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks, int flags) { chunkUpdateLimitChecker.clearHistory(); - return super.teleport(position, chunks); + return super.teleport(position, chunks, flags); } /** diff --git a/src/main/java/net/minestom/server/entity/RelativeFlags.java b/src/main/java/net/minestom/server/entity/RelativeFlags.java new file mode 100644 index 000000000..344dafad4 --- /dev/null +++ b/src/main/java/net/minestom/server/entity/RelativeFlags.java @@ -0,0 +1,13 @@ +package net.minestom.server.entity; + +public class RelativeFlags { + public static final int NONE = 0x00; + public static final int X = 0x01; + public static final int Y = 0x02; + public static final int Z = 0x04; + public static final int YAW = 0x08; + public static final int PITCH = 0x10; + public static final int COORD = X | Y | Z; + public static final int VIEW = YAW | PITCH; + public static final int ALL = COORD | VIEW; +} \ No newline at end of file diff --git a/src/main/java/net/minestom/server/utils/position/PositionUtils.java b/src/main/java/net/minestom/server/utils/position/PositionUtils.java index b00cd2800..a980657c5 100644 --- a/src/main/java/net/minestom/server/utils/position/PositionUtils.java +++ b/src/main/java/net/minestom/server/utils/position/PositionUtils.java @@ -1,6 +1,8 @@ package net.minestom.server.utils.position; import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.RelativeFlags; +import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -24,4 +26,13 @@ public final class PositionUtils { final double radians = -Math.atan2(dy, Math.max(Math.abs(dx), Math.abs(dz))); return (float) Math.toDegrees(radians); } + + public static @NotNull Pos getPositionWithRelativeFlags(@NotNull Pos start, @NotNull Pos modifier, @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { + double x = (flags & RelativeFlags.X) == 0 ? modifier.x() : start.x() + modifier.x(); + double y = (flags & RelativeFlags.Y) == 0 ? modifier.y() : start.y() + modifier.y(); + double z = (flags & RelativeFlags.Z) == 0 ? modifier.z() : start.z() + modifier.z(); + float yaw = (flags & RelativeFlags.YAW) == 0 ? modifier.yaw() : start.yaw() + modifier.yaw(); + float pitch = (flags & RelativeFlags.PITCH) == 0 ? modifier.pitch() : start.pitch() + modifier.pitch(); + return new Pos(x, y, z, yaw, pitch); + } } diff --git a/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java index 3bb347541..f9d5cc148 100644 --- a/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityTeleportIntegrationTest.java @@ -53,7 +53,7 @@ public class EntityTeleportIntegrationTest { var tracker = connection.trackIncoming(ServerPacket.class); var viewerTracker = viewerConnection.trackIncoming(ServerPacket.class); - var teleportPosition = new Pos(1, 42, 1); + var teleportPosition = new Pos(1, 42, 1).withYaw(5); player.teleport(teleportPosition).join(); assertEquals(teleportPosition, player.getPosition()); @@ -88,4 +88,21 @@ public class EntityTeleportIntegrationTest { player.teleport(teleportPosition).join(); assertEquals(teleportPosition, player.getPosition()); } + + @Test + public void playerTeleportWithFlagsTest(Env env) { + var instance = env.createFlatInstance(); + var connection = env.createConnection(); + var player = connection.connect(instance, new Pos(0, 0, 0)).join(); + + player.teleport(new Pos(10, 10, 10, 90, 0)).join(); + assertEquals(player.getPosition(), new Pos(10, 10, 10, 90, 0)); + + player.teleport(new Pos(0, 0, 0, 0, 0), null, RelativeFlags.ALL).join(); + assertEquals(player.getPosition(), new Pos(10, 10, 10, 90, 0)); + + var tracker = connection.trackIncoming(PlayerPositionAndLookPacket.class); + player.teleport(new Pos(5, 10, 2, 5, 5), null, RelativeFlags.VIEW).join(); + assertEquals(player.getPosition(), new Pos(5, 10, 2, 95, 5)); + } } diff --git a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java index 833dc5bb9..9752364c0 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java @@ -22,6 +22,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import static net.minestom.server.entity.Player.*; import static org.junit.jupiter.api.Assertions.*; @EnvTest