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 9652f481a..a11819960 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,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.
@@ -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); - return; + + // 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); } /** diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index f82b09fe2..b992534cd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -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(); 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 1d0c22800..10207bbd4 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 @@ -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) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index 35e3d17ae..0675bd5f4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -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(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java index 138b60e35..870d7b09a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java @@ -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(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoatVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoatVehicleComponent.java new file mode 100644 index 000000000..3a9b3d9ab --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoatVehicleComponent.java @@ -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 { + 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; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java index 1ed9328f0..77bda1435 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java @@ -98,10 +98,9 @@ public class CamelVehicleComponent extends VehicleComponent { SessionPlayerEntity player = vehicle.getSession().getPlayerEntity(); Vector3f inputVelocity = super.getInputVector(ctx, speed, input); float jumpStrength = player.getVehicleJumpStrength(); + player.setVehicleJumpStrength(0); - if (jumpStrength > 0) { - player.setVehicleJumpStrength(0); - + if (vehicle.isOnGround() && jumpStrength > 0) { if (jumpStrength >= 90) { jumpStrength = 1.0f; } else { diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/HorseVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HorseVehicleComponent.java new file mode 100644 index 000000000..620cee9e7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HorseVehicleComponent.java @@ -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 { + @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.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); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java index e83039837..90b96cb17 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java @@ -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 { +public class VehicleComponent { private static final ObjectDoublePair 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; @@ -391,7 +392,7 @@ public class VehicleComponent { // 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; @@ -426,6 +427,10 @@ public class VehicleComponent { } } + protected float getWaterSlowDown() { + return 0.8f; + } + protected void lavaMovement(VehicleContext ctx, double lavaHeight) { double gravity = getGravity(); double originalY = ctx.centerPos().getY(); @@ -948,7 +953,7 @@ public class VehicleComponent { // 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(); diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java b/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java index 77aec1336..ee4d8a0f2 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java @@ -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() { diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index c38784708..2108a4aa6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -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 this for context. diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java index 2248e3be2..923d4613d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java @@ -43,6 +43,7 @@ import org.geysermc.geyser.entity.type.living.animal.nautilus.AbstractNautilusEn import org.geysermc.geyser.entity.type.living.animal.nautilus.NautilusEntity; 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; @@ -70,6 +71,7 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator