From 8dfe4a20c69aac0d150c0941f62e035a73c37c76 Mon Sep 17 00:00:00 2001 From: mworzala Date: Sun, 1 Dec 2024 20:37:41 -0500 Subject: [PATCH] chore: support relative velocity in entity teleportation --- .../net/minestom/server/entity/Entity.java | 49 +++++++++++++------ .../net/minestom/server/entity/Player.java | 12 +++-- .../minestom/server/entity/RelativeFlags.java | 3 +- .../minestom/server/item/ItemComponent.java | 5 +- .../server/utils/position/PositionUtils.java | 8 +++ .../entity/player/PlayerIntegrationTest.java | 3 +- .../server/item/component/StringTest.java | 3 +- .../server/network/PacketWriteReadTest.java | 13 ++++- 8 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 456fad476..67c1e4768 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -274,6 +274,31 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } } + public @NotNull CompletableFuture teleport(@NotNull Pos position) { + return teleport(position, null, RelativeFlags.NONE); + } + + public @NotNull CompletableFuture teleport(@NotNull Pos position, @NotNull Vec velocity) { + return teleport(position, velocity, null, RelativeFlags.NONE); + } + + public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks, + @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { + return teleport(position, chunks, flags, true); + } + + public @NotNull CompletableFuture teleport(@NotNull Pos position, @NotNull Vec velocity, long @Nullable [] chunks, + @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { + return teleport(position, velocity, chunks, flags, true); + } + + public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks, + @MagicConstant(flagsFromClass = RelativeFlags.class) int flags, + boolean shouldConfirm) { + // Use delta coord if not providing a delta velocity (to avoid resetting velocity) + return teleport(position, Vec.ZERO, chunks, flags | RelativeFlags.DELTA_COORD, shouldConfirm); + } + /** * Teleports the entity only if the chunk at {@code position} is loaded or if * {@link Instance#hasEnabledAutoChunkLoad()} returns true. @@ -287,7 +312,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev * @param shouldConfirm if false, the teleportation will be done without confirmation * @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, @NotNull Vec velocity, long @Nullable [] chunks, @MagicConstant(flagsFromClass = RelativeFlags.class) int flags, boolean shouldConfirm) { Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!"); @@ -296,12 +321,14 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev EventDispatcher.call(event); final Pos globalPosition = PositionUtils.getPositionWithRelativeFlags(this.position, position, flags); + final Vec globalVelocity = PositionUtils.getVelocityWithRelativeFlags(this.velocity, velocity, flags); final Runnable endCallback = () -> { this.previousPosition = this.position; this.position = globalPosition; + this.velocity = globalVelocity; refreshCoordinate(globalPosition); - if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, flags, shouldConfirm); + if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, velocity, flags, shouldConfirm); else synchronizePosition(); }; @@ -320,15 +347,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } } - public @NotNull CompletableFuture teleport(@NotNull Pos position, long @Nullable [] chunks, - @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { - return teleport(position, chunks, flags, true); - } - - public @NotNull CompletableFuture teleport(@NotNull Pos position) { - return teleport(position, null, RelativeFlags.NONE); - } - /** * Changes the view of the entity. * @@ -1236,8 +1254,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev final Chunk chunk = getChunk(); assert chunk != null; if (distanceX > 8 || distanceY > 8 || distanceZ > 8) { - // TODO(1.21.2) should we be setting delta to zero? - PacketViewableUtils.prepareViewablePacket(chunk, new EntityTeleportPacket(getEntityId(), position, Vec.ZERO, 0, isOnGround()), this); + // Send relative 0 velocity to avoid affecting it in this case + PacketViewableUtils.prepareViewablePacket(chunk, new EntityTeleportPacket(getEntityId(), position, + Vec.ZERO, RelativeFlags.DELTA_COORD, isOnGround()), this); nextSynchronizationTick = synchronizationTicks + 1; } else if (positionChange && viewChange) { PacketViewableUtils.prepareViewablePacket(chunk, EntityPositionAndRotationPacket.getPacket(getEntityId(), position, @@ -1521,8 +1540,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } /** - * Used to synchronize entity position with viewers by sending an - * {@link EntityTeleportPacket} and {@link EntityHeadLookPacket} to viewers. + * Used to synchronize entity position with viewers by sending a full + * {@link EntityPositionSyncPacket} to viewers. */ @ApiStatus.Internal protected void synchronizePosition() { diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 1a401ab0f..d437b13fb 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -94,6 +94,7 @@ import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; +import org.intellij.lang.annotations.MagicConstant; import org.jctools.queues.MpscArrayQueue; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -733,7 +734,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou sendPendingChunks(); // Send available first chunk immediately to prevent falling through the floor } - synchronizePositionAfterTeleport(spawnPosition, 0, true); // So the player doesn't get stuck + synchronizePositionAfterTeleport(spawnPosition, Vec.ZERO, RelativeFlags.NONE, true); // So the player doesn't get stuck if (dimensionChange) { sendPacket(new SpawnPositionPacket(spawnPosition, 0)); @@ -811,7 +812,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou // In the vanilla server they have an anticheat which teleports the client back if they enter the floor, // but since Minestom does not have an anticheat this provides a similar effect. if (needsChunkPositionSync) { - synchronizePositionAfterTeleport(getPosition(), RelativeFlags.NONE, true); + synchronizePositionAfterTeleport(getPosition(), Vec.ZERO, RelativeFlags.NONE, true); needsChunkPositionSync = false; } } finally { @@ -1818,10 +1819,11 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou * @param shouldConfirm if false, the teleportation will be done without confirmation */ @ApiStatus.Internal - void synchronizePositionAfterTeleport(@NotNull Pos position, int relativeFlags, boolean shouldConfirm) { + void synchronizePositionAfterTeleport(@NotNull Pos position, @NotNull Point velocity, + @MagicConstant(flagsFromClass = RelativeFlags.class) int relativeFlags, + boolean shouldConfirm) { int teleportId = shouldConfirm ? getNextTeleportId() : -1; - // TODO(1.21.2): should delta be zero? - sendPacket(new PlayerPositionAndLookPacket(teleportId, position, Vec.ZERO, position.yaw(), position.pitch(), (byte) relativeFlags)); + sendPacket(new PlayerPositionAndLookPacket(teleportId, position, velocity, position.yaw(), position.pitch(), relativeFlags)); super.synchronizePosition(); } diff --git a/src/main/java/net/minestom/server/entity/RelativeFlags.java b/src/main/java/net/minestom/server/entity/RelativeFlags.java index b4014b4ad..4881d61b1 100644 --- a/src/main/java/net/minestom/server/entity/RelativeFlags.java +++ b/src/main/java/net/minestom/server/entity/RelativeFlags.java @@ -17,6 +17,7 @@ public class RelativeFlags { public static final int COORD = X | Y | Z; public static final int VIEW = YAW | PITCH; - public static final int DELTA = DELTA_X | DELTA_Y | DELTA_Z | ROTATE_DELTA; + public static final int DELTA_COORD = DELTA_X | DELTA_Y | DELTA_Z; + public static final int DELTA = DELTA_COORD | ROTATE_DELTA; public static final int ALL = COORD | VIEW | DELTA; } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/item/ItemComponent.java b/src/main/java/net/minestom/server/item/ItemComponent.java index 4ecea7c68..849c8a0cc 100644 --- a/src/main/java/net/minestom/server/item/ItemComponent.java +++ b/src/main/java/net/minestom/server/item/ItemComponent.java @@ -1,5 +1,6 @@ package net.minestom.server.item; +import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.Component; import net.kyori.adventure.util.RGBLike; import net.minestom.server.color.Color; @@ -95,8 +96,8 @@ public final class ItemComponent { public static final DataComponent> CONTAINER = register("container", ItemStack.NETWORK_TYPE.list(256), BinaryTagSerializer.ITEM.list()); public static final DataComponent BLOCK_STATE = register("block_state", ItemBlockState.NETWORK_TYPE, ItemBlockState.NBT_TYPE); public static final DataComponent> BEES = register("bees", Bee.NETWORK_TYPE.list(Short.MAX_VALUE), Bee.NBT_TYPE.list()); - // TODO(1.21.2) Updated NBT format > https://minecraft.wiki/w/Java_Edition_1.21.2#Data_components_3 - public static final DataComponent LOCK = register("lock", null, BinaryTagSerializer.STRING); + // Lock is an item predicate which we do not support, but can be user-represented as a compound tag (an empty tag would match everything). + public static final DataComponent LOCK = register("lock", null, BinaryTagSerializer.COMPOUND); public static final DataComponent CONTAINER_LOOT = register("container_loot", null, SeededContainerLoot.NBT_TYPE); public static final NetworkBuffer.Type PATCH_NETWORK_TYPE = DataComponentMap.patchNetworkType(ItemComponent::fromId); 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 a980657c5..2203d32d5 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,7 @@ package net.minestom.server.utils.position; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.RelativeFlags; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.ApiStatus; @@ -35,4 +36,11 @@ public final class PositionUtils { float pitch = (flags & RelativeFlags.PITCH) == 0 ? modifier.pitch() : start.pitch() + modifier.pitch(); return new Pos(x, y, z, yaw, pitch); } + + public static @NotNull Vec getVelocityWithRelativeFlags(@NotNull Vec start, @NotNull Vec modifier, @MagicConstant(flagsFromClass = RelativeFlags.class) int flags) { + double x = (flags & RelativeFlags.DELTA_X) == 0 ? modifier.x() : start.x() + modifier.x(); + double y = (flags & RelativeFlags.DELTA_Y) == 0 ? modifier.y() : start.y() + modifier.y(); + double z = (flags & RelativeFlags.DELTA_Z) == 0 ? modifier.z() : start.z() + modifier.z(); + return new Vec(x, y, z); + } } 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 8a019ff35..4ccd385a6 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerIntegrationTest.java @@ -248,7 +248,8 @@ public class PlayerIntegrationTest { assertEquals(startingPlayerPos.withView(30, 20), player.getPosition()); tracker.assertSingle(PlayerPositionAndLookPacket.class, packet -> { - assertEquals(RelativeFlags.COORD, packet.flags()); + // Should be relative coord and velocity because we are only trying to change the view. + assertEquals(RelativeFlags.COORD | RelativeFlags.DELTA_COORD, packet.flags()); assertEquals(new Pos(0, 0, 0, 30, 20), packet.position()); }); } diff --git a/src/test/java/net/minestom/server/item/component/StringTest.java b/src/test/java/net/minestom/server/item/component/StringTest.java index 72fc0b041..09278bf1d 100644 --- a/src/test/java/net/minestom/server/item/component/StringTest.java +++ b/src/test/java/net/minestom/server/item/component/StringTest.java @@ -14,8 +14,7 @@ public class StringTest extends AbstractItemComponentTest { // as a reminder that tests should be added for that new component type. private static final List> SHARED_COMPONENTS = List.of( ItemComponent.INSTRUMENT, - ItemComponent.NOTE_BLOCK_SOUND, - ItemComponent.LOCK + ItemComponent.NOTE_BLOCK_SOUND ); @Override diff --git a/src/test/java/net/minestom/server/network/PacketWriteReadTest.java b/src/test/java/net/minestom/server/network/PacketWriteReadTest.java index 51234075b..ee15d304d 100644 --- a/src/test/java/net/minestom/server/network/PacketWriteReadTest.java +++ b/src/test/java/net/minestom/server/network/PacketWriteReadTest.java @@ -24,7 +24,9 @@ import net.minestom.server.network.packet.server.play.*; import net.minestom.server.network.packet.server.status.ResponsePacket; import net.minestom.server.network.player.GameProfile; import net.minestom.server.recipe.Recipe; +import net.minestom.server.recipe.RecipeBookCategory; import net.minestom.server.recipe.RecipeProperty; +import net.minestom.server.recipe.display.RecipeDisplay; import net.minestom.server.recipe.display.SlotDisplay; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -80,7 +82,12 @@ public class PacketWriteReadTest { SERVER_PACKETS.add(new ClearTitlesPacket(false)); SERVER_PACKETS.add(new CloseWindowPacket((byte) 2)); SERVER_PACKETS.add(new CollectItemPacket(5, 5, 5)); -// SERVER_PACKETS.add(new PlaceGhostRecipePacket((byte) 2, "recipe")); // TODO(1.21.2) + var recipeDisplay = new RecipeDisplay.CraftingShapeless( + List.of(new SlotDisplay.Item(Material.STONE)), + new SlotDisplay.Item(Material.STONE_BRICKS), + new SlotDisplay.Item(Material.CRAFTING_TABLE) + ); + SERVER_PACKETS.add(new PlaceGhostRecipePacket(0, recipeDisplay)); SERVER_PACKETS.add(new DeathCombatEventPacket(5, COMPONENT)); SERVER_PACKETS.add(new DeclareRecipesPacket(Map.of( RecipeProperty.SMITHING_BASE, List.of(Material.STONE), @@ -93,7 +100,9 @@ public class PacketWriteReadTest { List.of(new DeclareRecipesPacket.StonecutterRecipe(new Recipe.Ingredient(Material.DIAMOND), new SlotDisplay.ItemStack(ItemStack.of(Material.GOLD_BLOCK)))) )); - // TODO(1.21.2) recipe book add/remove + SERVER_PACKETS.add(new RecipeBookAddPacket(List.of(new RecipeBookAddPacket.Entry(1, recipeDisplay, null, + RecipeBookCategory.CRAFTING_MISC, List.of(new Recipe.Ingredient(Material.STONE)), true, true)), false)); + SERVER_PACKETS.add(new RecipeBookRemovePacket(List.of(1))); SERVER_PACKETS.add(new DestroyEntitiesPacket(List.of(5, 5, 5))); SERVER_PACKETS.add(new DisconnectPacket(COMPONENT));