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; 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 * Contains commonly used {@link GeyserEntityDataType} constants for built-in entity
* metadata fields. * metadata fields.
@@ -48,6 +50,12 @@ public final class GeyserEntityDataTypes {
public static final GeyserEntityDataType<Float> SCALE = public static final GeyserEntityDataType<Float> SCALE =
GeyserEntityDataType.of(Float.class, "scale"); GeyserEntityDataType.of(Float.class, "scale");
/**
* Represents custom hitboxes for entities
*/
public static final GeyserListEntityDataType<Hitbox> HITBOXES =
GeyserListEntityDataType.of(Hitbox.class, "hitboxes");
private GeyserEntityDataTypes() { private GeyserEntityDataTypes() {
// no-op // 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 * The entity position as it is known to the Java server
*/ */
@Accessors(fluent = true) @Accessors(fluent = true)
protected Vector3f position; private Vector3f position;
@Setter(AccessLevel.NONE)
private Vector3f bedrockPosition;
protected Vector3f motion; protected Vector3f motion;
/** /**
@@ -708,8 +710,13 @@ public class Entity implements GeyserEntity {
return this.valid; return this.valid;
} }
public void position(Vector3f position) {
this.position = position;
this.bedrockPosition = position.up(offset);
}
public Vector3f bedrockPosition() { 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; this.offset = offset;
// TODO queue? // TODO queue?
if (isValid()) { if (isValid() && teleport) {
this.moveRelative(0, 0, 0, 0, 0, isOnGround()); this.moveRelative(0, 0, 0, 0, 0, isOnGround());
} }
} }

View File

@@ -64,7 +64,7 @@ public class FireballEntity extends ThrowableEntity {
newPosition = tickMovement(newPosition); newPosition = tickMovement(newPosition);
} }
super.moveAbsoluteImmediate(newPosition, yaw, pitch, headYaw, isOnGround, teleported); super.moveAbsoluteImmediate(newPosition, yaw, pitch, headYaw, isOnGround, teleported);
this.position = javaPosition; position(javaPosition);
this.motion = lastMotion; this.motion = lastMotion;
} }
@@ -73,6 +73,6 @@ public class FireballEntity extends ThrowableEntity {
if (removedInVoid()) { if (removedInVoid()) {
return; 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) { if (!collided) {
super.moveAbsoluteImmediate(javaPosition, yaw, pitch, headYaw, isOnGround, teleported); super.moveAbsoluteImmediate(javaPosition, yaw, pitch, headYaw, isOnGround, teleported);
} else { } 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(); float gravity = getGravity();
motion = motion.down(gravity); 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(); float drag = getDrag();
motion = motion.mul(drag); motion = motion.mul(drag);
@@ -162,7 +162,7 @@ public class FishingHookEntity extends ThrowableEntity {
* @return true if this entity is currently in air. * @return true if this entity is currently in air.
*/ */
protected boolean isInAir() { 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; return block == Block.JAVA_AIR_ID;
} }

View File

@@ -119,7 +119,7 @@ public class InteractionEntity extends Entity {
@Override @Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { 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 @Override
@@ -141,7 +141,7 @@ public class InteractionEntity extends Entity {
setBoundingBoxHeight(Math.min(height.getPrimitiveValue(), 64f)); setBoundingBoxHeight(Math.min(height.getPrimitiveValue(), 64f));
if (secondEntity != null) { 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) { 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.NAME, nametag);
secondEntity.getDirtyMetadata().put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, isNameTagVisible ? (byte) 1 : (byte) 0); 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) { if (!isOnGround() || (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) > 0.00001) {
float gravity = getGravity(); float gravity = getGravity();
motion = motion.down(gravity); 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(); float drag = getDrag();
motion = motion.mul(drag, 0.98f, drag); motion = motion.mul(drag, 0.98f, drag);
} }
@@ -118,7 +118,7 @@ public class ItemEntity extends ThrowableEntity {
this.offset = Math.abs(offset); this.offset = Math.abs(offset);
} }
super.moveAbsoluteImmediate(javaPosition, 0, 0, 0, isOnGround, teleported); 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()) waterLevel = session.getGeyser().getWorldManager().getBlockAtAsync(session, javaPosition.getFloorX(), javaPosition.getFloorY(), javaPosition.getFloorZ())
.thenApply(BlockStateValues::getWaterLevel); .thenApply(BlockStateValues::getWaterLevel);
@@ -137,7 +137,7 @@ public class ItemEntity extends ThrowableEntity {
@Override @Override
protected float getDrag() { protected float getDrag() {
if (isOnGround()) { if (isOnGround()) {
Vector3i groundBlockPos = position.toInt().down(); Vector3i groundBlockPos = position().toInt().down();
BlockState blockState = session.getGeyser().getWorldManager().blockAt(session, groundBlockPos); BlockState blockState = session.getGeyser().getWorldManager().blockAt(session, groundBlockPos);
return BlockStateValues.getSlipperiness(blockState) * 0.98f; return BlockStateValues.getSlipperiness(blockState) * 0.98f;
} }

View File

@@ -80,7 +80,7 @@ public class ItemFrameEntity extends HangingEntity {
super(context); super(context);
blockDefinition = buildBlockDefinition(Direction.SOUTH); // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary 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); session.getItemFrameCache().put(bedrockPosition, this);
} }

View File

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

View File

@@ -194,7 +194,7 @@ public class MinecartEntity extends Entity implements Tickable {
} }
private void updateCompletedStep() { 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 @Override

View File

@@ -92,7 +92,7 @@ public class PaintingEntity extends HangingEntity {
valid = true; valid = true;
session.getGeyser().getLogger().debug("Spawned painting on " + position); session.getGeyser().getLogger().debug("Spawned painting on " + position());
} }
@Override @Override
@@ -101,7 +101,7 @@ public class PaintingEntity extends HangingEntity {
} }
private Vector3f fixOffset(PaintingType paintingName) { private Vector3f fixOffset(PaintingType paintingName) {
Vector3f position = super.position; Vector3f position = position();
// ViaVersion already adds the offset for us on older versions, // ViaVersion already adds the offset for us on older versions,
// so no need to do it then otherwise it will be spaced // so no need to do it then otherwise it will be spaced
if (session.isEmulatePost1_18Logic()) { 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 the line count changed, update the position to account for the new offset
if (previousLineCount != lineCount) { 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) { public ThrowableEntity(EntitySpawnContext context) {
super(context); super(context);
this.lastJavaPosition = position; this.lastJavaPosition = position();
} }
/** /**
@@ -55,7 +55,7 @@ public class ThrowableEntity extends Entity implements Tickable {
if (removedInVoid()) { if (removedInVoid()) {
return; return;
} }
moveAbsoluteImmediate(position.add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false); moveAbsoluteImmediate(position().add(motion), getYaw(), getPitch(), getHeadYaw(), isOnGround(), false);
float drag = getDrag(); float drag = getDrag();
float gravity = getGravity(); float gravity = getGravity();
motion = motion.mul(drag).down(gravity); motion = motion.mul(drag).down(gravity);
@@ -75,15 +75,15 @@ public class ThrowableEntity extends Entity implements Tickable {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING); 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.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
moveEntityDeltaPacket.setX(javaPosition.getX()); moveEntityDeltaPacket.setX(javaPosition.getX());
} }
if (this.position.getY() != javaPosition.getY()) { if (this.position().getY() != javaPosition.getY()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
moveEntityDeltaPacket.setY(javaPosition.getY() + offset); 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.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(javaPosition.getZ()); moveEntityDeltaPacket.setZ(javaPosition.getZ());
} }
@@ -155,7 +155,7 @@ public class ThrowableEntity extends Entity implements Tickable {
* @return true if this entity is currently in water. * @return true if this entity is currently in water.
*/ */
protected boolean isInWater() { 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; return BlockStateValues.getWaterLevel(block) != -1;
} }
@@ -173,7 +173,7 @@ public class ThrowableEntity extends Entity implements Tickable {
@Override @Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { 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); moveAbsoluteImmediate(lastJavaPosition.add(relX, relY, relZ), yaw, pitch, headYaw, isOnGround, false);
lastJavaPosition = position; lastJavaPosition = position();
} }
@Override @Override
@@ -188,7 +188,7 @@ public class ThrowableEntity extends Entity implements Tickable {
* @return true if the entity was removed * @return true if the entity was removed
*/ */
public boolean removedInVoid() { public boolean removedInVoid() {
if (position.getY() < session.getDimensionType().minY() - 64) { if (position().getY() < session.getDimensionType().minY() - 64) {
session.getEntityCache().removeEntity(this); session.getEntityCache().removeEntity(this);
return true; return true;
} }

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ public class OcelotEntity extends AnimalEntity {
@NonNull @NonNull
@Override @Override
protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Attempt to feed
return InteractiveTag.FEED; return InteractiveTag.FEED;
} else { } else {
@@ -63,7 +63,7 @@ public class OcelotEntity extends AnimalEntity {
@NonNull @NonNull
@Override @Override
protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 // Attempt to feed
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
} else { } 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 // The java client renders digging particles on its own, but bedrock does not
if (digTicks > 0 && --digTicks < DIG_START && digTicks % 5 == 0) { if (digTicks > 0 && --digTicks < DIG_START && digTicks % 5 == 0) {
Vector3f rot = Vector3f.createDirectionDeg(0, -getYaw()).mul(2.25f); 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())); int blockId = session.getBlockMappings().getBedrockBlockId(session.getGeyser().getWorldManager().getBlockAt(session, pos.toInt().down()));
LevelEventPacket levelEventPacket = new LevelEventPacket(); LevelEventPacket levelEventPacket = new LevelEventPacket();

View File

@@ -149,12 +149,12 @@ public class VillagerEntity extends AbstractMerchantEntity {
setPitch(pitch); setPitch(pitch);
setHeadYaw(headYaw); setHeadYaw(headYaw);
setOnGround(isOnGround); 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(); MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
moveEntityPacket.setRuntimeEntityId(geyserId); moveEntityPacket.setRuntimeEntityId(geyserId);
moveEntityPacket.setRotation(Vector3f.from(0, 0, bedRotation)); 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.setOnGround(isOnGround);
moveEntityPacket.setTeleported(false); moveEntityPacket.setTeleported(false);
session.sendUpstreamPacket(moveEntityPacket); session.sendUpstreamPacket(moveEntityPacket);

View File

@@ -134,7 +134,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
for (int i = 0; i < segmentHistory.length; i++) { for (int i = 0; i < segmentHistory.length; i++) {
segmentHistory[i] = new Segment(); segmentHistory[i] = new Segment();
segmentHistory[i].yaw = getHeadYaw(); 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 // Send updated positions
for (EnderDragonPartEntity part : allParts) { 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 xOffset = 8f * (random.nextFloat() - 0.5f);
float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f; float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f;
float zOffset = 8f * (random.nextFloat() - 0.5f); 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(); LevelEventPacket particlePacket = new LevelEventPacket();
particlePacket.setType(ParticleType.EXPLODE); particlePacket.setType(ParticleType.EXPLODE);
particlePacket.setPosition(particlePos); particlePacket.setPosition(particlePos);
@@ -311,7 +311,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable {
private void pushSegment() { private void pushSegment() {
latestSegment = (latestSegment + 1) % segmentHistory.length; latestSegment = (latestSegment + 1) % segmentHistory.length;
segmentHistory[latestSegment].yaw = getHeadYaw(); 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.AddPlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.geysermc.geyser.entity.spawn.EntitySpawnContext; 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.entity.type.LivingEntity;
import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.skin.SkinManager; import org.geysermc.geyser.skin.SkinManager;
@@ -162,7 +161,7 @@ public class AvatarEntity extends LivingEntity {
setYaw(yaw); setYaw(yaw);
setPitch(pitch); setPitch(pitch);
setHeadYaw(headYaw); 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); 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 // If the player is moved while sleeping, we have to adjust their y, so it appears
// correctly on Bedrock. This fixes GSit's lay. // correctly on Bedrock. This fixes GSit's lay.
if (getFlag(EntityFlag.SLEEPING)) { 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 // 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); movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
} }
} }
@@ -190,7 +189,7 @@ public class AvatarEntity extends LivingEntity {
} }
@Override @Override
public Entity position(Vector3f position) { public void position(Vector3f position) {
if (this.bedPosition != null) { if (this.bedPosition != null) {
// As of Bedrock 1.21.22 and Fabric 1.21.1 // As of Bedrock 1.21.22 and Fabric 1.21.1
// Messes with Bedrock if we send this to the client itself, though. // Messes with Bedrock if we send this to the client itself, though.
@@ -198,7 +197,6 @@ public class AvatarEntity extends LivingEntity {
} else { } else {
super.position(position); super.position(position);
} }
return this;
} }
@Override @Override
@@ -316,7 +314,7 @@ public class AvatarEntity extends LivingEntity {
if (pose == Pose.SWIMMING) { if (pose == Pose.SWIMMING) {
// This is just for, so we know if player is swimming or crawling. // 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); setFlag(EntityFlag.SWIMMING, true);
} else { } else {
setFlag(EntityFlag.CRAWLING, true); setFlag(EntityFlag.CRAWLING, true);

View File

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

View File

@@ -163,11 +163,11 @@ public class SessionPlayerEntity extends PlayerEntity {
@Override @Override
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { 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); super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
session.getCollisionManager().updatePlayerBoundingBox(this.position); session.getCollisionManager().updatePlayerBoundingBox(position());
} }
@Override @Override
public Entity position(Vector3f position) { public void position(Vector3f position) {
if (valid) { // Don't update during session init if (valid) { // Don't update during session init
session.getCollisionManager().updatePlayerBoundingBox(position); session.getCollisionManager().updatePlayerBoundingBox(position);
@@ -175,8 +175,7 @@ public class SessionPlayerEntity extends PlayerEntity {
session.setNoClip(false); session.setNoClip(false);
} }
} }
this.position = position; super.position(position);
return this;
} }
/** /**
@@ -212,10 +211,10 @@ public class SessionPlayerEntity extends PlayerEntity {
* Set the player's position from a position sent in a Bedrock packet * Set the player's position from a position sent in a Bedrock packet
*/ */
public void setPositionFromBedrock(Vector3f position) { 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. // 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); session.setNoClip(false);
} }
} }
@@ -497,7 +496,7 @@ public class SessionPlayerEntity extends PlayerEntity {
if (session.getGameMode() == GameMode.SPECTATOR) { if (session.getGameMode() == GameMode.SPECTATOR) {
return false; 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)) { if (state.block().is(session, BlockTag.CLIMBABLE)) {
return true; return true;
} }
@@ -506,7 +505,7 @@ public class SessionPlayerEntity extends PlayerEntity {
if (!state.getValue(Properties.OPEN)) { if (!state.getValue(Properties.OPEN)) {
return false; return false;
} else { } 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); 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 @Override
protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) { protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) {
Vector3f bedrockPos = javaPos.toFloat(); Vector3f oldPosition = vehicle.position();
vehicle.position(javaPos.toFloat());
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId()); moveEntityDeltaPacket.setRuntimeEntityId(vehicle.geyserId());
if (vehicle.isOnGround()) { if (vehicle.isOnGround()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); 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.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.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.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(bedrockPos.getZ()); moveEntityDeltaPacket.setZ(vehicle.bedrockPosition().getZ());
} }
vehicle.setPosition(bedrockPos);
if (vehicle.getPitch() != lastRotation.getX()) { if (vehicle.getPitch() != lastRotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
@@ -175,7 +175,7 @@ public class BoatVehicleComponent extends VehicleComponent<BoatEntity> {
} }
if (!moveEntityDeltaPacket.getFlags().isEmpty()) { if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket); vehicle.getSession().sendUpstreamPacketImmediately(moveEntityDeltaPacket);
} }
ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw() - 90, vehicle.getPitch(), vehicle.isOnGround()); 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) * @param lastRotation the previous rotation of the vehicle (pitch, yaw, headYaw)
*/ */
protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) { protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) {
Vector3f bedrockPos = javaPos.toFloat(); Vector3f oldPosition = vehicle.position();
vehicle.position(javaPos.toFloat());
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
moveEntityDeltaPacket.setRuntimeEntityId(vehicle.geyserId()); moveEntityDeltaPacket.setRuntimeEntityId(vehicle.geyserId());
@@ -747,19 +748,18 @@ public class VehicleComponent<T extends Entity & ClientVehicle> {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); 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.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.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.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
moveEntityDeltaPacket.setZ(bedrockPos.getZ()); moveEntityDeltaPacket.setZ(vehicle.bedrockPosition().getZ());
} }
vehicle.position(bedrockPos);
if (vehicle.getPitch() != lastRotation.getX()) { if (vehicle.getPitch() != lastRotation.getX()) {
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); 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)); TYPES.put("scale", new GeyserEntityDataImpl<>(Float.class, "scale", EntityDataTypes.SCALE));
// "custom" // "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) { 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.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.entity.data.GeyserEntityDataType; 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.GeyserEntityDefinition;
import org.geysermc.geyser.api.entity.definition.JavaEntityType; import org.geysermc.geyser.api.entity.definition.JavaEntityType;
import org.geysermc.geyser.api.event.EventRegistrar; 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.GeyserCameraFade;
import org.geysermc.geyser.impl.camera.GeyserCameraPosition; import org.geysermc.geyser.impl.camera.GeyserCameraPosition;
import org.geysermc.geyser.impl.entity.GeyserEntityDataImpl; 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.GeyserCustomItemData;
import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserCustomItemOptions;
import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; 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(GeyserEntityDefinition.class, args -> BedrockEntityDefinition.getOrCreate((Identifier) args[0]));
providers.put(JavaEntityType.class, args -> GeyserEntityType.ofVanilla((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(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; return providers;
} }

View File

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