1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

Add support for server-controlled boat/horse riding on 1.21.130+ (#6033)

* Initial work, fixed boat on 1.21.130.

* Implement client-sided horse movement.

* Don't allow jumping in water.

* Fixed horse riding for older version, don't send movement in auth for boat.

* Removed some code.

* Oops, fixed jumping xD.

* Fixed boat paddling animation.

* Some boat fixes

Fix buoyancy
Handle relative move packets in Entity now; fixes vanilla movement
Slightly expand vehicle block cache to account for floating point error; fixes cache misses

* Address review.

---------

Co-authored-by: AJ Ferguson <AJ-Ferguson@users.noreply.github.com>
This commit is contained in:
oryxel
2025-12-13 03:39:58 +07:00
committed by GitHub
parent e4c11401db
commit 65fdb4c8a2
12 changed files with 674 additions and 55 deletions

View File

@@ -26,12 +26,16 @@
package org.geysermc.geyser.entity.type;
import lombok.Getter;
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.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.vehicle.BoatVehicleComponent;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
@@ -41,7 +45,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.Serve
import java.util.UUID;
public class BoatEntity extends Entity implements Leashable, Tickable {
public class BoatEntity extends Entity implements Tickable, Leashable, ClientVehicle {
/**
* Required when IS_BUOYANT is sent in order for boats to work in the water. <br>
@@ -53,6 +57,8 @@ public class BoatEntity extends Entity implements Leashable, Tickable {
"\"big_wave_speed\":10.0,\"drag_down_on_buoyancy_removed\":0.0,\"liquid_blocks\":[\"minecraft:water\"," +
"\"minecraft:flowing_water\"],\"simulate_waves\":false}";
private final BoatVehicleComponent vehicleComponent = new BoatVehicleComponent(this, 0);
private boolean isPaddlingLeft;
private float paddleTimeLeft;
private boolean isPaddlingRight;
@@ -67,8 +73,8 @@ public class BoatEntity extends Entity implements Leashable, Tickable {
private long leashHolderBedrockId = -1;
// Looks too fast and too choppy with 0.1f, which is how I believe the Microsoftian client handles it
private final float ROWING_SPEED = 0.1f;
// 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;
public BoatEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, BoatVariant variant) {
// Initial rotation is incorrect
@@ -192,8 +198,13 @@ public class BoatEntity extends Entity implements Leashable, Tickable {
// For packet timing accuracy, we'll send the packets here, as that's what Java Edition 1.21.3 does.
ServerboundPaddleBoatPacket steerPacket = new ServerboundPaddleBoatPacket(session.isSteeringLeft(), session.isSteeringRight());
session.sendDownstreamGamePacket(steerPacket);
// If the vehicle is not controlled by the client, then we will have to send the client the rowing time else the animation won't play!
if (session.isInClientPredictedVehicle()) {
return;
}
}
doTick = !doTick; // Run every other tick
if (!doTick || passengers.isEmpty()) {
return;
@@ -212,11 +223,35 @@ public class BoatEntity extends Entity implements Leashable, Tickable {
paddleTimeRight += ROWING_SPEED;
dirtyMetadata.put(EntityDataTypes.ROW_TIME_RIGHT, paddleTimeRight);
}
if (isPaddlingLeft || isPaddlingRight) {
updateBedrockMetadata();
}
}
@Override
public long leashHolderBedrockId() {
return leashHolderBedrockId;
return this.leashHolderBedrockId;
}
@Override
public VehicleComponent<?> getVehicleComponent() {
return this.vehicleComponent;
}
@Override
public Vector3f getRiddenInput(Vector2f input) {
return Vector3f.ZERO;
}
@Override
public float getVehicleSpeed() {
return 0;
}
@Override
public boolean isClientControlled() {
return !session.isInClientPredictedVehicle() && !passengers.isEmpty() && this.session.getPlayerEntity() == passengers.get(0);
}
/**

View File

@@ -53,6 +53,7 @@ import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
import org.geysermc.geyser.entity.properties.type.PropertyType;
import org.geysermc.geyser.entity.type.living.MobEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.level.physics.BoundingBox;
@@ -258,6 +259,13 @@ public class Entity implements GeyserEntity {
}
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
if (this instanceof ClientVehicle clientVehicle) {
if (clientVehicle.isClientControlled()) {
return;
}
clientVehicle.getVehicleComponent().moveRelative(relX, relY, relZ);
}
position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
MoveEntityDeltaPacket moveEntityPacket = new MoveEntityDeltaPacket();

View File

@@ -356,18 +356,6 @@ public class LivingEntity extends Entity {
return super.interact(hand);
}
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
if (this instanceof ClientVehicle clientVehicle) {
if (clientVehicle.isClientControlled()) {
return;
}
clientVehicle.getVehicleComponent().moveRelative(relX, relY, relZ);
}
super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
}
@Override
public boolean setBoundingBoxHeight(float height) {
if (valid && this instanceof ClientVehicle clientVehicle) {

View File

@@ -27,7 +27,9 @@ package org.geysermc.geyser.entity.type.living.animal.horse;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
@@ -37,6 +39,9 @@ import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.living.animal.AnimalEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.HorseVehicleComponent;
import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.input.InputLocksFlag;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
@@ -47,12 +52,16 @@ import org.geysermc.geyser.session.cache.tags.Tag;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import java.util.UUID;
public class AbstractHorseEntity extends AnimalEntity {
public class AbstractHorseEntity extends AnimalEntity implements ClientVehicle {
private final HorseVehicleComponent vehicleComponent = new HorseVehicleComponent(this);
public AbstractHorseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@@ -94,6 +103,15 @@ public class AbstractHorseEntity extends AnimalEntity {
}
}
@Override
protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttributeType type) {
AttributeData attributeData = super.calculateAttribute(javaAttribute, type);
if (javaAttribute.getType() == AttributeType.Builtin.JUMP_STRENGTH) {
vehicleComponent.setHorseJumpStrength(attributeData.getValue());
}
return attributeData;
}
@Override
public boolean doesJumpDismount() {
return !this.getFlag(EntityFlag.SADDLED);
@@ -309,4 +327,25 @@ public class AbstractHorseEntity extends AnimalEntity {
return isAlive() && !isBaby() && getFlag(EntityFlag.TAMED);
}
}
@Override
public VehicleComponent<?> getVehicleComponent() {
return this.vehicleComponent;
}
@Override
public Vector3f getRiddenInput(Vector2f input) {
input = input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f);
return Vector3f.from(input.getX(), 0.0, input.getY());
}
@Override
public float getVehicleSpeed() {
return vehicleComponent.getMoveSpeed();
}
@Override
public boolean isClientControlled() {
return getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) == session.getPlayerEntity() && !session.isInClientPredictedVehicle();
}
}

View File

@@ -25,7 +25,6 @@
package org.geysermc.geyser.entity.type.living.animal.horse;
import org.cloudburstmc.math.vector.Vector2f;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
@@ -152,12 +151,6 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle {
return vehicleComponent;
}
@Override
public Vector3f getRiddenInput(Vector2f input) {
input = input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f);
return Vector3f.from(input.getX(), 0.0, input.getY());
}
@Override
public boolean isClientControlled() {
return getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) == session.getPlayerEntity();

View File

@@ -0,0 +1,433 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.entity.vehicle;
import org.cloudburstmc.math.GenericMath;
import org.cloudburstmc.math.TrigMath;
import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3d;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.level.block.Fluid;
import org.geysermc.geyser.level.block.type.BedBlock;
import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.physics.Axis;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket;
public class BoatVehicleComponent extends VehicleComponent<BoatEntity> {
private Status status, oldStatus;
private double waterLevel;
private float landFriction;
private double lastYd;
private float deltaRotation;
public BoatVehicleComponent(BoatEntity vehicle, float stepHeight) {
super(vehicle, stepHeight);
this.gravity = 0.04;
}
@Override
public void tickVehicle() {
if (!vehicle.isClientControlled()) {
return;
}
final VehicleContext context = new VehicleContext();
context.loadSurroundingBlocks();
this.oldStatus = this.status;
this.status = getStatus(context);
floatBoat(context);
final Vector3f lastRotation = vehicle.getBedrockRotation();
controlBoat();
Vector3f motion = vehicle.getMotion();
Vector3f movementMultiplier = getBlockMovementMultiplier(context);
if (movementMultiplier != null) {
motion = motion.mul(movementMultiplier);
}
Vector3d correctedMovement = vehicle.getSession().getWorldBorder().correctMovement(boundingBox, motion.toDouble());
correctedMovement = vehicle.getSession().getCollisionManager().correctMovement(
correctedMovement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava()
);
// Non-zero values indicate a collision on that axis
Vector3d moveDiff = motion.toDouble().sub(correctedMovement);
boundingBox.translate(correctedMovement);
context.loadSurroundingBlocks(); // Context must be reloaded after vehicle is moved
boolean verticalCollision = moveDiff.getY() != 0;
vehicle.setOnGround(verticalCollision && motion.getY() < 0);
boolean bounced = false;
if (vehicle.isOnGround()) {
Block landingBlock = getLandingBlock(context).block();
if (landingBlock == Blocks.SLIME_BLOCK) {
motion = Vector3f.from(motion.getX(), -motion.getY(), motion.getZ());
bounced = true;
// Slow horizontal movement
float absY = Math.abs(motion.getY());
if (absY < 0.1f) {
float mul = 0.4f + absY * 0.2f;
motion = motion.mul(mul, 1.0f, mul);
}
} else if (landingBlock instanceof BedBlock) {
motion = Vector3f.from(motion.getX(), -motion.getY() * 0.66f, motion.getZ());
bounced = true;
}
}
// Set motion to 0 if a movement multiplier was used, else set to 0 on each axis with a collision
if (movementMultiplier != null) {
motion = Vector3f.ZERO;
} else {
motion = motion.mul(
moveDiff.getX() == 0 ? 1 : 0,
!verticalCollision || bounced ? 1 : 0,
moveDiff.getZ() == 0 ? 1 : 0
);
}
// Send the new position to the bedrock client and java server
moveVehicle(context.centerPos(), lastRotation);
vehicle.setMotion(motion);
// This got ran twice in Boat entity for certain reason?
applyBlockCollisionEffects(context);
applyBlockCollisionEffects(context);
}
@Override
protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) {
Vector3f bedrockPos = javaPos.toFloat();
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId());
if (vehicle.isOnGround()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
}
if (vehicle.getPosition().getX() != bedrockPos.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(bedrockPos.getX());
}
if (vehicle.getPosition().getY() != bedrockPos.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(bedrockPos.getY() + vehicle.getDefinition().offset());
}
if (vehicle.getPosition().getZ() != bedrockPos.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(bedrockPos.getZ());
}
vehicle.setPosition(bedrockPos);
if (vehicle.getPitch() != lastRotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
moveEntityDeltaPacket.setPitch(vehicle.getPitch());
}
if (vehicle.getYaw() != lastRotation.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW);
moveEntityDeltaPacket.setYaw(vehicle.getYaw());
}
if (vehicle.getHeadYaw() != lastRotation.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW);
moveEntityDeltaPacket.setHeadYaw(vehicle.getHeadYaw());
}
if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket);
}
ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw() - 90, vehicle.getPitch(), vehicle.isOnGround());
vehicle.getSession().sendDownstreamPacket(moveVehiclePacket);
}
private void controlBoat() {
float acceleration = 0.0F;
final Vector2f input = vehicle.getSession().getPlayerEntity().getVehicleInput();
boolean up = input.getY() > 0;
boolean down = input.getY() < 0;
boolean left = input.getX() > 0;
boolean right = input.getX() < 0;
if (left) this.deltaRotation--;
if (right) this.deltaRotation++;
if (right != left && !up && !down) acceleration += 0.005F;
vehicle.setYaw(vehicle.getYaw() + this.deltaRotation);
vehicle.setHeadYaw(vehicle.getYaw());
if (up) acceleration += 0.04F;
if (down) acceleration -= 0.005F;
float yaw = vehicle.getYaw() - 90;
final Vector3f motion = Vector3f.from(TrigMath.sin((-yaw * 0.017453292F)) * acceleration, 0, TrigMath.cos((yaw * 0.017453292F)) * acceleration);
vehicle.setMotion(vehicle.getMotion().add(motion));
vehicle.getSession().setSteeringLeft(right && !left || up);
vehicle.getSession().setSteeringRight(left && !right || up);
}
private void floatBoat(final VehicleContext context) {
double gravity = -getGravity();
double buoyancy = 0.0D;
float frictionMutiplier = 0.05F;
if (this.oldStatus == Status.IN_AIR && this.status != Status.IN_AIR && this.status != Status.ON_LAND) {
this.waterLevel = getBoundingBox().getMax(Axis.Y);
double targetY = (getWaterLevelAbove(context) - this.vehicle.getBoundingBoxHeight()) + 0.101D;
BoundingBox box = boundingBox.clone();
box.translate(0, targetY - getBoundingBox().getMin(Axis.Y), 0);
boolean empty = true;
for (BlockPositionIterator iter = vehicle.getSession().getCollisionManager().collidableBlocksIterator(box); iter.hasNext(); iter.next()) {
final BlockCollision collision = BlockUtils.getCollision(context.getBlockId(iter.getX(), iter.getY(), iter.getZ()));
if (collision != null && collision.checkIntersection(Vector3i.from(iter.getX(), iter.getY(), iter.getZ()), box)) {
empty = false;
break;
}
}
if (empty) {
vehicle.setMotion(vehicle.getMotion().mul(1, 0, 1));
boundingBox.setMiddleY(targetY + (boundingBox.getSizeY() / 2));
this.lastYd = 0;
}
this.status = Status.IN_WATER;
} else {
if (this.status == Status.IN_WATER) {
buoyancy = (this.waterLevel - getBoundingBox().getMin(Axis.Y)) / vehicle.getBoundingBoxHeight();
frictionMutiplier = 0.9F;
} else if (this.status == Status.UNDER_FLOWING_WATER) {
gravity = -7.0E-4D;
frictionMutiplier = 0.9F;
} else if (this.status == Status.UNDER_WATER) {
buoyancy = 0.009999999776482582D;
frictionMutiplier = 0.45F;
} else if (this.status == Status.IN_AIR) {
frictionMutiplier = 0.9F;
} else if (this.status == Status.ON_LAND) {
frictionMutiplier = this.landFriction;
this.landFriction /= 2.0F;
}
vehicle.setMotion(vehicle.getMotion().mul(frictionMutiplier, 1, frictionMutiplier).up((float) gravity));
this.deltaRotation *= frictionMutiplier;
if (buoyancy > 0.0D) {
Vector3f motion = vehicle.getMotion();
vehicle.setMotion(Vector3f.from(
motion.getX(),
(float) (motion.getY() + buoyancy * (this.gravity / 0.65)) * 0.75f,
motion.getZ()
));
}
}
}
private float getWaterLevelAbove(final VehicleContext context) {
BoundingBox aabb = getBoundingBox();
int minX = GenericMath.floor(aabb.getMin(Axis.X));
int maxX = GenericMath.ceil(aabb.getMax(Axis.X));
int minY = GenericMath.floor(aabb.getMax(Axis.Y));
int maxY = GenericMath.ceil(aabb.getMax(Axis.Y) - this.lastYd);
int minZ = GenericMath.floor(aabb.getMin(Axis.Z));
int maxZ = GenericMath.ceil(aabb.getMax(Axis.Z));
for (int y = minY; y < maxY; y++) {
float blockHeight = 0.0F;
for (int x = minX; x < maxX; x++) {
for (int z = minZ; z < maxZ; z++) {
final float fluidHeight = getLogicalFluidHeight(Fluid.WATER, context.getBlockId(x, y, z));
if (fluidHeight > 0) {
blockHeight = Math.max(blockHeight, fluidHeight);
}
}
}
if (blockHeight < 1.0F) {
return y + blockHeight;
}
}
return (maxY + 1);
}
private float getGroundFriction(final VehicleContext context) {
BoundingBox boatShape = getBoundingBox().clone();
// 0.001 high box extending downwards from the boat
boatShape.setMiddleY(boatShape.getMin(Axis.Y) - 0.0005);
boatShape.setSizeY(0.001);
if (boatShape.isEmpty()) {
return Float.NaN;
}
int x0 = GenericMath.floor(boatShape.getMin(Axis.X)) - 1;
int x1 = GenericMath.ceil(boatShape.getMax(Axis.X)) + 1;
int y0 = GenericMath.floor(boatShape.getMin(Axis.Y)) - 1;
int y1 = GenericMath.ceil(boatShape.getMax(Axis.Y)) + 1;
int z0 = GenericMath.floor(boatShape.getMin(Axis.Z)) - 1;
int z1 = GenericMath.ceil(boatShape.getMax(Axis.Z)) + 1;
float friction = 0.0F;
int count = 0;
for (int x = x0; x < x1; x++) {
for (int z = z0; z < z1; z++) {
int edges = ((x == x0 || x == x1 - 1) ? 1 : 0) + ((z == z0 || z == z1 - 1) ? 1 : 0);
if (edges == 2) {
continue;
}
for (int y = y0; y < y1; y++) {
if (edges > 0 && !(y != y0 && y != y1 - 1)) {
continue;
}
final BlockState state = context.getBlock(x, y, z);
if (state.is(Blocks.LILY_PAD)) {
continue;
}
final BlockCollision collision = BlockUtils.getCollision(state.javaId());
if (collision == null || collision.getBoundingBoxes().length == 0) {
continue;
}
if (collision.checkIntersection(Vector3i.from(x, y, z), boatShape)) {
friction += BlockStateValues.getSlipperiness(state);
count++;
}
}
}
}
return friction / count;
}
private Status isUnderwater(final VehicleContext context) {
BoundingBox boatShape = getBoundingBox().clone();
double maxY = boatShape.getMax(Axis.Y) + 0.001D;
int x0 = GenericMath.floor(boatShape.getMin(Axis.X));
int x1 = GenericMath.ceil(boatShape.getMax(Axis.X));
int y0 = GenericMath.floor(boatShape.getMax(Axis.Y));
int y1 = GenericMath.ceil(maxY);
int z0 = GenericMath.floor(boatShape.getMin(Axis.Z));
int z1 = GenericMath.ceil(boatShape.getMax(Axis.Z));
boolean underWater = false;
for (int x = x0; x < x1; x++) {
for (int y = y0; y < y1; y++) {
for (int z = z0; z < z1; z++) {
final float fluidHeight = getLogicalFluidHeight(Fluid.WATER, context.getBlockId(x, y, z));
if (fluidHeight <= 0 || maxY > y + fluidHeight) {
continue;
}
if (fluidHeight == 1) {
underWater = true;
} else {
return Status.UNDER_FLOWING_WATER;
}
}
}
}
return underWater ? Status.UNDER_WATER : null;
}
private boolean checkInWater(final VehicleContext context) {
this.waterLevel = Double.MIN_VALUE;
final BoundingBox boatShape = getBoundingBox();
int minX = GenericMath.floor(boatShape.getMin(Axis.X));
int maxX = GenericMath.ceil(boatShape.getMax(Axis.X));
int minY = GenericMath.floor(boatShape.getMin(Axis.Y));
int maxY = GenericMath.ceil(boatShape.getMin(Axis.Y) + 0.001D);
int minZ = GenericMath.floor(boatShape.getMin(Axis.Z));
int maxZ = GenericMath.ceil(boatShape.getMax(Axis.Z));
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
final float fluidHeight = getLogicalFluidHeight(Fluid.WATER, context.getBlockId(x, y, z));
if (fluidHeight <= 0) {
continue;
}
float height = y + fluidHeight;
this.waterLevel = Math.max(height, this.waterLevel);
if (boatShape.getMin(Axis.Y) < height) {
return true;
}
}
}
}
return false;
}
private Status getStatus(final VehicleContext context) {
Status waterStatus = isUnderwater(context);
if (waterStatus != null) {
this.waterLevel = getBoundingBox().getMax(Axis.Y);
return waterStatus;
}
if (checkInWater(context)) {
return Status.IN_WATER;
}
float friction = getGroundFriction(context);
if (friction > 0.0F) {
this.landFriction = friction;
return Status.ON_LAND;
}
return Status.IN_AIR;
}
public enum Status {
IN_WATER, UNDER_WATER, UNDER_FLOWING_WATER, ON_LAND, IN_AIR;
}
}

View File

@@ -98,10 +98,9 @@ public class CamelVehicleComponent extends VehicleComponent<CamelEntity> {
SessionPlayerEntity player = vehicle.getSession().getPlayerEntity();
Vector3f inputVelocity = super.getInputVector(ctx, speed, input);
float jumpStrength = player.getVehicleJumpStrength();
if (jumpStrength > 0) {
player.setVehicleJumpStrength(0);
if (vehicle.isOnGround() && jumpStrength > 0) {
if (jumpStrength >= 90) {
jumpStrength = 1.0f;
} else {

View File

@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.entity.vehicle;
import lombok.Setter;
import org.cloudburstmc.math.TrigMath;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.SkeletonHorseEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
public class HorseVehicleComponent extends VehicleComponent<AbstractHorseEntity> {
@Setter
private float horseJumpStrength = 0.7f; // Not sent by vanilla Java server when spawned
private int effectJumpBoost;
@Setter
private boolean allowStandSliding;
public HorseVehicleComponent(AbstractHorseEntity vehicle) {
super(vehicle, 1.5f);
}
@Override
public void tickVehicle() {
if (!vehicle.getFlag(EntityFlag.STANDING)) {
this.allowStandSliding = false;
}
super.tickVehicle();
}
@Override
protected Vector3f getInputVector(VehicleComponent<AbstractHorseEntity>.VehicleContext ctx, float speed, Vector3f input) {
SessionPlayerEntity player = vehicle.getSession().getPlayerEntity();
float jumpLeapStrength = player.getVehicleJumpStrength();
if (vehicle.isOnGround() && jumpLeapStrength == 0.0F && vehicle.getFlag(EntityFlag.STANDING) && !this.allowStandSliding) {
return Vector3f.ZERO;
}
player.setVehicleJumpStrength(0);
Vector3f inputVelocity = super.getInputVector(ctx, speed, input);
if (vehicle.isOnGround() && jumpLeapStrength > 0) {
if (jumpLeapStrength >= 90) {
jumpLeapStrength = 1.0f;
} else {
jumpLeapStrength = 0.4f + 0.4f * jumpLeapStrength / 90.0f;
}
float jumpStrength = this.horseJumpStrength * getJumpVelocityMultiplier(ctx) + (this.effectJumpBoost * 0.1f);
inputVelocity = Vector3f.from(inputVelocity.getX(), jumpStrength, inputVelocity.getZ());
if (input.getZ() > 0.0) {
inputVelocity = inputVelocity.add(-0.4F * TrigMath.sin(vehicle.getYaw() * 0.017453292F) * jumpLeapStrength, 0.0, 0.4F * TrigMath.cos(vehicle.getYaw() * 0.017453292F) * jumpLeapStrength);
}
}
return inputVelocity;
}
@Override
protected float getWaterSlowDown() {
return vehicle instanceof SkeletonHorseEntity ? 0.96f : super.getWaterSlowDown();
}
@Override
public void setEffect(Effect effect, int effectAmplifier) {
if (effect == Effect.JUMP_BOOST) {
effectJumpBoost = effectAmplifier + 1;
} else {
super.setEffect(effect, effectAmplifier);
}
}
@Override
public void removeEffect(Effect effect) {
if (effect == Effect.JUMP_BOOST) {
effectJumpBoost = 0;
} else {
super.removeEffect(effect);
}
}
}

View File

@@ -37,6 +37,7 @@ import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.geysermc.erosion.util.BlockPositionIterator;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.block.Blocks;
@@ -59,7 +60,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeT
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket;
public class VehicleComponent<T extends LivingEntity & ClientVehicle> {
public class VehicleComponent<T extends Entity & ClientVehicle> {
private static final ObjectDoublePair<Fluid> EMPTY_FLUID_PAIR = ObjectDoublePair.of(Fluid.EMPTY, 0.0);
private static final float MAX_LOGICAL_FLUID_HEIGHT = 8.0f / BlockStateValues.NUM_FLUID_LEVELS;
private static final float BASE_SLIPPERINESS_CUBED = 0.6f * 0.6f * 0.6f;
@@ -383,7 +384,7 @@ public class VehicleComponent<T extends LivingEntity & ClientVehicle> {
// Mojmap: LivingEntity#travelInFluid
protected void waterMovement(VehicleContext ctx) {
double gravity = getGravity();
float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier
float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : getWaterSlowDown();
double originalY = ctx.centerPos().getY();
boolean falling = vehicle.getMotion().getY() <= 0;
@@ -418,6 +419,10 @@ public class VehicleComponent<T extends LivingEntity & ClientVehicle> {
}
}
protected float getWaterSlowDown() {
return 0.8f;
}
protected void lavaMovement(VehicleContext ctx, double lavaHeight) {
double gravity = getGravity();
double originalY = ctx.centerPos().getY();
@@ -917,7 +922,7 @@ public class VehicleComponent<T extends LivingEntity & ClientVehicle> {
// Reuse block cache if vehicle moved less than 1 block
if (this.cachePos == null || this.cachePos.distanceSquared(this.centerPos) > 1) {
BoundingBox box = boundingBox.clone();
box.expand(2);
box.expand(2.0001);
Vector3i min = box.getMin().toInt();
Vector3i max = box.getMax().toInt();

View File

@@ -194,6 +194,10 @@ public class BoundingBox implements Cloneable {
};
}
public boolean isEmpty() {
return getMax(Axis.X) - getMin(Axis.X) < 1.0E-7D || getMax(Axis.Y) - getMin(Axis.Y) < 1.0E-7D || getMax(Axis.Z) - getMin(Axis.Z) < 1.0E-7D;
}
@SneakyThrows(CloneNotSupportedException.class)
@Override
public BoundingBox clone() {

View File

@@ -577,6 +577,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private boolean steeringRight;
/**
* Stores whether the player is inside a client predicted vehicle.
*/
@Getter
@Setter
private boolean inClientPredictedVehicle;
/**
* Store the last time the player interacted. Used to fix a right-click spam bug.
* See <a href="https://github.com/GeyserMC/Geyser/issues/503">this</a> for context.

View File

@@ -41,6 +41,7 @@ import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.LlamaEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.entity.vehicle.HorseVehicleComponent;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
@@ -68,6 +69,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
SessionPlayerEntity entity = session.getPlayerEntity();
session.setClientTicks(packet.getTick());
session.setInClientPredictedVehicle(packet.getInputData().contains(PlayerAuthInputData.IN_CLIENT_PREDICTED_IN_VEHICLE) && entity.getVehicle() != null);
boolean wasJumping = session.getInputCache().wasJumping();
session.getInputCache().processInputs(entity, packet);
@@ -211,7 +213,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
}
// Only set steering values when the vehicle is a boat and when the client is actually in it
if (entity.getVehicle() instanceof BoatEntity && inputData.contains(PlayerAuthInputData.IN_CLIENT_PREDICTED_IN_VEHICLE)) {
if (entity.getVehicle() instanceof BoatEntity && session.isInClientPredictedVehicle()) {
boolean up = inputData.contains(PlayerAuthInputData.UP);
// Yes. These are flipped. Welcome to Bedrock edition.
// Hi random stranger. I am six days into updating for 1.21.3. How's it going?
@@ -237,19 +239,22 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
if (vehicle == null) {
return;
}
// TODO: Should we also check for protocol version here? If yes then this should be test on multiple platform first.
boolean inClientPredictedVehicle = packet.getInputData().contains(PlayerAuthInputData.IN_CLIENT_PREDICTED_IN_VEHICLE);
if (vehicle instanceof ClientVehicle) {
session.getPlayerEntity().setVehicleInput(packet.getMotion());
}
boolean sendMovement = false;
if (vehicle instanceof AbstractHorseEntity && !(vehicle instanceof LlamaEntity)) {
sendMovement = !(vehicle instanceof ClientVehicle);
sendMovement = inClientPredictedVehicle;
} else if (vehicle instanceof BoatEntity) {
// The player is either the only or the front rider.
sendMovement = vehicle.getPassengers().size() == 1 || session.getPlayerEntity().isRidingInFront();
sendMovement = inClientPredictedVehicle && (vehicle.getPassengers().size() == 1 || session.getPlayerEntity().isRidingInFront());
}
if (vehicle instanceof AbstractHorseEntity && !vehicle.getFlag(EntityFlag.HAS_DASH_COOLDOWN)) {
if (vehicle instanceof AbstractHorseEntity horse && !vehicle.getFlag(EntityFlag.HAS_DASH_COOLDOWN)) {
// Behavior verified as of Java Edition 1.21.3
int currentJumpingTicks = session.getInputCache().getJumpingTicks();
if (currentJumpingTicks < 0) {
@@ -268,6 +273,10 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
PlayerState.START_HORSE_JUMP, finalVehicleJumpStrength));
session.getInputCache().setJumpingTicks(-10);
session.getPlayerEntity().setVehicleJumpStrength(finalVehicleJumpStrength);
if (horse.getVehicleComponent() instanceof HorseVehicleComponent horseVehicleComponent) {
horseVehicleComponent.setAllowStandSliding(true);
}
} else if (!wasJumping && holdingJump) {
session.getInputCache().setJumpingTicks(0);
session.getInputCache().setJumpScale(0);
@@ -286,33 +295,24 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
if (sendMovement) {
// We only need to determine onGround status this way for client predicted vehicles.
// For other vehicle, Geyser already handle it in VehicleComponent or the Java server handle it.
if (packet.getInputData().contains(PlayerAuthInputData.IN_CLIENT_PREDICTED_IN_VEHICLE)) {
Vector3f position = vehicle.getPosition();
if (vehicle instanceof BoatEntity) {
position = position.down(vehicle.getDefinition().offset());
}
final BoundingBox box = new BoundingBox(
position.up(vehicle.getBoundingBoxHeight() / 2f).toDouble(),
position.down(vehicle instanceof BoatEntity ? vehicle.getDefinition().offset() : 0).up(vehicle.getBoundingBoxHeight() / 2f).toDouble(),
vehicle.getBoundingBoxWidth(), vehicle.getBoundingBoxHeight(), vehicle.getBoundingBoxWidth()
);
// Manually calculate the vertical collision ourselves, the VERTICAL_COLLISION input data is inaccurate inside a vehicle!
Vector3d movement = session.getPlayerEntity().getLastTickEndVelocity().toDouble();
Vector3d correctedMovement = session.getCollisionManager().correctMovementForCollisions(movement, box, true, false);
vehicle.setOnGround(correctedMovement.getY() != movement.getY() && session.getPlayerEntity().getLastTickEndVelocity().getY() < 0);
}
Vector3f vehiclePosition = packet.getPosition();
Vector2f vehicleRotation = packet.getVehicleRotation();
if (vehicleRotation == null) {
return; // If the client just got in or out of a vehicle for example. Or if this vehicle isn't client predicted.
return; // If the client just got in or out of a vehicle for example.
}
if (session.getWorldBorder().isPassingIntoBorderBoundaries(vehiclePosition, false)) {
Vector3f position = vehicle.getPosition();
if (vehicle instanceof BoatEntity boat) {
// Undo the changes usually applied to the boat
boat.moveAbsoluteWithoutAdjustments(position, vehicle.getYaw(), vehicle.isOnGround(), true);
@@ -333,7 +333,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator<Pla
vehicle.setPosition(vehiclePosition);
ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(
vehiclePosition.toDouble(),
vehicleRotation.getY() - 90, vehiclePosition.getX(), // TODO I wonder if this is related to the horse spinning bugs...
vehicle instanceof BoatEntity ? vehicleRotation.getY() - 90 : vehicleRotation.getY(), vehiclePosition.getX(),
vehicle.isOnGround()
);
session.sendDownstreamGamePacket(moveVehiclePacket);