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

Expose entity hitboxes in API, store bedrock position to avoid re-calculation

This commit is contained in:
onebeastchris
2025-12-16 23:00:59 +01:00
parent 9f05f1ef9e
commit 199737d1ef
30 changed files with 416 additions and 85 deletions

View File

@@ -1,5 +1,7 @@
package org.geysermc.geyser.api.entity.data;
import org.geysermc.geyser.api.entity.data.types.Hitbox;
/**
* Contains commonly used {@link GeyserEntityDataType} constants for built-in entity
* metadata fields.
@@ -48,6 +50,12 @@ public final class GeyserEntityDataTypes {
public static final GeyserEntityDataType<Float> SCALE =
GeyserEntityDataType.of(Float.class, "scale");
/**
* Represents custom hitboxes for entities
*/
public static final GeyserListEntityDataType<Hitbox> HITBOXES =
GeyserListEntityDataType.of(Hitbox.class, "hitboxes");
private GeyserEntityDataTypes() {
// no-op
}

View File

@@ -0,0 +1,48 @@
/*
* 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.api.entity.data;
import org.geysermc.geyser.api.GeyserApi;
import java.util.List;
/**
* Represents a list of objects for an entity data types
* For example, there can be multiple hitboxes on an entity
*
* @param <T>
*/
public interface GeyserListEntityDataType<T> extends GeyserEntityDataType<List<T>> {
Class<T> listTypeClass();
/**
* API usage only, use the types defined in {@link GeyserEntityDataTypes}
*/
static <T> GeyserListEntityDataType<T> of(Class<T> typeClass, String name) {
return GeyserApi.api().provider(GeyserListEntityDataType.class, List.class, typeClass, name);
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.api.entity.data.types;
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.api.GeyserApi;
/**
* Represents an entity hitbox.
*/
public interface Hitbox {
/**
* The min "corner" of the hitbox
* @return the vector of the corner
*/
Vector3f min();
/**
* The max "corner" of the hitbox
* @return the vector of the corner
*/
Vector3f max();
/**
* The pivot of the hitbox
* @return
*/
Vector3f pivot();
static Builder builder() {
return GeyserApi.api().provider(Builder.class);
}
/**
* The builder for the hitbox
*/
interface Builder {
Builder min(Vector3f min);
Builder max(Vector3f max);
Builder origin(Vector3f pivot);
Hitbox build();
}
}

View File

@@ -113,7 +113,9 @@ public class Entity implements GeyserEntity {
* The entity position as it is known to the Java server
*/
@Accessors(fluent = true)
protected Vector3f position;
private Vector3f position;
@Setter(AccessLevel.NONE)
private Vector3f bedrockPosition;
protected Vector3f motion;
/**
@@ -708,8 +710,13 @@ public class Entity implements GeyserEntity {
return this.valid;
}
public void position(Vector3f position) {
this.position = position;
this.bedrockPosition = position.up(offset);
}
public Vector3f bedrockPosition() {
return position.up(offset);
return bedrockPosition;
}
/**
@@ -860,10 +867,10 @@ public class Entity implements GeyserEntity {
}
}
public void offset(float offset) {
public void offset(float offset, boolean teleport) {
this.offset = offset;
// TODO queue?
if (isValid()) {
if (isValid() && teleport) {
this.moveRelative(0, 0, 0, 0, 0, isOnGround());
}
}

View File

@@ -64,7 +64,7 @@ public class FireballEntity extends ThrowableEntity {
newPosition = tickMovement(newPosition);
}
super.moveAbsoluteImmediate(newPosition, yaw, pitch, headYaw, isOnGround, teleported);
this.position = javaPosition;
position(javaPosition);
this.motion = lastMotion;
}
@@ -73,6 +73,6 @@ public class FireballEntity extends ThrowableEntity {
if (removedInVoid()) {
return;
}
moveAbsoluteImmediate(tickMovement(position), getYaw(), getPitch(), getHeadYaw(), false, false);
moveAbsoluteImmediate(tickMovement(position()), getYaw(), getPitch(), getHeadYaw(), false, false);
}
}

View File

@@ -113,7 +113,7 @@ public class FishingHookEntity extends ThrowableEntity {
if (!collided) {
super.moveAbsoluteImmediate(javaPosition, yaw, pitch, headYaw, isOnGround, teleported);
} else {
super.moveAbsoluteImmediate(this.position, yaw, pitch, headYaw, true, true);
super.moveAbsoluteImmediate(this.position(), yaw, pitch, headYaw, true, true);
}
}
@@ -144,7 +144,7 @@ public class FishingHookEntity extends ThrowableEntity {
float gravity = getGravity();
motion = motion.down(gravity);
moveAbsoluteImmediate(position.add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
moveAbsoluteImmediate(position().add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
float drag = getDrag();
motion = motion.mul(drag);
@@ -162,7 +162,7 @@ public class FishingHookEntity extends ThrowableEntity {
* @return true if this entity is currently in air.
*/
protected boolean isInAir() {
int block = session.getGeyser().getWorldManager().getBlockAt(session, position.toInt());
int block = session.getGeyser().getWorldManager().getBlockAt(session, position().toInt());
return block == Block.JAVA_AIR_ID;
}

View File

@@ -119,7 +119,7 @@ public class InteractionEntity extends Entity {
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
moveAbsolute(position.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false);
moveAbsolute(position().add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false);
}
@Override
@@ -141,7 +141,7 @@ public class InteractionEntity extends Entity {
setBoundingBoxHeight(Math.min(height.getPrimitiveValue(), 64f));
if (secondEntity != null) {
secondEntity.moveAbsolute(position.up(getBoundingBoxHeight()), yaw, pitch, onGround, true);
secondEntity.moveAbsolute(position().up(getBoundingBoxHeight()), yaw, pitch, onGround, true);
}
}
@@ -159,7 +159,7 @@ public class InteractionEntity extends Entity {
}
if (this.secondEntity == null) {
secondEntity = new ArmorStandEntity(EntitySpawnContext.inherited(session, VanillaEntities.ARMOR_STAND, this, position.up(getBoundingBoxHeight())));
secondEntity = new ArmorStandEntity(EntitySpawnContext.inherited(session, VanillaEntities.ARMOR_STAND, this, position().up(getBoundingBoxHeight())));
}
secondEntity.getDirtyMetadata().put(EntityDataTypes.NAME, nametag);
secondEntity.getDirtyMetadata().put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, isNameTagVisible ? (byte) 1 : (byte) 0);

View File

@@ -79,7 +79,7 @@ public class ItemEntity extends ThrowableEntity {
if (!isOnGround() || (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) > 0.00001) {
float gravity = getGravity();
motion = motion.down(gravity);
moveAbsoluteImmediate(position.add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
moveAbsoluteImmediate(position().add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
float drag = getDrag();
motion = motion.mul(drag, 0.98f, drag);
}
@@ -118,7 +118,7 @@ public class ItemEntity extends ThrowableEntity {
this.offset = Math.abs(offset);
}
super.moveAbsoluteImmediate(javaPosition, 0, 0, 0, isOnGround, teleported);
this.position = javaPosition;
position(javaPosition);
waterLevel = session.getGeyser().getWorldManager().getBlockAtAsync(session, javaPosition.getFloorX(), javaPosition.getFloorY(), javaPosition.getFloorZ())
.thenApply(BlockStateValues::getWaterLevel);
@@ -137,7 +137,7 @@ public class ItemEntity extends ThrowableEntity {
@Override
protected float getDrag() {
if (isOnGround()) {
Vector3i groundBlockPos = position.toInt().down();
Vector3i groundBlockPos = position().toInt().down();
BlockState blockState = session.getGeyser().getWorldManager().blockAt(session, groundBlockPos);
return BlockStateValues.getSlipperiness(blockState) * 0.98f;
}

View File

@@ -80,7 +80,7 @@ public class ItemFrameEntity extends HangingEntity {
super(context);
blockDefinition = buildBlockDefinition(Direction.SOUTH); // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary
bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ());
bedrockPosition = position().floor().toInt();
session.getItemFrameCache().put(bedrockPosition, this);
}

View File

@@ -35,7 +35,7 @@ public class LeashKnotEntity extends Entity {
super(context);
// Position is incorrect by default
// TODO offset
position(position.add(0.5f, 0.25f, 0.5f));
position(position().add(0.5f, 0.25f, 0.5f));
}
@Override

View File

@@ -194,7 +194,7 @@ public class MinecartEntity extends Entity implements Tickable {
}
private void updateCompletedStep() {
lastCompletedStep = new MinecartStep(position.toDouble(), motion.toDouble(), yaw, pitch, 0.0F);
lastCompletedStep = new MinecartStep(position().toDouble(), motion.toDouble(), yaw, pitch, 0.0F);
}
@Override

View File

@@ -92,7 +92,7 @@ public class PaintingEntity extends HangingEntity {
valid = true;
session.getGeyser().getLogger().debug("Spawned painting on " + position);
session.getGeyser().getLogger().debug("Spawned painting on " + position());
}
@Override
@@ -101,7 +101,7 @@ public class PaintingEntity extends HangingEntity {
}
private Vector3f fixOffset(PaintingType paintingName) {
Vector3f position = super.position;
Vector3f position = position();
// ViaVersion already adds the offset for us on older versions,
// so no need to do it then otherwise it will be spaced
if (session.isEmulatePost1_18Logic()) {

View File

@@ -90,7 +90,7 @@ public class TextDisplayEntity extends DisplayBaseEntity {
// If the line count changed, update the position to account for the new offset
if (previousLineCount != lineCount) {
moveAbsolute(position, yaw, pitch, headYaw, onGround, false);
moveAbsolute(position(), yaw, pitch, headYaw, onGround, false);
}
}

View File

@@ -43,7 +43,7 @@ public class ThrowableEntity extends Entity implements Tickable {
public ThrowableEntity(EntitySpawnContext context) {
super(context);
this.lastJavaPosition = position;
this.lastJavaPosition = position();
}
/**
@@ -55,7 +55,7 @@ public class ThrowableEntity extends Entity implements Tickable {
if (removedInVoid()) {
return;
}
moveAbsoluteImmediate(position.add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
moveAbsoluteImmediate(position().add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
float drag = getDrag();
float gravity = getGravity();
motion = motion.mul(drag).down(gravity);
@@ -75,15 +75,15 @@ public class ThrowableEntity extends Entity implements Tickable {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING);
}
if (this.position.getX() != javaPosition.getX()) {
if (this.position().getX() != javaPosition.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(javaPosition.getX());
}
if (this.position.getY() != javaPosition.getY()) {
if (this.position().getY() != javaPosition.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(javaPosition.getY() + offset);
}
if (this.position.getZ() != javaPosition.getZ()) {
if (this.position().getZ() != javaPosition.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(javaPosition.getZ());
}
@@ -155,7 +155,7 @@ public class ThrowableEntity extends Entity implements Tickable {
* @return true if this entity is currently in water.
*/
protected boolean isInWater() {
int block = session.getGeyser().getWorldManager().getBlockAt(session, position.toInt());
int block = session.getGeyser().getWorldManager().getBlockAt(session, position().toInt());
return BlockStateValues.getWaterLevel(block) != -1;
}
@@ -173,7 +173,7 @@ public class ThrowableEntity extends Entity implements Tickable {
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
moveAbsoluteImmediate(lastJavaPosition.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false);
lastJavaPosition = position;
lastJavaPosition = position();
}
@Override
@@ -188,7 +188,7 @@ public class ThrowableEntity extends Entity implements Tickable {
* @return true if the entity was removed
*/
public boolean removedInVoid() {
if (position.getY() < session.getDimensionType().minY() - 64) {
if (position().getY() < session.getDimensionType().minY() - 64) {
session.getEntityCache().removeEntity(this);
return true;
}

View File

@@ -92,11 +92,9 @@ public class ArmorStandEntity extends LivingEntity {
@Override
public void spawnEntity() {
Vector3f javaPosition = position;
// Apply the offset if we're the second entity
position = position.up(getYOffset());
offset(getYOffset(), false);
super.spawnEntity();
position = javaPosition;
}
@Override
@@ -109,7 +107,7 @@ public class ArmorStandEntity extends LivingEntity {
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
moveAbsolute(position.add(relX, relY, relZ), yaw, pitch, headYaw, onGround, false);
moveAbsolute(position().add(relX, relY, relZ), yaw, pitch, headYaw, onGround, false);
}
@Override
@@ -118,9 +116,8 @@ public class ArmorStandEntity extends LivingEntity {
secondEntity.moveAbsolute(javaPosition, yaw, pitch, headYaw, isOnGround, teleported);
}
// Fake the height to be above where it is so the nametag appears in the right location
float yOffset = getYOffset();
super.moveAbsolute(yOffset != 0 ? javaPosition.up(yOffset) : javaPosition, yaw, yaw, yaw, isOnGround, teleported);
this.position = javaPosition;
offset(getYOffset(), false);
super.moveAbsolute(javaPosition, yaw, yaw, yaw, isOnGround, teleported);
}
@Override
@@ -240,7 +237,7 @@ public class ArmorStandEntity extends LivingEntity {
super.updateBedrockMetadata();
if (positionUpdateRequired) {
positionUpdateRequired = false;
moveAbsolute(position, yaw, pitch, headYaw, onGround, true);
moveAbsolute(position(), yaw, pitch, headYaw, onGround, true);
}
}
@@ -341,8 +338,7 @@ public class ArmorStandEntity extends LivingEntity {
if (secondEntity == null) {
// Create the second entity. It doesn't need to worry about the items, but it does need to worry about
// the metadata as it will hold the name tag.
// TODO
secondEntity = new ArmorStandEntity(EntitySpawnContext.inherited(session, VanillaEntities.ARMOR_STAND, this, position));
secondEntity = new ArmorStandEntity(EntitySpawnContext.inherited(session, VanillaEntities.ARMOR_STAND, this, position()));
secondEntity.primaryEntity = false;
}
// Copy metadata

View File

@@ -128,7 +128,7 @@ public class SquidEntity extends AgeableWaterEntity implements Tickable {
if (getFlag(EntityFlag.RIDING)) {
inWater = CompletableFuture.completedFuture(false);
} else {
inWater = session.getGeyser().getWorldManager().getBlockAtAsync(session, position.toInt())
inWater = session.getGeyser().getWorldManager().getBlockAtAsync(session, position().toInt())
.thenApply(block -> BlockStateValues.getWaterLevel(block) != -1);
}
}

View File

@@ -52,7 +52,7 @@ public class OcelotEntity extends AnimalEntity {
@NonNull
@Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().position().distanceSquared(position) < 9f) {
if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().position().distanceSquared(position()) < 9f) {
// Attempt to feed
return InteractiveTag.FEED;
} else {
@@ -63,7 +63,7 @@ public class OcelotEntity extends AnimalEntity {
@NonNull
@Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) {
if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().position().distanceSquared(position) < 9f) {
if (!getFlag(EntityFlag.TRUSTING) && canEat(itemInHand) && session.getPlayerEntity().position().distanceSquared(position()) < 9f) {
// Attempt to feed
return InteractionResult.SUCCESS;
} else {

View File

@@ -102,7 +102,7 @@ public class SnifferEntity extends AnimalEntity implements Tickable {
// The java client renders digging particles on its own, but bedrock does not
if (digTicks > 0 && --digTicks < DIG_START && digTicks % 5 == 0) {
Vector3f rot = Vector3f.createDirectionDeg(0, -getYaw()).mul(2.25f);
Vector3f pos = position.add(rot).up(0.2f).floor(); // Handle non-full blocks
Vector3f pos = position().add(rot).up(0.2f).floor(); // Handle non-full blocks
int blockId = session.getBlockMappings().getBedrockBlockId(session.getGeyser().getWorldManager().getBlockAt(session, pos.toInt().down()));
LevelEventPacket levelEventPacket = new LevelEventPacket();

View File

@@ -149,12 +149,12 @@ public class VillagerEntity extends AbstractMerchantEntity {
setPitch(pitch);
setHeadYaw(headYaw);
setOnGround(isOnGround);
this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
position(Vector3f.from(position().getX() + relX, position().getY() + relY, position().getZ() + relZ));
MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
moveEntityPacket.setRuntimeEntityId(geyserId);
moveEntityPacket.setRotation(Vector3f.from(0, 0, bedRotation));
moveEntityPacket.setPosition(Vector3f.from(position.getX() + xOffset, position.getY() + offset, position.getZ() + zOffset));
moveEntityPacket.setPosition(position().add(xOffset, offset, zOffset));
moveEntityPacket.setOnGround(isOnGround);
moveEntityPacket.setTeleported(false);
session.sendUpstreamPacket(moveEntityPacket);

View File

@@ -134,7 +134,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
for (int i = 0; i < segmentHistory.length; i++) {
segmentHistory[i] = new Segment();
segmentHistory[i].yaw = getHeadYaw();
segmentHistory[i].y = position.getY();
segmentHistory[i].y = position().getY();
}
}
@@ -206,7 +206,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
}
// Send updated positions
for (EnderDragonPartEntity part : allParts) {
part.moveAbsolute(part.position().add(position), 0, 0, 0, false, false);
part.moveAbsolute(part.position().add(position()), 0, 0, 0, false, false);
}
}
@@ -277,7 +277,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
float xOffset = 8f * (random.nextFloat() - 0.5f);
float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f;
float zOffset = 8f * (random.nextFloat() - 0.5f);
Vector3f particlePos = position.add(xOffset, yOffset, zOffset);
Vector3f particlePos = position().add(xOffset, yOffset, zOffset);
LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(ParticleType.EXPLODE);
particlePacket.setPosition(particlePos);
@@ -311,7 +311,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
private void pushSegment() {
latestSegment = (latestSegment + 1) % segmentHistory.length;
segmentHistory[latestSegment].yaw = getHeadYaw();
segmentHistory[latestSegment].y = position.getY();
segmentHistory[latestSegment].y = position().getY();
}
/**

View File

@@ -41,7 +41,6 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.geysermc.geyser.entity.spawn.EntitySpawnContext;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.skin.SkinManager;
@@ -162,7 +161,7 @@ public class AvatarEntity extends LivingEntity {
setYaw(yaw);
setPitch(pitch);
setHeadYaw(headYaw);
this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
position(Vector3f.from(position().getX() + relX, position().getY() + relY, position().getZ() + relZ));
setOnGround(isOnGround);
@@ -175,9 +174,9 @@ public class AvatarEntity extends LivingEntity {
// If the player is moved while sleeping, we have to adjust their y, so it appears
// correctly on Bedrock. This fixes GSit's lay.
if (getFlag(EntityFlag.SLEEPING)) {
if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) {
if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position().toInt()) > 4)) {
// Force the player movement by using a teleport
movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - offset + 0.2f, position.getZ()));
movePlayerPacket.setPosition(Vector3f.from(position().getX(), position().getY() - offset + 0.2f, position().getZ()));
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
}
}
@@ -190,7 +189,7 @@ public class AvatarEntity extends LivingEntity {
}
@Override
public Entity position(Vector3f position) {
public void position(Vector3f position) {
if (this.bedPosition != null) {
// As of Bedrock 1.21.22 and Fabric 1.21.1
// Messes with Bedrock if we send this to the client itself, though.
@@ -198,7 +197,6 @@ public class AvatarEntity extends LivingEntity {
} else {
super.position(position);
}
return this;
}
@Override
@@ -316,7 +314,7 @@ public class AvatarEntity extends LivingEntity {
if (pose == Pose.SWIMMING) {
// This is just for, so we know if player is swimming or crawling.
if (session.getGeyser().getWorldManager().blockAt(session, position.toInt()).is(Blocks.WATER)) {
if (session.getGeyser().getWorldManager().blockAt(session, position().toInt()).is(Blocks.WATER)) {
setFlag(EntityFlag.SWIMMING, true);
} else {
setFlag(EntityFlag.CRAWLING, true);

View File

@@ -161,7 +161,7 @@ public class PlayerEntity extends AvatarEntity implements GeyserPlayerEntity {
return;
}
// The parrot is a separate entity in Bedrock, but part of the player entity in Java
EntitySpawnContext context = EntitySpawnContext.inherited(session, VanillaEntities.PARROT, this, position);
EntitySpawnContext context = EntitySpawnContext.inherited(session, VanillaEntities.PARROT, this, position());
if (context.callParrotEvent(this, variant.getAsInt(), !isLeft)) {
GeyserImpl.getInstance().getLogger().debug(session, "Cancelled parrot spawn as definition is null!");
return;

View File

@@ -163,11 +163,11 @@ public class SessionPlayerEntity extends PlayerEntity {
@Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
session.getCollisionManager().updatePlayerBoundingBox(this.position);
session.getCollisionManager().updatePlayerBoundingBox(position());
}
@Override
public Entity position(Vector3f position) {
public void position(Vector3f position) {
if (valid) { // Don't update during session init
session.getCollisionManager().updatePlayerBoundingBox(position);
@@ -175,8 +175,7 @@ public class SessionPlayerEntity extends PlayerEntity {
session.setNoClip(false);
}
}
this.position = position;
return this;
super.position(position);
}
/**
@@ -212,10 +211,10 @@ public class SessionPlayerEntity extends PlayerEntity {
* Set the player's position from a position sent in a Bedrock packet
*/
public void setPositionFromBedrock(Vector3f position) {
this.position = position.down(offset);
position(position.down(offset));
// Player is "above" the void so they're not supposed to no clip.
if (session.isNoClip() && this.position.getY() >= session.getBedrockDimension().minY() - 5) {
if (session.isNoClip() && position().getY() >= session.getBedrockDimension().minY() - 5) {
session.setNoClip(false);
}
}
@@ -497,7 +496,7 @@ public class SessionPlayerEntity extends PlayerEntity {
if (session.getGameMode() == GameMode.SPECTATOR) {
return false;
}
BlockState state = session.getGeyser().getWorldManager().blockAt(session, position.toInt());
BlockState state = session.getGeyser().getWorldManager().blockAt(session, position().toInt());
if (state.block().is(session, BlockTag.CLIMBABLE)) {
return true;
}
@@ -506,7 +505,7 @@ public class SessionPlayerEntity extends PlayerEntity {
if (!state.getValue(Properties.OPEN)) {
return false;
} else {
BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, position.toInt().down());
BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, position().toInt().down());
return belowState.is(Blocks.LADDER) && belowState.getValue(Properties.HORIZONTAL_FACING) == state.getValue(Properties.HORIZONTAL_FACING);
}
}

View File

@@ -138,28 +138,28 @@ public class BoatVehicleComponent extends VehicleComponent<BoatEntity> {
@Override
protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) {
Vector3f bedrockPos = javaPos.toFloat();
Vector3f oldPosition = vehicle.position();
vehicle.position(javaPos.toFloat());
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId());
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.geyserId());
if (vehicle.isOnGround()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
}
if (vehicle.getPosition().getX() != bedrockPos.getX()) {
if (vehicle.position().getX() != oldPosition.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(bedrockPos.getX());
moveEntityDeltaPacket.setX(vehicle.bedrockPosition().getX());
}
if (vehicle.getPosition().getY() != bedrockPos.getY()) {
if (vehicle.position().getY() != oldPosition.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(bedrockPos.getY() + vehicle.getDefinition().offset());
moveEntityDeltaPacket.setY(vehicle.bedrockPosition().getY());
}
if (vehicle.getPosition().getZ() != bedrockPos.getZ()) {
if (vehicle.position().getZ() != oldPosition.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(bedrockPos.getZ());
moveEntityDeltaPacket.setZ(vehicle.bedrockPosition().getZ());
}
vehicle.setPosition(bedrockPos);
if (vehicle.getPitch() != lastRotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
@@ -175,7 +175,7 @@ public class BoatVehicleComponent extends VehicleComponent<BoatEntity> {
}
if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket);
vehicle.getSession().sendUpstreamPacketImmediately(moveEntityDeltaPacket);
}
ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw() - 90, vehicle.getPitch(), vehicle.isOnGround());

View File

@@ -738,7 +738,8 @@ public class VehicleComponent<T extends Entity & ClientVehicle> {
* @param lastRotation the previous rotation of the vehicle (pitch, yaw, headYaw)
*/
protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) {
Vector3f bedrockPos = javaPos.toFloat();
Vector3f oldPosition = vehicle.position();
vehicle.position(javaPos.toFloat());
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.geyserId());
@@ -747,19 +748,18 @@ public class VehicleComponent<T extends Entity & ClientVehicle> {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
}
if (vehicle.position().getX() != bedrockPos.getX()) {
if (vehicle.position().getX() != oldPosition.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(bedrockPos.getX());
moveEntityDeltaPacket.setX(vehicle.bedrockPosition().getX());
}
if (vehicle.position().getY() != bedrockPos.getY()) {
if (vehicle.position().getY() != oldPosition.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(bedrockPos.getY());
moveEntityDeltaPacket.setY(vehicle.bedrockPosition().getY());
}
if (vehicle.position().getZ() != bedrockPos.getZ()) {
if (vehicle.position().getZ() != oldPosition.getZ()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(bedrockPos.getZ());
moveEntityDeltaPacket.setZ(vehicle.bedrockPosition().getZ());
}
vehicle.position(bedrockPos);
if (vehicle.getPitch() != lastRotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);

View File

@@ -50,7 +50,7 @@ public class GeyserEntityDataImpl<T> implements GeyserEntityDataType<T> {
TYPES.put("scale", new GeyserEntityDataImpl<>(Float.class, "scale", EntityDataTypes.SCALE));
// "custom"
TYPES.put("vertical_offset", new GeyserEntityDataImpl<>(Float.class, "offset", Entity::offset, Entity::getOffset));
TYPES.put("vertical_offset", new GeyserEntityDataImpl<>(Float.class, "offset", (entity, value) -> entity.offset(value, true), Entity::getOffset));
}
public static GeyserEntityDataImpl<?> lookup(Class<?> clazz, String name) {

View File

@@ -0,0 +1,82 @@
/*
* 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.impl.entity;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.geysermc.geyser.api.entity.data.GeyserListEntityDataType;
import org.geysermc.geyser.api.entity.data.types.Hitbox;
import org.geysermc.geyser.entity.type.Entity;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
public class GeyserListEntityDataImpl<ListType> extends GeyserEntityDataImpl<List<ListType>> implements GeyserListEntityDataType<ListType> {
public static Map<String, GeyserListEntityDataImpl<?>> TYPES;
static {
TYPES = new Object2ObjectOpenHashMap<>();
TYPES.put("hitboxes", new GeyserListEntityDataImpl<>(Hitbox.class, "hitboxes",
(entity, hitboxes) -> entity.getDirtyMetadata().put(EntityDataTypes.HITBOX, HitboxImpl.toNbtMap(hitboxes)),
(entity -> HitboxImpl.fromMetaData((NbtMap) entity.getMetadata().get(EntityDataTypes.HITBOX)))));
}
private final Class<ListType> listTypeClass;
public GeyserListEntityDataImpl(Class<ListType> typeClass, String name, BiConsumer<Entity, List<ListType>> consumer, Function<Entity, List<ListType>> getter) {
//noinspection unchecked - we do not talk about it
super((Class<List<ListType>>) (Class<?>) List.class, name, consumer, getter);
this.listTypeClass = typeClass;
}
@Override
public Class<ListType> listTypeClass() {
return listTypeClass;
}
public static GeyserListEntityDataImpl<?> lookup(Class<?> clazz, Class<?> listTypeClass, String name) {
Objects.requireNonNull(clazz);
Objects.requireNonNull(listTypeClass);
Objects.requireNonNull(name);
if (clazz != List.class) {
throw new IllegalStateException("Cannot look up list entity data for " + clazz + " and " + listTypeClass + " for " + name);
}
var type = TYPES.get(name);
if (type == null) {
throw new IllegalArgumentException("Unknown entity data type: " + name);
}
if (type.listTypeClass() == listTypeClass) {
return TYPES.get(name);
}
throw new IllegalArgumentException("Unknown entity data type: " + name);
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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.impl.entity;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.api.entity.data.types.Hitbox;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public record HitboxImpl(
Vector3f min,
Vector3f max,
Vector3f pivot
) implements Hitbox {
public static List<Hitbox> fromMetaData(@Nullable NbtMap metaDataMap) {
if (metaDataMap == null) {
return List.of();
}
List<Hitbox> boxes = new ArrayList<>();
List<NbtMap> hitboxes = metaDataMap.getList("Hitxboxes", NbtType.COMPOUND);
for (NbtMap hitbox : hitboxes) {
boxes.add(new HitboxImpl(
Vector3f.from(hitbox.getFloat("MinX"), hitbox.getFloat("MinY"), hitbox.getFloat("MinZ")),
Vector3f.from(hitbox.getFloat("MaxX"), hitbox.getFloat("MaxY"), hitbox.getFloat("MaxZ")),
Vector3f.from(hitbox.getFloat("PivotX"),hitbox.getFloat("PivotY"),hitbox.getFloat("PivotZ"))
));
}
return boxes;
}
public NbtMap toNbtMap() {
return NbtMap.builder()
.putFloat("MinX", min.getX())
.putFloat("MinY", min.getY())
.putFloat("MinZ", min.getZ())
.putFloat("MaxX", max.getX())
.putFloat("MaxY", max.getY())
.putFloat("MaxZ", max.getZ())
.putFloat("PivotX", pivot.getX())
.putFloat("PivotY", pivot.getY())
.putFloat("PivotZ", pivot.getZ())
.build();
}
public static NbtMap toNbtMap(List<Hitbox> hitboxes) {
List<NbtMap> list = new ArrayList<>();
for (Hitbox hitbox : hitboxes) {
if (hitbox instanceof HitboxImpl impl) {
list.add(impl.toNbtMap());
} else {
throw new IllegalArgumentException("Unknown hitbox class implementation: " + hitbox.getClass().getSimpleName());
}
}
return NbtMap.builder().putList("Hitboxes", NbtType.COMPOUND, list).build();
}
public static class Builder implements Hitbox.Builder {
Vector3f min, max, pivot;
@Override
public Hitbox.Builder min(Vector3f min) {
Objects.requireNonNull(min, "min");
this.min = min;
return this;
}
@Override
public Hitbox.Builder max(Vector3f max) {
Objects.requireNonNull(max, "max");
this.max = max;
return this;
}
@Override
public Hitbox.Builder origin(Vector3f pivot) {
Objects.requireNonNull(pivot, "pivot");
this.pivot = pivot;
return this;
}
@Override
public Hitbox build() {
return new HitboxImpl(min == null ? Vector3f.ZERO : min, max == null ? Vector3f.ZERO : max, pivot == null ? Vector3f.ZERO : pivot);
}
}
}

View File

@@ -35,6 +35,8 @@ import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.entity.data.GeyserEntityDataType;
import org.geysermc.geyser.api.entity.data.GeyserListEntityDataType;
import org.geysermc.geyser.api.entity.data.types.Hitbox;
import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
import org.geysermc.geyser.api.entity.definition.JavaEntityType;
import org.geysermc.geyser.api.event.EventRegistrar;
@@ -56,6 +58,8 @@ import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.impl.camera.GeyserCameraFade;
import org.geysermc.geyser.impl.camera.GeyserCameraPosition;
import org.geysermc.geyser.impl.entity.GeyserEntityDataImpl;
import org.geysermc.geyser.impl.entity.GeyserListEntityDataImpl;
import org.geysermc.geyser.impl.entity.HitboxImpl;
import org.geysermc.geyser.item.GeyserCustomItemData;
import org.geysermc.geyser.item.GeyserCustomItemOptions;
import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData;
@@ -119,6 +123,9 @@ public class ProviderRegistryLoader implements RegistryLoader<Map<Class<?>, Prov
providers.put(GeyserEntityDefinition.class, args -> BedrockEntityDefinition.getOrCreate((Identifier) args[0]));
providers.put(JavaEntityType.class, args -> GeyserEntityType.ofVanilla((Identifier) args[0]));
providers.put(GeyserEntityDataType.class, args -> GeyserEntityDataImpl.lookup((Class<?>) args[0], (String) args[1]));
providers.put(GeyserListEntityDataType.class, args -> GeyserListEntityDataImpl.lookup((Class<?>) args[0], (Class<?>) args[1], (String) args[2]));
providers.put(Hitbox.Builder.class, args -> new HitboxImpl.Builder());
return providers;
}

View File

@@ -89,7 +89,6 @@ import java.util.function.Consumer;
public final class EntityUtils {
private static final AtomicInteger RUNTIME_ID_ALLOCATOR = new AtomicInteger(100000);
public static final float PLAYER_ENTITY_OFFSET = 1.62F;
/**
* A constant array of the two hands that a player can interact with an entity.