From ef55f76f0dd8c3d9af4e28cf7e1a065a9a64fd85 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Mon, 29 Sep 2025 13:12:02 +0000 Subject: [PATCH] Start working on mannequins --- .../geyser/entity/EntityDefinitions.java | 13 +- .../entity/type/player/AvatarEntity.java | 321 ++++++++++++++++++ .../entity/type/player/PlayerEntity.java | 272 +-------------- .../entity/type/player/SkullPlayerEntity.java | 38 +-- 4 files changed, 342 insertions(+), 302 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index ef5a9bf99..dbd880adc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -148,6 +148,7 @@ import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity import org.geysermc.geyser.entity.type.living.monster.raid.RavagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.VindicatorEntity; +import org.geysermc.geyser.entity.type.player.AvatarEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -653,12 +654,18 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .build(); - PLAYER = EntityDefinition.inherited(null, livingEntityBase) + + EntityDefinition avatarEntityBase = EntityDefinition.inherited(AvatarEntity::new, livingEntityBase) + .height(1.8f).width(0.6f) + .offset(1.62f) + .addTranslator(null) // Player main hand + .addTranslator(MetadataTypes.BYTE, AvatarEntity::setSkinVisibility) + .build(); + + PLAYER = EntityDefinition.inherited(null, avatarEntityBase) .type(EntityType.PLAYER) .height(1.8f).width(0.6f) .offset(1.62f) - .addTranslator(null) // Player main hand - .addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility) .addTranslator(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts) .addTranslator(null) // Player score .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setLeftParrot) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java new file mode 100644 index 000000000..c41f5362a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.player; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.Ability; +import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; +import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +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.EntityDefinition; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.item.ItemTranslator; +import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public class AvatarEntity extends LivingEntity { + public static final float SNEAKING_POSE_HEIGHT = 1.5f; + protected static final List BASE_ABILITY_LAYER; + + protected String username; + + /** + * The textures property from the GameProfile. + */ + @Nullable + protected String texturesProperty; + + private String cachedScore = ""; + private boolean scoreVisible = true; + + @Nullable + private Vector3i bedPosition; + + static { + AbilityLayer abilityLayer = new AbilityLayer(); + abilityLayer.setLayerType(AbilityLayer.Type.BASE); + Ability[] abilities = Ability.values(); + Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with + Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with + BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); + } + + public AvatarEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, + Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + this.username = username; + this.nametag = username; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior + dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff); + } + + @Override + public void spawnEntity() { + AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); + addPlayerPacket.setUuid(uuid); + addPlayerPacket.setUsername(username); + addPlayerPacket.setRuntimeEntityId(geyserId); + addPlayerPacket.setUniqueEntityId(geyserId); + addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); + addPlayerPacket.setRotation(getBedrockRotation()); + addPlayerPacket.setMotion(motion); + addPlayerPacket.setHand(ItemTranslator.translateToBedrock(session, getMainHandItem())); + addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); + addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); + addPlayerPacket.setDeviceId(""); + addPlayerPacket.setPlatformChatId(""); + addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO + addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing + addPlayerPacket.getMetadata().putFlags(flags); + dirtyMetadata.apply(addPlayerPacket.getMetadata()); + + setFlagsDirty(false); + + valid = true; + session.sendUpstreamPacket(addPlayerPacket); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + setPosition(position); + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + + setOnGround(isOnGround); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(this.position); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setOnGround(isOnGround); + movePlayerPacket.setMode(this instanceof SessionPlayerEntity || teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); + if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + } + + session.sendUpstreamPacket(movePlayerPacket); + + if (teleported && !(this instanceof SessionPlayerEntity)) { + // As of 1.19.0, head yaw seems to be ignored during teleports, also don't do this for session player. + updateHeadLookRotation(headYaw); + } + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + setYaw(yaw); + setPitch(pitch); + setHeadYaw(headYaw); + this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); + + setOnGround(isOnGround); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(geyserId); + movePlayerPacket.setPosition(position); + movePlayerPacket.setRotation(getBedrockRotation()); + movePlayerPacket.setOnGround(isOnGround); + movePlayerPacket.setMode(this instanceof SessionPlayerEntity ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); + // 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)) { + // Force the player movement by using a teleport + movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ())); + movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); + } + } + + if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { + movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); + } + + session.sendUpstreamPacket(movePlayerPacket); + } + + @Override + public void setPosition(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. + super.setPosition(position.up(0.2f)); + } else { + super.setPosition(position.add(0, definition.offset(), 0)); + } + } + + @Override + public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) { + bedPosition = super.setBedPosition(entityMetadata); + if (bedPosition != null) { + // Required to sync position of entity to bed + // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper + this.setPosition(bedPosition.toFloat()); + + // TODO evaluate if needed + int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition); + // Bed has to be updated, or else player is floating in the air + ChunkUtils.updateBlock(session, bed, bedPosition); + + // Indicate that the player should enter the sleep cycle + // Has to be a byte or it does not work + // (Bed position is what actually triggers sleep - "pose" is only optional) + dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2); + } else { + // Player is no longer sleeping + dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0); + return null; + } + return bedPosition; + } + + public void setSkinVisibility(ByteEntityMetadata entityMetadata) { + // OptionalPack usage for toggling skin bits + // In Java Edition, a bit being set means that part should be enabled + // However, to ensure that the pack still works on other servers, we invert the bit so all values by default + // are true (0). + dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff); + } + + @Override + public String getDisplayName() { + return username; + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // Doesn't do anything for players + // TODO test mannequins + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Doesn't do anything for players + // TODO test mannequins + } + + public void setBelowNameText(String text) { + if (text == null) { + text = ""; + } + + boolean changed = !Objects.equals(cachedScore, text); + cachedScore = text; + if (scoreVisible && changed) { + dirtyMetadata.put(EntityDataTypes.SCORE, text); + } + } + + @Override + protected void scoreVisibility(boolean show) { + boolean visibilityChanged = scoreVisible != show; + scoreVisible = show; + if (!visibilityChanged) { + return; + } + // if the player has no cachedScore, we never have to change the score. + // hide = set to "" (does nothing), show = change from "" (does nothing) + if (cachedScore.isEmpty()) { + return; + } + dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : ""); + } + + @Override + public void setPose(Pose pose) { + super.setPose(pose); + setFlag(EntityFlag.SWIMMING, false); + setFlag(EntityFlag.CRAWLING, false); + + if (pose == Pose.SWIMMING) { + // This is just for, so we know if player is swimming or crawling. + // TODO test, changed from position (field) to position() (method), which adds offset + if (session.getGeyser().getWorldManager().blockAt(session, position.toInt()).is(Blocks.WATER)) { + setFlag(EntityFlag.SWIMMING, true); + } else { + setFlag(EntityFlag.CRAWLING, true); + // Look at https://github.com/GeyserMC/Geyser/issues/5316, we're fixing this by spoofing player pitch to 0. + updateRotation(this.yaw, 0, this.onGround); + } + } + } + + @Override + public void setPitch(float pitch) { + super.setPitch(getFlag(EntityFlag.CRAWLING) ? 0 : pitch); + } + + @Override + public void setDimensionsFromPose(Pose pose) { + float height; + float width; + switch (pose) { + case SNEAKING -> { + height = SNEAKING_POSE_HEIGHT; + width = definition.width(); + } + case FALL_FLYING, SPIN_ATTACK, SWIMMING -> { + height = 0.6f; + width = definition.width(); + } + case DYING -> { + height = 0.2f; + width = 0.2f; + } + default -> { + super.setDimensionsFromPose(pose); + return; + } + } + setBoundingBoxWidth(width); + setBoundingBoxHeight(height); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 660c9152b..75614454c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -27,73 +27,28 @@ package org.geysermc.geyser.entity.type.player; import lombok.Getter; import lombok.Setter; -import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.math.vector.Vector3i; -import org.cloudburstmc.protocol.bedrock.data.Ability; -import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; -import org.cloudburstmc.protocol.bedrock.data.GameType; -import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; -import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; -import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; -import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; -import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.item.ItemTranslator; -import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; import java.util.concurrent.TimeUnit; @Getter @Setter -public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { - public static final float SNEAKING_POSE_HEIGHT = 1.5f; - protected static final List BASE_ABILITY_LAYER; - - static { - AbilityLayer abilityLayer = new AbilityLayer(); - abilityLayer.setLayerType(AbilityLayer.Type.BASE); - Ability[] abilities = Ability.values(); - Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with - Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with - BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); - } - - private String username; - - private String cachedScore = ""; - private boolean scoreVisible = true; - - /** - * The textures property from the GameProfile. - */ - @Nullable - private String texturesProperty; - - @Nullable - private Vector3i bedPosition; +public class PlayerEntity extends AvatarEntity implements GeyserPlayerEntity { /** * Saves the parrot currently on the player's left shoulder; otherwise null @@ -111,48 +66,17 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { public PlayerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username, @Nullable String texturesProperty) { - super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw); - - this.username = username; - this.nametag = username; + super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw, username); this.texturesProperty = texturesProperty; } @Override protected void initializeMetadata() { super.initializeMetadata(); - // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior - dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, 0xff); - } - - @Override - public void spawnEntity() { - AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); - addPlayerPacket.setUuid(uuid); - addPlayerPacket.setUsername(username); - addPlayerPacket.setRuntimeEntityId(geyserId); - addPlayerPacket.setUniqueEntityId(geyserId); - addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); - addPlayerPacket.setRotation(getBedrockRotation()); - addPlayerPacket.setMotion(motion); - addPlayerPacket.setHand(ItemTranslator.translateToBedrock(session, getMainHandItem())); - addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); - addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); - addPlayerPacket.setDeviceId(""); - addPlayerPacket.setPlatformChatId(""); - addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO - addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing - addPlayerPacket.getMetadata().putFlags(flags); // Since 1.20.60, the nametag does not show properly if this is not set :/ // The nametag does disappear properly when the player is invisible though. dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); - dirtyMetadata.apply(addPlayerPacket.getMetadata()); - - setFlagsDirty(false); - - valid = true; - session.sendUpstreamPacket(addPlayerPacket); } @Override @@ -186,30 +110,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { @Override public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { - setPosition(position); - setYaw(yaw); - setPitch(pitch); - setHeadYaw(headYaw); - - setOnGround(isOnGround); - - MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); - movePlayerPacket.setRuntimeEntityId(geyserId); - movePlayerPacket.setPosition(this.position); - movePlayerPacket.setRotation(getBedrockRotation()); - movePlayerPacket.setOnGround(isOnGround); - movePlayerPacket.setMode(this instanceof SessionPlayerEntity || teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); - if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { - movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); - } - - session.sendUpstreamPacket(movePlayerPacket); - - if (teleported && !(this instanceof SessionPlayerEntity)) { - // As of 1.19.0, head yaw seems to be ignored during teleports, also don't do this for session player. - updateHeadLookRotation(headYaw); - } - + super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported); if (leftParrot != null) { leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported); } @@ -220,34 +121,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { - setYaw(yaw); - setPitch(pitch); - setHeadYaw(headYaw); - this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ); - - setOnGround(isOnGround); - - MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); - movePlayerPacket.setRuntimeEntityId(geyserId); - movePlayerPacket.setPosition(position); - movePlayerPacket.setRotation(getBedrockRotation()); - movePlayerPacket.setOnGround(isOnGround); - movePlayerPacket.setMode(this instanceof SessionPlayerEntity ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL); - // 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)) { - // Force the player movement by using a teleport - movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ())); - movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT); - } - } - - if (movePlayerPacket.getMode() == MovePlayerPacket.Mode.TELEPORT) { - movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR); - } - - session.sendUpstreamPacket(movePlayerPacket); + super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); if (leftParrot != null) { leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true); } @@ -256,42 +130,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { } } - @Override - public void setPosition(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. - super.setPosition(position.up(0.2f)); - } else { - super.setPosition(position.add(0, definition.offset(), 0)); - } - } - - @Override - public @Nullable Vector3i setBedPosition(EntityMetadata, ?> entityMetadata) { - bedPosition = super.setBedPosition(entityMetadata); - if (bedPosition != null) { - // Required to sync position of entity to bed - // Fixes https://github.com/GeyserMC/Geyser/issues/3595 on vanilla 1.19.3 servers - did not happen on Paper - this.setPosition(bedPosition.toFloat()); - - // TODO evaluate if needed - int bed = session.getGeyser().getWorldManager().getBlockAt(session, bedPosition); - // Bed has to be updated, or else player is floating in the air - ChunkUtils.updateBlock(session, bed, bedPosition); - - // Indicate that the player should enter the sleep cycle - // Has to be a byte or it does not work - // (Bed position is what actually triggers sleep - "pose" is only optional) - dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 2); - } else { - // Player is no longer sleeping - dirtyMetadata.put(EntityDataTypes.PLAYER_FLAGS, (byte) 0); - return null; - } - return bedPosition; - } - public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) { // Extra hearts - is not metadata but an attribute on Bedrock UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); @@ -302,14 +140,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { session.sendUpstreamPacket(attributesPacket); } - public void setSkinVisibility(ByteEntityMetadata entityMetadata) { - // OptionalPack usage for toggling skin bits - // In Java Edition, a bit being set means that part should be enabled - // However, to ensure that the pack still works on other servers, we invert the bit so all values by default - // are true (0). - dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff); - } - public void setLeftParrot(EntityMetadata entityMetadata) { setParrot(entityMetadata.getValue(), true); } @@ -322,7 +152,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { * Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just * spawns it from the NBT data provided */ - protected void setParrot(OptionalInt variant, boolean isLeft) { // TODO test this as of 1.21.9 + protected void setParrot(OptionalInt variant, boolean isLeft) { if (variant.isPresent()) { if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) { // No need to update a parrot's data when it already exists @@ -362,21 +192,12 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { } } - @Override - public String getDisplayName() { - return username; - } - - @Override - public void setDisplayName(EntityMetadata, ?> entityMetadata) { - // Doesn't do anything for players - } - @Override public String teamIdentifier() { return username; } + // TODO test mannequins @Override protected void setNametag(@Nullable String nametag, boolean fromDisplayName) { // when fromDisplayName, LivingEntity will call scoreboard code. After that @@ -388,87 +209,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { super.setNametag(nametag, fromDisplayName); } - @Override - public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { - // Doesn't do anything for players - } - - public void setBelowNameText(String text) { - if (text == null) { - text = ""; - } - - boolean changed = !Objects.equals(cachedScore, text); - cachedScore = text; - if (isScoreVisible() && changed) { - dirtyMetadata.put(EntityDataTypes.SCORE, text); - } - } - - @Override - protected void scoreVisibility(boolean show) { - boolean visibilityChanged = scoreVisible != show; - scoreVisible = show; - if (!visibilityChanged) { - return; - } - // if the player has no cachedScore, we never have to change the score. - // hide = set to "" (does nothing), show = change from "" (does nothing) - if (cachedScore.isEmpty()) { - return; - } - dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : ""); - } - - @Override - public void setPose(Pose pose) { - super.setPose(pose); - setFlag(EntityFlag.SWIMMING, false); - setFlag(EntityFlag.CRAWLING, false); - - if (pose == Pose.SWIMMING) { - // This is just for, so we know if player is swimming or crawling. - if (session.getGeyser().getWorldManager().blockAt(session, this.position().toInt()).is(Blocks.WATER)) { - setFlag(EntityFlag.SWIMMING, true); - } else { - setFlag(EntityFlag.CRAWLING, true); - // Look at https://github.com/GeyserMC/Geyser/issues/5316, we're fixing this by spoofing player pitch to 0. - updateRotation(this.yaw, 0, this.onGround); - } - } - } - - @Override - public void setPitch(float pitch) { - super.setPitch(getFlag(EntityFlag.CRAWLING) ? 0 : pitch); - } - - @Override - public void setDimensionsFromPose(Pose pose) { - float height; - float width; - switch (pose) { - case SNEAKING -> { - height = SNEAKING_POSE_HEIGHT; - width = definition.width(); - } - case FALL_FLYING, SPIN_ATTACK, SWIMMING -> { - height = 0.6f; - width = definition.width(); - } - case DYING -> { - height = 0.2f; - width = 0.2f; - } - default -> { - super.setDimensionsFromPose(pose); - return; - } - } - setBoundingBoxWidth(width); - setBoundingBoxHeight(height); - } - /** * @return the UUID that should be used when dealing with Bedrock's tab list. */ diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java index fb36e3462..dbefa0b95 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java @@ -34,6 +34,7 @@ import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; +import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.level.block.property.Properties; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.WallSkullBlock; @@ -51,7 +52,7 @@ import java.util.concurrent.TimeUnit; * A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no * custom player skulls in Bedrock. */ -public class SkullPlayerEntity extends PlayerEntity { +public class SkullPlayerEntity extends AvatarEntity { @Getter private UUID skullUUID; @@ -60,7 +61,7 @@ public class SkullPlayerEntity extends PlayerEntity { private Vector3i skullPosition; public SkullPlayerEntity(GeyserSession session, long geyserId) { - super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); + super(session, 0, geyserId, UUID.randomUUID(), EntityDefinitions.PLAYER, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, ""); } @Override @@ -74,45 +75,16 @@ public class SkullPlayerEntity extends PlayerEntity { setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded } - /** - * Overwritten so each entity doesn't check for a linked entity - */ - @Override - public void spawnEntity() { - AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); - addPlayerPacket.setUuid(getUuid()); - addPlayerPacket.setUsername(getUsername()); - addPlayerPacket.setRuntimeEntityId(geyserId); - addPlayerPacket.setUniqueEntityId(geyserId); - addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0)); - addPlayerPacket.setRotation(getBedrockRotation()); - addPlayerPacket.setMotion(motion); - addPlayerPacket.setHand(ItemTranslator.translateToBedrock(session, getMainHandItem())); - addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.ANY); - addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER); - addPlayerPacket.setDeviceId(""); - addPlayerPacket.setPlatformChatId(""); - addPlayerPacket.setGameType(GameType.SURVIVAL); - addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); - addPlayerPacket.getMetadata().putFlags(flags); - dirtyMetadata.apply(addPlayerPacket.getMetadata()); - - setFlagsDirty(false); - - valid = true; - session.sendUpstreamPacket(addPlayerPacket); - } - public void updateSkull(SkullCache.Skull skull) { skullPosition = skull.getPosition(); - if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) { + if (!Objects.equals(skull.getTexturesProperty(), texturesProperty) || !Objects.equals(skullUUID, skull.getUuid())) { // Make skull invisible as we change skins setFlag(EntityFlag.INVISIBLE, true); updateBedrockMetadata(); skullUUID = skull.getUuid(); - setTexturesProperty(skull.getTexturesProperty()); + texturesProperty = skull.getTexturesProperty(); SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { // Delay to minimize split-second "player" pop-in