chore: support relative velocity in entity teleportation

This commit is contained in:
mworzala 2024-12-01 20:37:41 -05:00 committed by Matt Worzala
parent c44fe9db46
commit 8dfe4a20c6
8 changed files with 68 additions and 28 deletions

View File

@ -274,6 +274,31 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
} }
} }
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position) {
return teleport(position, null, RelativeFlags.NONE);
}
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, @NotNull Vec velocity) {
return teleport(position, velocity, null, RelativeFlags.NONE);
}
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, long @Nullable [] chunks,
@MagicConstant(flagsFromClass = RelativeFlags.class) int flags) {
return teleport(position, chunks, flags, true);
}
public @NotNull CompletableFuture<Void> 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<Void> 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 * Teleports the entity only if the chunk at {@code position} is loaded or if
* {@link Instance#hasEnabledAutoChunkLoad()} returns true. * {@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 * @param shouldConfirm if false, the teleportation will be done without confirmation
* @throws IllegalStateException if you try to teleport an entity before settings its instance * @throws IllegalStateException if you try to teleport an entity before settings its instance
*/ */
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, long @Nullable [] chunks, public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, @NotNull Vec velocity, long @Nullable [] chunks,
@MagicConstant(flagsFromClass = RelativeFlags.class) int flags, @MagicConstant(flagsFromClass = RelativeFlags.class) int flags,
boolean shouldConfirm) { boolean shouldConfirm) {
Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!"); 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); EventDispatcher.call(event);
final Pos globalPosition = PositionUtils.getPositionWithRelativeFlags(this.position, position, flags); final Pos globalPosition = PositionUtils.getPositionWithRelativeFlags(this.position, position, flags);
final Vec globalVelocity = PositionUtils.getVelocityWithRelativeFlags(this.velocity, velocity, flags);
final Runnable endCallback = () -> { final Runnable endCallback = () -> {
this.previousPosition = this.position; this.previousPosition = this.position;
this.position = globalPosition; this.position = globalPosition;
this.velocity = globalVelocity;
refreshCoordinate(globalPosition); 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(); else synchronizePosition();
}; };
@ -320,15 +347,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
} }
} }
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position, long @Nullable [] chunks,
@MagicConstant(flagsFromClass = RelativeFlags.class) int flags) {
return teleport(position, chunks, flags, true);
}
public @NotNull CompletableFuture<Void> teleport(@NotNull Pos position) {
return teleport(position, null, RelativeFlags.NONE);
}
/** /**
* Changes the view of the entity. * Changes the view of the entity.
* *
@ -1236,8 +1254,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
final Chunk chunk = getChunk(); final Chunk chunk = getChunk();
assert chunk != null; assert chunk != null;
if (distanceX > 8 || distanceY > 8 || distanceZ > 8) { if (distanceX > 8 || distanceY > 8 || distanceZ > 8) {
// TODO(1.21.2) should we be setting delta to zero? // Send relative 0 velocity to avoid affecting it in this case
PacketViewableUtils.prepareViewablePacket(chunk, new EntityTeleportPacket(getEntityId(), position, Vec.ZERO, 0, isOnGround()), this); PacketViewableUtils.prepareViewablePacket(chunk, new EntityTeleportPacket(getEntityId(), position,
Vec.ZERO, RelativeFlags.DELTA_COORD, isOnGround()), this);
nextSynchronizationTick = synchronizationTicks + 1; nextSynchronizationTick = synchronizationTicks + 1;
} else if (positionChange && viewChange) { } else if (positionChange && viewChange) {
PacketViewableUtils.prepareViewablePacket(chunk, EntityPositionAndRotationPacket.getPacket(getEntityId(), position, 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 * Used to synchronize entity position with viewers by sending a full
* {@link EntityTeleportPacket} and {@link EntityHeadLookPacket} to viewers. * {@link EntityPositionSyncPacket} to viewers.
*/ */
@ApiStatus.Internal @ApiStatus.Internal
protected void synchronizePosition() { protected void synchronizePosition() {

View File

@ -94,6 +94,7 @@ import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import net.minestom.server.world.DimensionType; import net.minestom.server.world.DimensionType;
import org.intellij.lang.annotations.MagicConstant;
import org.jctools.queues.MpscArrayQueue; import org.jctools.queues.MpscArrayQueue;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; 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 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) { if (dimensionChange) {
sendPacket(new SpawnPositionPacket(spawnPosition, 0)); 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, // 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. // but since Minestom does not have an anticheat this provides a similar effect.
if (needsChunkPositionSync) { if (needsChunkPositionSync) {
synchronizePositionAfterTeleport(getPosition(), RelativeFlags.NONE, true); synchronizePositionAfterTeleport(getPosition(), Vec.ZERO, RelativeFlags.NONE, true);
needsChunkPositionSync = false; needsChunkPositionSync = false;
} }
} finally { } finally {
@ -1818,10 +1819,11 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
* @param shouldConfirm if false, the teleportation will be done without confirmation * @param shouldConfirm if false, the teleportation will be done without confirmation
*/ */
@ApiStatus.Internal @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; int teleportId = shouldConfirm ? getNextTeleportId() : -1;
// TODO(1.21.2): should delta be zero? sendPacket(new PlayerPositionAndLookPacket(teleportId, position, velocity, position.yaw(), position.pitch(), relativeFlags));
sendPacket(new PlayerPositionAndLookPacket(teleportId, position, Vec.ZERO, position.yaw(), position.pitch(), (byte) relativeFlags));
super.synchronizePosition(); super.synchronizePosition();
} }

View File

@ -17,6 +17,7 @@ public class RelativeFlags {
public static final int COORD = X | Y | Z; public static final int COORD = X | Y | Z;
public static final int VIEW = YAW | PITCH; 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; public static final int ALL = COORD | VIEW | DELTA;
} }

View File

@ -1,5 +1,6 @@
package net.minestom.server.item; package net.minestom.server.item;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.util.RGBLike; import net.kyori.adventure.util.RGBLike;
import net.minestom.server.color.Color; import net.minestom.server.color.Color;
@ -95,8 +96,8 @@ public final class ItemComponent {
public static final DataComponent<List<ItemStack>> CONTAINER = register("container", ItemStack.NETWORK_TYPE.list(256), BinaryTagSerializer.ITEM.list()); public static final DataComponent<List<ItemStack>> CONTAINER = register("container", ItemStack.NETWORK_TYPE.list(256), BinaryTagSerializer.ITEM.list());
public static final DataComponent<ItemBlockState> BLOCK_STATE = register("block_state", ItemBlockState.NETWORK_TYPE, ItemBlockState.NBT_TYPE); public static final DataComponent<ItemBlockState> BLOCK_STATE = register("block_state", ItemBlockState.NETWORK_TYPE, ItemBlockState.NBT_TYPE);
public static final DataComponent<List<Bee>> BEES = register("bees", Bee.NETWORK_TYPE.list(Short.MAX_VALUE), Bee.NBT_TYPE.list()); public static final DataComponent<List<Bee>> 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 // 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<String> LOCK = register("lock", null, BinaryTagSerializer.STRING); public static final DataComponent<CompoundBinaryTag> LOCK = register("lock", null, BinaryTagSerializer.COMPOUND);
public static final DataComponent<SeededContainerLoot> CONTAINER_LOOT = register("container_loot", null, SeededContainerLoot.NBT_TYPE); public static final DataComponent<SeededContainerLoot> CONTAINER_LOOT = register("container_loot", null, SeededContainerLoot.NBT_TYPE);
public static final NetworkBuffer.Type<DataComponentMap> PATCH_NETWORK_TYPE = DataComponentMap.patchNetworkType(ItemComponent::fromId); public static final NetworkBuffer.Type<DataComponentMap> PATCH_NETWORK_TYPE = DataComponentMap.patchNetworkType(ItemComponent::fromId);

View File

@ -1,6 +1,7 @@
package net.minestom.server.utils.position; package net.minestom.server.utils.position;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.RelativeFlags; import net.minestom.server.entity.RelativeFlags;
import org.intellij.lang.annotations.MagicConstant; import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.ApiStatus; 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(); float pitch = (flags & RelativeFlags.PITCH) == 0 ? modifier.pitch() : start.pitch() + modifier.pitch();
return new Pos(x, y, z, yaw, 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);
}
} }

View File

@ -248,7 +248,8 @@ public class PlayerIntegrationTest {
assertEquals(startingPlayerPos.withView(30, 20), player.getPosition()); assertEquals(startingPlayerPos.withView(30, 20), player.getPosition());
tracker.assertSingle(PlayerPositionAndLookPacket.class, packet -> { 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()); assertEquals(new Pos(0, 0, 0, 30, 20), packet.position());
}); });
} }

View File

@ -14,8 +14,7 @@ public class StringTest extends AbstractItemComponentTest<String> {
// as a reminder that tests should be added for that new component type. // as a reminder that tests should be added for that new component type.
private static final List<DataComponent<String>> SHARED_COMPONENTS = List.of( private static final List<DataComponent<String>> SHARED_COMPONENTS = List.of(
ItemComponent.INSTRUMENT, ItemComponent.INSTRUMENT,
ItemComponent.NOTE_BLOCK_SOUND, ItemComponent.NOTE_BLOCK_SOUND
ItemComponent.LOCK
); );
@Override @Override

View File

@ -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.packet.server.status.ResponsePacket;
import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.GameProfile;
import net.minestom.server.recipe.Recipe; import net.minestom.server.recipe.Recipe;
import net.minestom.server.recipe.RecipeBookCategory;
import net.minestom.server.recipe.RecipeProperty; import net.minestom.server.recipe.RecipeProperty;
import net.minestom.server.recipe.display.RecipeDisplay;
import net.minestom.server.recipe.display.SlotDisplay; import net.minestom.server.recipe.display.SlotDisplay;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -80,7 +82,12 @@ public class PacketWriteReadTest {
SERVER_PACKETS.add(new ClearTitlesPacket(false)); SERVER_PACKETS.add(new ClearTitlesPacket(false));
SERVER_PACKETS.add(new CloseWindowPacket((byte) 2)); SERVER_PACKETS.add(new CloseWindowPacket((byte) 2));
SERVER_PACKETS.add(new CollectItemPacket(5, 5, 5)); 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 DeathCombatEventPacket(5, COMPONENT));
SERVER_PACKETS.add(new DeclareRecipesPacket(Map.of( SERVER_PACKETS.add(new DeclareRecipesPacket(Map.of(
RecipeProperty.SMITHING_BASE, List.of(Material.STONE), RecipeProperty.SMITHING_BASE, List.of(Material.STONE),
@ -93,7 +100,9 @@ public class PacketWriteReadTest {
List.of(new DeclareRecipesPacket.StonecutterRecipe(new Recipe.Ingredient(Material.DIAMOND), List.of(new DeclareRecipesPacket.StonecutterRecipe(new Recipe.Ingredient(Material.DIAMOND),
new SlotDisplay.ItemStack(ItemStack.of(Material.GOLD_BLOCK)))) 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 DestroyEntitiesPacket(List.of(5, 5, 5)));
SERVER_PACKETS.add(new DisconnectPacket(COMPONENT)); SERVER_PACKETS.add(new DisconnectPacket(COMPONENT));