diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java index 16587d125..414ed0541 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java @@ -66,7 +66,7 @@ public class DisplayBaseEntity extends Entity { this.setRiderSeatPosition(this.baseTranslation); this.moveRelative(this.baseTranslation.getX(), this.baseTranslation.getY(), this.baseTranslation.getZ(), yaw, pitch, headYaw, false); } else { - EntityUtils.updateMountOffset(this, this.vehicle, true, true, false); + EntityUtils.updateMountOffset(this, this.vehicle, true, true, 0, 1); this.updateBedrockMetadata(); } } 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 fa1f05417..b03dea3ed 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 @@ -621,7 +621,7 @@ public class Entity implements GeyserEntity { Entity passenger = passengers.get(i); if (passenger != null) { boolean rider = i == 0; - EntityUtils.updateMountOffset(passenger, this, rider, true, passengers.size() > 1); + EntityUtils.updateMountOffset(passenger, this, rider, true, i, passengers.size()); passenger.updateBedrockMetadata(); } } @@ -633,7 +633,7 @@ public class Entity implements GeyserEntity { protected void updateMountOffset() { if (vehicle != null) { boolean rider = vehicle.getPassengers().get(0) == this; - EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().size() > 1); + EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().indexOf(this), vehicle.getPassengers().size()); updateBedrockMetadata(); } } 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 363ce6008..92048eb25 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 @@ -41,7 +41,9 @@ import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; 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.HappyGhastEntity; import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.registry.type.ItemMapping; @@ -49,6 +51,7 @@ import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; +import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; @@ -510,7 +513,13 @@ public class LivingEntity extends Entity { } } case ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE)); - case FLYING_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED)); + case FLYING_SPEED -> { + AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED); + newAttributes.add(attributeData); + if (this instanceof HappyGhastEntity ghast && ghast.getVehicleComponent() instanceof HappyGhastVehicleComponent component) { + component.setFlyingSpeed(attributeData.getValue()); + } + } case FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE)); case KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE)); case JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH)); @@ -519,10 +528,41 @@ public class LivingEntity extends Entity { setAttributeScale((float) AttributeUtils.calculateValue(javaAttribute)); updateBedrockMetadata(); } + case WATER_MOVEMENT_EFFICIENCY -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setWaterMovementEfficiency(AttributeUtils.calculateValue(javaAttribute)); + } + } } } } + protected boolean hasBodyArmor() { + return this.hasValidEquippableItemForSlot(EquipmentSlot.BODY); + } + + private boolean hasValidEquippableItemForSlot(EquipmentSlot slot) { + // MojMap LivingEntity#hasItemInSlot + GeyserItemStack itemInSlot = equipment.get(slot); + if (itemInSlot != null) { + // MojMap LivingEntity#isEquippableInSlot + Equippable equippable = itemInSlot.getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null) { + return slot == equippable.slot() && + canUseSlot(slot) && + EntityUtils.equipmentUsableByEntity(session, equippable, this.definition.entityType()); + } else { + return slot == EquipmentSlot.MAIN_HAND && canUseSlot(EquipmentSlot.MAIN_HAND); + } + } + + return false; + } + + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } + /** * Calculates the complete attribute value to send to Bedrock. Will be overriden if attributes need to be cached. */ diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java index 38d6f1eb4..e8cb59e21 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java @@ -27,36 +27,60 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.TrigMath; +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.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.AttributeUtils; 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.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import java.util.List; import java.util.UUID; -public class HappyGhastEntity extends AnimalEntity { +public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { + + public static final float[] X_OFFSETS = {0.0F, -1.7F, 0.0F, 1.7F}; + public static final float[] Z_OFFSETS = {1.7F, 0.0F, -1.7F, 0.0F}; + + private final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f); + private boolean staysStill; + public HappyGhastEntity(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); + } + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // BDS 1.21.90 setFlag(EntityFlag.CAN_FLY, true); - setFlag(EntityFlag.TAMED, true); setFlag(EntityFlag.CAN_WALK, true); + setFlag(EntityFlag.TAMED, true); + setFlag(EntityFlag.BODY_ROTATION_ALWAYS_FOLLOWS_HEAD, true); + setFlag(EntityFlag.COLLIDABLE, true); setFlag(EntityFlag.WASD_AIR_CONTROLLED, true); setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true); - // TODO: verify which flags are necessary - - setAirSupply(100); + propertyManager.add("minecraft:can_move", true); } @Override @@ -71,6 +95,7 @@ public class HappyGhastEntity extends AnimalEntity { } public void setStaysStill(BooleanEntityMetadata entityMetadata) { + staysStill = entityMetadata.getPrimitiveValue(); propertyManager.add("minecraft:can_move", !entityMetadata.getPrimitiveValue()); updateBedrockEntityProperties(); } @@ -124,4 +149,65 @@ public class HappyGhastEntity extends AnimalEntity { } } } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + float x = input.getX(); + float y = 0.0f; + float z = 0.0f; + + if (input.getY() != 0.0f) { + float pitch = session.getPlayerEntity().getPitch(); + z = TrigMath.cos(pitch * TrigMath.DEG_TO_RAD); + y = -TrigMath.sin(pitch * TrigMath.DEG_TO_RAD); + if (input.getY() < 0.0f) { + z *= -0.5f; + y *= -0.5f; + } + } + + if (session.getInputCache().wasJumping()) { + y += 0.5f; + } + + return Vector3f.from(x, y, z).mul(3.9f * vehicleComponent.getFlyingSpeed()); + } + + @Override + public float getVehicleSpeed() { + return 1; // TODO this doesnt seem right? + } + + @Override + public boolean isClientControlled() { + if (!hasBodyArmor() || getFlag(EntityFlag.NO_AI) || staysStill) { + return false; + } + + return getFirstPassenger() instanceof SessionPlayerEntity; + } + + private Entity getFirstPassenger() { + return passengers.isEmpty() ? null : passengers.get(0); + } + + @Override + protected void updateAttribute(Attribute javaAttribute, List newAttributes) { + super.updateAttribute(javaAttribute, newAttributes); + if (javaAttribute.getType() instanceof AttributeType.Builtin type) { + if (type == AttributeType.Builtin.CAMERA_DISTANCE) { + vehicleComponent.setCameraDistance((float) AttributeUtils.calculateValue(javaAttribute)); + } + } + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.BODY ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java index 236b22c51..36a126687 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java @@ -47,6 +47,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; 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.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -169,8 +170,8 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return Vector2f.UNIT_Y; + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; } @Override @@ -195,4 +196,9 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic public boolean canWalkOnLava() { return true; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index d6a8ece7c..7ae672bcc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -48,6 +48,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; 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.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -128,8 +129,8 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return Vector2f.UNIT_Y; + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; } @Override @@ -154,4 +155,9 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli public JavaRegistryKey variantRegistry() { return JavaRegistries.PIG_VARIANT; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } 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 7b6184579..8b0e77c73 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 @@ -45,6 +45,7 @@ import org.geysermc.geyser.session.cache.tags.ItemTag; 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.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -286,4 +287,13 @@ public class AbstractHorseEntity extends AnimalEntity { return InteractionResult.SUCCESS; } } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + if (slot != EquipmentSlot.SADDLE) { + return super.canUseSlot(slot); + } else { + return isAlive() && !isBaby() && getFlag(EntityFlag.TAMED); + } + } } 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 6239122f4..db72b8ea2 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 @@ -155,8 +155,9 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f); + 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 diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java index b8a9a8f28..478f89aeb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java @@ -29,6 +29,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -44,4 +45,9 @@ public class HorseEntity extends AbstractHorseEntity { dirtyMetadata.put(EntityDataTypes.VARIANT, value & 255); dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (value >> 8) % 5); } -} \ No newline at end of file + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java index d27a1fff3..6ef1cd57a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java @@ -35,6 +35,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -61,4 +62,9 @@ public class LlamaEntity extends ChestedHorseEntity { protected @Nullable Tag getFoodTag() { return ItemTag.LLAMA_FOOD; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } } 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 7d022ed7c..1ed9328f0 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 @@ -90,13 +90,13 @@ public class CamelVehicleComponent extends VehicleComponent { } @Override - protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { if (isStationary()) { return Vector3f.ZERO; } SessionPlayerEntity player = vehicle.getSession().getPlayerEntity(); - Vector3f inputVelocity = super.getInputVelocity(ctx, speed); + Vector3f inputVelocity = super.getInputVector(ctx, speed, input); float jumpStrength = player.getVehicleJumpStrength(); if (jumpStrength > 0) { @@ -117,11 +117,11 @@ public class CamelVehicleComponent extends VehicleComponent { } @Override - protected Vector2f getVehicleRotation() { + protected Vector2f getRiddenRotation() { if (isStationary()) { return Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); } - return super.getVehicleRotation(); + return super.getRiddenRotation(); } /** diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java index e6aaf1daa..c0480ee90 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java @@ -26,14 +26,18 @@ package org.geysermc.geyser.entity.vehicle; import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; public interface ClientVehicle { VehicleComponent getVehicleComponent(); - Vector2f getAdjustedInput(Vector2f input); + // MojMap LivingEntity#getRiddenInput + Vector3f getRiddenInput(Vector2f input); + // MojMap LivingEntity#getRiddenSpeed float getVehicleSpeed(); + // MojMap Mob#getControllingPassenger boolean isClientControlled(); default boolean canWalkOnLava() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java new file mode 100644 index 000000000..88045064a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java @@ -0,0 +1,128 @@ +/* + * 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.Getter; +import lombok.Setter; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.Fluid; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; + +@Setter +@Getter +public class HappyGhastVehicleComponent extends VehicleComponent { + + private float flyingSpeed; + private float cameraDistance; + + public HappyGhastVehicleComponent(HappyGhastEntity vehicle, float stepHeight) { + super(vehicle, stepHeight); + // Happy Ghast has different defaults + flyingSpeed = 0.05f; + moveSpeed = 0.05f; + cameraDistance = 8.0f; + } + + @Override + protected void updateRotation() { + float addYaw = MathUtils.wrapDegrees(getRiddenRotation().getX() - vehicle.getYaw()) * 0.08f; + vehicle.setYaw(vehicle.getYaw() + addYaw); + } + + @Override + public void onMount() { + super.onMount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION, false); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION_DEGREES, 181f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, cameraDistance); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + @Override + public void onDismount() { + super.onDismount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, (float) AttributeType.Builtin.CAMERA_DISTANCE.getDef()); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + /** + * Called every session tick while the player is mounted on the vehicle. + */ + public void tickVehicle() { + if (!vehicle.isClientControlled()) { + return; + } + + VehicleContext ctx = new VehicleContext(); + ctx.loadSurroundingBlocks(); + + // LivingEntity#travelFlying + Fluid fluid = checkForFluid(ctx); + float drag = switch (fluid) { + case WATER -> 0.8f; + case LAVA -> 0.5f; + case EMPTY -> 0.91f; + }; + // HappyGhast#travel + travel(ctx, flyingSpeed * 5.0f / 3.0f); + vehicle.setMotion(vehicle.getMotion().mul(drag)); + } + + private Fluid checkForFluid(VehicleContext ctx) { + Fluid result = Fluid.EMPTY; + + BoundingBox box = boundingBox.clone(); + box.expand(-0.001); + + Vector3d min = box.getMin(); + Vector3d max = box.getMax(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + for (iter.reset(); iter.hasNext(); iter.next()) { + BlockState blockState = ctx.getBlock(iter); + if (blockState.is(Blocks.WATER)) { + return Fluid.WATER; // Water takes priority over lava + } + if (blockState.is(Blocks.LAVA)) { + result = Fluid.LAVA; + } + } + + return result; + } +} 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 61875ec90..a7a1f2416 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 @@ -26,6 +26,8 @@ package org.geysermc.geyser.entity.vehicle; import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.TrigMath; import org.cloudburstmc.math.vector.Vector2f; @@ -64,20 +66,27 @@ public class VehicleComponent { private static final float MIN_VELOCITY = 0.003f; protected final T vehicle; + @Getter protected final BoundingBox boundingBox; + protected Vector2f lastSentRotation; // (yaw, pitch) protected float stepHeight; + @Getter @Setter protected float moveSpeed; protected double gravity; + @Getter @Setter + protected double waterMovementEfficiency; protected int effectLevitation; protected boolean effectSlowFalling; protected boolean effectWeaving; public VehicleComponent(T vehicle, float stepHeight) { this.vehicle = vehicle; + this.lastSentRotation = Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); this.stepHeight = stepHeight; this.moveSpeed = (float) AttributeType.Builtin.MOVEMENT_SPEED.getDef(); this.gravity = AttributeType.Builtin.GRAVITY.getDef(); + this.waterMovementEfficiency = AttributeType.Builtin.WATER_MOVEMENT_EFFICIENCY.getDef(); double width = vehicle.getBoundingBoxWidth(); double height = vehicle.getBoundingBoxHeight(); @@ -117,10 +126,6 @@ public class VehicleComponent { boundingBox.translate(vec); } - public BoundingBox getBoundingBox() { - return this.boundingBox; - } - public void setEffect(Effect effect, int effectAmplifier) { switch (effect) { case LEVITATION -> effectLevitation = effectAmplifier + 1; @@ -137,14 +142,6 @@ public class VehicleComponent { } } - public void setMoveSpeed(float moveSpeed) { - this.moveSpeed = moveSpeed; - } - - public float getMoveSpeed() { - return moveSpeed; - } - public void setStepHeight(float stepHeight) { this.stepHeight = MathUtils.clamp(stepHeight, 1.0f, 10.0f); } @@ -193,6 +190,13 @@ public class VehicleComponent { } } + protected void updateRotation() { + Vector2f rot = getRiddenRotation(); + vehicle.setYaw(rot.getX()); + vehicle.setHeadYaw(rot.getX()); + vehicle.setPitch(rot.getY()); + } + /** * Adds velocity of all colliding fluids to the vehicle, and returns the height of the fluid to use for movement. * @@ -208,6 +212,7 @@ public class VehicleComponent { BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + // Mojmap Entity#updateInWaterStateAndDoFluidPushing double waterHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.WATER, 0.014, min.getY()); double lavaHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.LAVA, vehicle.getSession().getDimensionType().ultrawarm() ? 0.007 : 0.007 / 3, min.getY()); @@ -341,11 +346,12 @@ public class VehicleComponent { } /** - * Java edition returns the zero vector if the length of the input vector is less than 0.0001 + * Java edition returns the zero vector if the length of the input vector is less than 0.00001f */ protected Vector3d javaNormalize(Vector3d vec) { double len = vec.length(); - return len < 1.0E-4 ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); + // Used to be 1.0E-4 + return len < 1.0E-5F ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); } protected float getWorldFluidHeight(Fluid fluidType, int blockId) { @@ -373,6 +379,7 @@ public class VehicleComponent { return BlockUtils.getCollision(adjacentBlockId) instanceof SolidCollision; } + // Mojmap: LivingEntity#travelInFluid protected void waterMovement(VehicleContext ctx) { double gravity = getGravity(); float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier @@ -380,6 +387,21 @@ public class VehicleComponent { boolean falling = vehicle.getMotion().getY() <= 0; // NOT IMPLEMENTED: depth strider and dolphins grace +// float g = 0.02f; +// float waterMovementEfficiencyMultiplier = (float) waterMovementEfficiency; +// if (!vehicle.isOnGround()) { +// // TODO test +// waterMovementEfficiencyMultiplier *= 0.5f; +// } +// +// if (waterMovementEfficiencyMultiplier > 0.0F) { +// drag += (0.54600006F - drag) * waterMovementEfficiencyMultiplier; +// g += (this.getSpeed() - g) * waterMovementEfficiencyMultiplier; +// } + +// if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) { +// drag = 0.96F; +// } boolean horizontalCollision = travel(ctx, 0.02f); @@ -570,6 +592,7 @@ public class VehicleComponent { * * @return true if there was a horizontal collision */ + // Mojmap: LivingEntity#moveRelative / LivingEntity#move protected boolean travel(VehicleContext ctx, float speed) { Vector3f motion = vehicle.getMotion(); @@ -582,9 +605,13 @@ public class VehicleComponent { Math.abs(motion.getZ()) < MIN_VELOCITY ? 0 : motion.getZ() ); + updateRotation(); + Vector2f playerInput = vehicle.getSession().getPlayerEntity().getVehicleInput(); + Vector3f riddenInput = vehicle.getRiddenInput(playerInput.mul(0.98f)); + // !isImmobile if (vehicle.isAlive()) { - motion = motion.add(getInputVelocity(ctx, speed)); + motion = motion.add(getInputVector(ctx, speed, riddenInput)); } Vector3f movementMultiplier = getBlockMovementMultiplier(ctx); @@ -670,41 +697,30 @@ public class VehicleComponent { return false; } - /** - * Translates the player's input into velocity. - * - * @param ctx context - * @param speed multiplier for input - * @return velocity - */ - protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { - Vector2f input = vehicle.getSession().getPlayerEntity().getVehicleInput(); - input = input.mul(0.98f); - input = vehicle.getAdjustedInput(input); - input = normalizeInput(input); + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { + double lenSquared = input.lengthSquared(); + if (lenSquared < 1.0E-7) { + return Vector3f.ZERO; + } + + if (lenSquared > 1.0f) { + input = input.normalize(); + } input = input.mul(speed); - // Match player rotation - float yaw = vehicle.getSession().getPlayerEntity().getYaw(); + // Match vehicle rotation + float yaw = vehicle.getYaw(); float sin = TrigMath.sin(yaw * TrigMath.DEG_TO_RAD); float cos = TrigMath.cos(yaw * TrigMath.DEG_TO_RAD); - return Vector3f.from(input.getX() * cos - input.getY() * sin, 0, input.getY() * cos + input.getX() * sin); - } - - protected Vector2f normalizeInput(Vector2f input) { - float lenSquared = input.lengthSquared(); - if (lenSquared < 1.0E-7) { - return Vector2f.ZERO; - } else if (lenSquared > 1.0) { - return input.normalize(); - } - return input; + return Vector3f.from(input.getX() * cos - input.getZ() * sin, input.getY(), input.getZ() * cos + input.getX() * sin); } /** * Gets the rotation to use for the vehicle. This is based on the player's head rotation. + * + * @return (yaw, pitch) */ - protected Vector2f getVehicleRotation() { + protected Vector2f getRiddenRotation() { LivingEntity player = vehicle.getSession().getPlayerEntity(); return Vector2f.from(player.getYaw(), player.getPitch() * 0.5f); } @@ -717,7 +733,6 @@ public class VehicleComponent { */ protected void moveVehicle(Vector3d javaPos) { Vector3f bedrockPos = javaPos.toFloat(); - Vector2f rotation = getVehicleRotation(); MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId()); @@ -740,27 +755,25 @@ public class VehicleComponent { } vehicle.setPosition(bedrockPos); - if (vehicle.getYaw() != rotation.getX()) { + if (vehicle.getYaw() != lastSentRotation.getX()) { moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); - moveEntityDeltaPacket.setYaw(rotation.getX()); - vehicle.setYaw(rotation.getX()); + moveEntityDeltaPacket.setYaw(vehicle.getYaw()); } - if (vehicle.getPitch() != rotation.getY()) { + if (vehicle.getPitch() != lastSentRotation.getY()) { moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); - moveEntityDeltaPacket.setPitch(rotation.getY()); - vehicle.setPitch(rotation.getY()); + moveEntityDeltaPacket.setPitch(vehicle.getPitch()); } - if (vehicle.getHeadYaw() != rotation.getX()) { // Same as yaw + if (vehicle.getHeadYaw() != lastSentRotation.getX()) { // Same as yaw moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); - moveEntityDeltaPacket.setHeadYaw(rotation.getX()); - vehicle.setHeadYaw(rotation.getX()); + moveEntityDeltaPacket.setHeadYaw(vehicle.getYaw()); } + lastSentRotation = Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); if (!moveEntityDeltaPacket.getFlags().isEmpty()) { vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket); } - ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, rotation.getX(), rotation.getY(), vehicle.isOnGround()); + ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw(), vehicle.getPitch(), vehicle.isOnGround()); vehicle.getSession().sendDownstreamPacket(moveVehiclePacket); } diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java index c055b825c..2072ace29 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java @@ -213,9 +213,6 @@ public class MapHasher { } public HashCode build() { - if (unhashed != null) { - System.out.println(unhashed); - } return encoder.map(map); } } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 722c65ac0..25acb493f 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -48,9 +48,12 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.dialog.Dialog; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Optional; @@ -64,6 +67,14 @@ public class JavaRegistries { Block::javaId, Block::javaIdentifier, key -> Optional.ofNullable(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get(key.asString())).orElse(-1)); public static final JavaRegistryKey ITEM = createHardcoded("item", Registries.JAVA_ITEMS, Item::javaId, Item::javaKey, key -> Optional.ofNullable(Registries.JAVA_ITEM_IDENTIFIERS.get(key.asString())).map(Item::javaId).orElse(-1)); + public static JavaRegistryKey ENTITY_TYPE = createHardcoded("entity_type", Arrays.asList(EntityType.values()), EntityType::ordinal, + type -> MinecraftKey.key(type.name().toLowerCase(Locale.ROOT)), key -> { + try { + return EntityType.valueOf(key.value().toUpperCase(Locale.ROOT)).ordinal(); + } catch (IllegalArgumentException exception) { + return -1; // Non-existent entity type + } + }); public static final JavaRegistryKey CHAT_TYPE = create("chat_type"); public static final JavaRegistryKey DIMENSION_TYPE = create("dimension_type"); @@ -95,6 +106,11 @@ public class JavaRegistries { private static JavaRegistryKey createHardcoded(String key, ListRegistry registry, RegistryNetworkMapper networkSerializer, RegistryIdentifierMapper identifierMapper, RegistryIdMapper idMapper) { + return createHardcoded(key, registry.get(), networkSerializer, identifierMapper, idMapper); + } + + private static JavaRegistryKey createHardcoded(String key, List registry, RegistryNetworkMapper networkSerializer, + RegistryIdentifierMapper identifierMapper, RegistryIdMapper idMapper) { return create(key, new HardcodedLookup<>(registry, networkSerializer, identifierMapper, idMapper)); } @@ -130,7 +146,7 @@ public class JavaRegistries { int get(Key key); } - private record HardcodedLookup(ListRegistry registry, RegistryNetworkMapper networkMapper, RegistryIdentifierMapper identifierMapper, + private record HardcodedLookup(List registry, RegistryNetworkMapper networkMapper, RegistryIdentifierMapper identifierMapper, RegistryIdMapper idMapper) implements JavaRegistryKey.RegistryLookup { @Override diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetPassengersTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetPassengersTranslator.java index 1ca9e268a..1c3ac4688 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetPassengersTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetPassengersTranslator.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.translator.protocol.java.entity; +import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; @@ -50,7 +51,9 @@ public class JavaSetPassengersTranslator extends PacketTranslator newPassengers = new ArrayList<>(); - for (int passengerId : packet.getPassengerIds()) { + int @NonNull [] passengerIds = packet.getPassengerIds(); + for (int i = 0; i < passengerIds.length; i++) { + int passengerId = passengerIds[i]; Entity passenger = session.getEntityCache().getEntityByJavaId(passengerId); if (passenger == session.getPlayerEntity()) { session.getPlayerEntity().setVehicle(entity); @@ -76,13 +79,15 @@ public class JavaSetPassengersTranslator extends PacketTranslator 1)); + EntityUtils.updateMountOffset(passenger, entity, rider, true, i, packet.getPassengerIds().length); // Force an update to the passenger metadata passenger.updateBedrockMetadata(); } // Handle passengers that were removed - for (Entity passenger : entity.getPassengers()) { + List passengers = entity.getPassengers(); + for (int i = 0; i < passengers.size(); i++) { + Entity passenger = passengers.get(i); if (passenger == null) { continue; } @@ -93,7 +98,7 @@ public class JavaSetPassengersTranslator extends PacketTranslator 1)); + EntityUtils.updateMountOffset(passenger, entity, false, false, i, packet.getPassengerIds().length); // Force an update to the passenger metadata passenger.updateBedrockMetadata(); diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index 388162a49..7ddc03920 100644 --- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java @@ -39,15 +39,19 @@ import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.TextDisplayEntity; import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.tags.GeyserHolderSet; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import java.util.Locale; @@ -166,7 +170,7 @@ public final class EntityUtils { /** * Adjust an entity's height if they have mounted/dismounted an entity. */ - public static void updateMountOffset(Entity passenger, Entity mount, boolean rider, boolean riding, boolean moreThanOneEntity) { + public static void updateMountOffset(Entity passenger, Entity mount, boolean rider, boolean riding, int index, int passengers) { passenger.setFlag(EntityFlag.RIDING, riding); if (riding) { // Without the Y offset, Bedrock players will find themselves in the floor when mounting @@ -179,7 +183,7 @@ public final class EntityUtils { switch (mount.getDefinition().entityType()) { case CAMEL -> { zOffset = 0.5f; - if (moreThanOneEntity) { + if (passengers > 1) { if (!rider) { zOffset = -0.7f; } @@ -221,12 +225,18 @@ public final class EntityUtils { } } } + case HAPPY_GHAST -> { + int seatingIndex = Math.min(index, 4); + xOffset = HappyGhastEntity.X_OFFSETS[seatingIndex]; + yOffset = 3.4f; + zOffset = HappyGhastEntity.Z_OFFSETS[seatingIndex]; + } } if (mount instanceof ChestBoatEntity) { xOffset = 0.15F; } else if (mount instanceof BoatEntity) { // Without the X offset, more than one entity on a boat is stacked on top of each other - if (moreThanOneEntity) { + if (passengers > 1) { xOffset = rider ? 0.2f : -0.6f; if (passenger instanceof AnimalEntity) { xOffset += 0.2f; @@ -342,6 +352,15 @@ public final class EntityUtils { return translatedEntityName("minecraft", typeName, session); } + public static boolean equipmentUsableByEntity(GeyserSession session, Equippable equippable, EntityType entity) { + if (equippable.allowedEntities() == null) { + return true; + } + + GeyserHolderSet holderSet = GeyserHolderSet.fromHolderSet(JavaRegistries.ENTITY_TYPE, equippable.allowedEntities()); + return session.getTagCache().is(holderSet, entity); + } + private EntityUtils() { } }