From 8aac26c123264e7ecf07ede8b4515669e8a545fa Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 11 Jan 2026 22:43:26 +0700 Subject: [PATCH 1/4] Initial work lerping boats. --- .../geyser/entity/type/BoatEntity.java | 86 +++++++++++++++++++ .../geyser/entity/type/LivingEntity.java | 4 + 2 files changed, 90 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 8c59e447ad0..e68c6459d32 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -26,11 +26,13 @@ package org.geysermc.geyser.entity.type; import lombok.Getter; +import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; +import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.vehicle.BoatVehicleComponent; @@ -76,6 +78,10 @@ public class BoatEntity extends Entity implements Tickable, Leashable, ClientVeh // This is the best value, I can't really found any value that doesn't look choppy and laggy or that is not too slow, blame bedrock. private final float ROWING_SPEED = 0.04f; + private Vector3f lerpPosition; + private int lerpSteps; + protected boolean dirtyYaw, dirtyHeadYaw; + public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, BoatVariant variant) { // Initial rotation is incorrect super(session, entityId, geyserId, uuid, definition, position.add(0d, definition.offset(), 0d), motion, yaw + 90, 0, yaw + 90); @@ -125,6 +131,41 @@ public void moveAbsoluteWithoutAdjustments(Vector3f position, float yaw, boolean super.moveAbsoluteRaw(position, yaw, 0, yaw, isOnGround, teleported); } + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + if ((relX != 0 || relY != 0 || relZ != 0) && position.distanceSquared(session.getPlayerEntity().position()) < 4096) { + this.dirtyYaw = yaw != this.yaw; + this.dirtyHeadYaw = headYaw != this.headYaw; + + setYaw(yaw); + setPitch(0); + setHeadYaw(headYaw); + + this.lerpPosition = Vector3f.from(lerpPosition.getX() + relX, lerpPosition.getY() + relY, lerpPosition.getZ() + relZ); + this.lerpSteps = 3; + } else { + super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); + } + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + + this.lerpPosition = position; + + // It's vanilla behaviour to lerp if the position is within 64 blocks, however we also check if the position is close enough to the player + // position to see if it can actually affect anything to save network. + if (position.distanceSquared(this.position) < 4096 && position.distanceSquared(session.getPlayerEntity().position()) < 4096) { + this.dirtyYaw = this.dirtyHeadYaw = true; + this.lerpSteps = 3; + } else { + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); + } + } + @Override public void moveRelativeRaw(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { super.moveRelativeRaw(relX, relY, relZ, yaw, 0, yaw, isOnGround); @@ -193,6 +234,51 @@ public InteractionResult interact(Hand hand) { @Override public void tick() { + if (this.lerpSteps > 0) { + float time = 1.0f / this.lerpSteps; + float lerpXTotal = GenericMath.lerp(this.position.getX(), this.lerpPosition.getX(), time); + float lerpYTotal = GenericMath.lerp(this.position.getY(), this.lerpPosition.getY(), time); + float lerpZTotal = GenericMath.lerp(this.position.getZ(), this.lerpPosition.getZ(), time); + + MoveEntityDeltaPacket moveEntityPacket = new MoveEntityDeltaPacket(); + moveEntityPacket.setRuntimeEntityId(geyserId); + moveEntityPacket.setX(lerpXTotal); + moveEntityPacket.setY(lerpYTotal); + moveEntityPacket.setZ(lerpZTotal); + moveEntityPacket.setYaw(this.yaw); + moveEntityPacket.setPitch(this.pitch); + moveEntityPacket.setHeadYaw(this.headYaw); + if (onGround) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); + } + if (lerpXTotal != this.position.getX()) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + } + if (lerpYTotal != this.position.getY()) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + } + if (lerpZTotal != this.position.getZ()) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + } + if (this.dirtyYaw) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + } + if (this.dirtyHeadYaw) { + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); + } + moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING); + + this.dirtyYaw = this.dirtyHeadYaw = false; + + // Queue this and send it immediately later with the rest. + session.getQueuedImmediatelyPackets().add(moveEntityPacket); + + this.position = Vector3f.from(lerpXTotal, lerpYTotal, lerpZTotal); + this.lerpSteps--; + + vehicleComponent.moveAbsolute(lerpXTotal, lerpYTotal, lerpZTotal); + } + // Java sends simply "true" and "false" (is_paddling_left), Bedrock keeps sending packets as you're rowing if (session.getPlayerEntity().getVehicle() == this) { // For packet timing accuracy, we'll send the packets here, as that's what Java Edition 1.21.3 does. diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index cab98f498c1..82331ee8ba4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -490,6 +490,10 @@ public void tick() { this.position = Vector3f.from(lerpXTotal, lerpYTotal, lerpZTotal); this.lerpSteps--; + + if (this instanceof ClientVehicle vehicle) { + vehicle.getVehicleComponent().moveAbsolute(lerpXTotal, lerpYTotal, lerpZTotal); + } } } From 63f3112e5ac26625be02d405d5d3c286fa07eea8 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 11 Jan 2026 23:13:48 +0700 Subject: [PATCH 2/4] fixed some stuff. --- .../geyser/entity/type/BoatEntity.java | 36 +++++++++---------- .../org/geysermc/geyser/util/MathUtils.java | 4 +++ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index e68c6459d32..7d318992d92 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -41,6 +41,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPaddleBoatPacket; @@ -79,8 +80,8 @@ public class BoatEntity extends Entity implements Tickable, Leashable, ClientVeh private final float ROWING_SPEED = 0.04f; private Vector3f lerpPosition; + private float lerpYaw, lerpHeadYaw; private int lerpSteps; - protected boolean dirtyYaw, dirtyHeadYaw; public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, BoatVariant variant) { // Initial rotation is incorrect @@ -133,14 +134,11 @@ public void moveAbsoluteWithoutAdjustments(Vector3f position, float yaw, boolean @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { - if ((relX != 0 || relY != 0 || relZ != 0) && position.distanceSquared(session.getPlayerEntity().position()) < 4096) { - this.dirtyYaw = yaw != this.yaw; - this.dirtyHeadYaw = headYaw != this.headYaw; + this.lerpYaw = yaw + 90; + this.lerpHeadYaw = headYaw + 90; - setYaw(yaw); + if ((relX != 0 || relY != 0 || relZ != 0) && position.distanceSquared(session.getPlayerEntity().position()) < 4096) { setPitch(0); - setHeadYaw(headYaw); - this.lerpPosition = Vector3f.from(lerpPosition.getX() + relX, lerpPosition.getY() + relY, lerpPosition.getZ() + relZ); this.lerpSteps = 3; } else { @@ -150,16 +148,14 @@ public void moveRelative(double relX, double relY, double relZ, float yaw, float @Override public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { - setYaw(yaw); - setPitch(pitch); - setHeadYaw(headYaw); - this.lerpPosition = position; + this.lerpYaw = yaw + 90; + this.lerpHeadYaw = headYaw + 90; // It's vanilla behaviour to lerp if the position is within 64 blocks, however we also check if the position is close enough to the player // position to see if it can actually affect anything to save network. if (position.distanceSquared(this.position) < 4096 && position.distanceSquared(session.getPlayerEntity().position()) < 4096) { - this.dirtyYaw = this.dirtyHeadYaw = true; + setPitch(0); this.lerpSteps = 3; } else { super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); @@ -237,17 +233,19 @@ public void tick() { if (this.lerpSteps > 0) { float time = 1.0f / this.lerpSteps; float lerpXTotal = GenericMath.lerp(this.position.getX(), this.lerpPosition.getX(), time); - float lerpYTotal = GenericMath.lerp(this.position.getY(), this.lerpPosition.getY(), time); + float lerpYTotal = GenericMath.lerp(this.position.getY() - this.definition.offset(), this.lerpPosition.getY(), time) + this.definition.offset(); float lerpZTotal = GenericMath.lerp(this.position.getZ(), this.lerpPosition.getZ(), time); + float lerpYaw = MathUtils.rotLerp(this.yaw, this.lerpYaw, time); + float lerpHeadYaw = MathUtils.rotLerp(this.headYaw, this.lerpHeadYaw, time); MoveEntityDeltaPacket moveEntityPacket = new MoveEntityDeltaPacket(); moveEntityPacket.setRuntimeEntityId(geyserId); moveEntityPacket.setX(lerpXTotal); moveEntityPacket.setY(lerpYTotal); moveEntityPacket.setZ(lerpZTotal); - moveEntityPacket.setYaw(this.yaw); + moveEntityPacket.setYaw(lerpYaw); moveEntityPacket.setPitch(this.pitch); - moveEntityPacket.setHeadYaw(this.headYaw); + moveEntityPacket.setHeadYaw(lerpHeadYaw); if (onGround) { moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); } @@ -260,20 +258,20 @@ public void tick() { if (lerpZTotal != this.position.getZ()) { moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); } - if (this.dirtyYaw) { + if (lerpYaw != this.yaw) { moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); } - if (this.dirtyHeadYaw) { + if (lerpHeadYaw != this.headYaw) { moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); } moveEntityPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING); - this.dirtyYaw = this.dirtyHeadYaw = false; - // Queue this and send it immediately later with the rest. session.getQueuedImmediatelyPackets().add(moveEntityPacket); this.position = Vector3f.from(lerpXTotal, lerpYTotal, lerpZTotal); + this.yaw = lerpYaw; + this.headYaw = lerpHeadYaw; this.lerpSteps--; vehicleComponent.moveAbsolute(lerpXTotal, lerpYTotal, lerpZTotal); diff --git a/core/src/main/java/org/geysermc/geyser/util/MathUtils.java b/core/src/main/java/org/geysermc/geyser/util/MathUtils.java index 49a630325c6..3e5339b1e82 100644 --- a/core/src/main/java/org/geysermc/geyser/util/MathUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/MathUtils.java @@ -58,6 +58,10 @@ public static Vector3f calculateViewVector(float pitch, float yaw) { return Vector3f.from(var6 * var7, -var8, var5 * var7); } + public static float rotLerp(float var1, float var2, float var0) { + return var1 + var0 * wrapDegrees(var2 - var1); + } + /** * Wrap the given float degrees to be between -180.0 and 180.0. * From a0eaacce96ef367540f3c3360d52005598252529 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 11 Jan 2026 23:17:19 +0700 Subject: [PATCH 3/4] oops. --- .../java/org/geysermc/geyser/entity/type/MinecartEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index d0a5121c28e..1759aed50a2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -130,7 +130,7 @@ public void moveRelative(double relX, double relY, double relZ, float yaw, float @Override public void tick() { // This is based off Java OldMinecartBehavior class. - if (!session.isUsingExperimentalMinecartLogic()) { + if (!session.isUsingExperimentalMinecartLogic() && this.steps > 0) { float time = 1.0f / this.steps; float lerpXTotal = GenericMath.lerp(this.position.getX(), this.lerpPosition.getX(), time); float lerpYTotal = GenericMath.lerp(this.position.getY() - definition.offset(), this.lerpPosition.getY(), time) + definition.offset(); From c51c6ca049c0560b5d6df398c52e76fbe01e4ed0 Mon Sep 17 00:00:00 2001 From: oryxel1 Date: Sun, 11 Jan 2026 23:37:39 +0700 Subject: [PATCH 4/4] revert this. --- .../java/org/geysermc/geyser/entity/type/MinecartEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index 1759aed50a2..d0a5121c28e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -130,7 +130,7 @@ public void moveRelative(double relX, double relY, double relZ, float yaw, float @Override public void tick() { // This is based off Java OldMinecartBehavior class. - if (!session.isUsingExperimentalMinecartLogic() && this.steps > 0) { + if (!session.isUsingExperimentalMinecartLogic()) { float time = 1.0f / this.steps; float lerpXTotal = GenericMath.lerp(this.position.getX(), this.lerpPosition.getX(), time); float lerpYTotal = GenericMath.lerp(this.position.getY() - definition.offset(), this.lerpPosition.getY(), time) + definition.offset();