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

Start working on mannequins

This commit is contained in:
Eclipse
2025-09-29 13:12:02 +00:00
parent d94bd7c806
commit ef55f76f0d
4 changed files with 342 additions and 302 deletions

View File

@@ -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.<PlayerEntity>inherited(null, livingEntityBase)
EntityDefinition<AvatarEntity> 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.<PlayerEntity>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)

View File

@@ -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<AbilityLayer> 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<? extends AvatarEntity> 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<Optional<Vector3i>, ?> 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<Optional<Component>, ?> 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);
}
}

View File

@@ -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<AbilityLayer> 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<Optional<Vector3i>, ?> 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<OptionalInt, ?> 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<Optional<Component>, ?> 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.
*/

View File

@@ -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