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.RavagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; 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.living.monster.raid.VindicatorEntity;
import org.geysermc.geyser.entity.type.player.AvatarEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.translator.text.MessageTranslator;
@@ -653,12 +654,18 @@ public final class EntityDefinitions {
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation)
.addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation)
.build(); .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) .type(EntityType.PLAYER)
.height(1.8f).width(0.6f) .height(1.8f).width(0.6f)
.offset(1.62f) .offset(1.62f)
.addTranslator(null) // Player main hand
.addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility)
.addTranslator(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts) .addTranslator(MetadataTypes.FLOAT, PlayerEntity::setAbsorptionHearts)
.addTranslator(null) // Player score .addTranslator(null) // Player score
.addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setLeftParrot) .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.Getter;
import lombok.Setter; import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; 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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; 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.SetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity; 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.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.session.GeyserSession; 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.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 org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt; import java.util.OptionalInt;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Getter @Setter @Getter @Setter
public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { public class PlayerEntity extends AvatarEntity 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;
/** /**
* Saves the parrot currently on the player's left shoulder; otherwise null * 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, 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) { 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); super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw, username);
this.username = username;
this.nametag = username;
this.texturesProperty = texturesProperty; this.texturesProperty = texturesProperty;
} }
@Override @Override
protected void initializeMetadata() { protected void initializeMetadata() {
super.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 :/ // 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. // The nametag does disappear properly when the player is invisible though.
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1);
dirtyMetadata.apply(addPlayerPacket.getMetadata());
setFlagsDirty(false);
valid = true;
session.sendUpstreamPacket(addPlayerPacket);
} }
@Override @Override
@@ -186,30 +110,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override @Override
public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
setPosition(position); super.moveAbsolute(position, yaw, pitch, headYaw, isOnGround, teleported);
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);
}
if (leftParrot != null) { if (leftParrot != null) {
leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported); leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
} }
@@ -220,34 +121,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@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) {
setYaw(yaw); super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
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);
if (leftParrot != null) { if (leftParrot != null) {
leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true); 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) { public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) {
// Extra hearts - is not metadata but an attribute on Bedrock // Extra hearts - is not metadata but an attribute on Bedrock
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
@@ -302,14 +140,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
session.sendUpstreamPacket(attributesPacket); 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) { public void setLeftParrot(EntityMetadata<OptionalInt, ?> entityMetadata) {
setParrot(entityMetadata.getValue(), true); 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 * Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just
* spawns it from the NBT data provided * 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 (variant.isPresent()) {
if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) { if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
// No need to update a parrot's data when it already exists // 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 @Override
public String teamIdentifier() { public String teamIdentifier() {
return username; return username;
} }
// TODO test mannequins
@Override @Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) { protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that // when fromDisplayName, LivingEntity will call scoreboard code. After that
@@ -388,87 +209,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
super.setNametag(nametag, fromDisplayName); 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. * @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.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; 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.property.Properties;
import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.block.type.WallSkullBlock; 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 * A wrapper to handle skulls more effectively - skulls have to be treated as entities since there are no
* custom player skulls in Bedrock. * custom player skulls in Bedrock.
*/ */
public class SkullPlayerEntity extends PlayerEntity { public class SkullPlayerEntity extends AvatarEntity {
@Getter @Getter
private UUID skullUUID; private UUID skullUUID;
@@ -60,7 +61,7 @@ public class SkullPlayerEntity extends PlayerEntity {
private Vector3i skullPosition; private Vector3i skullPosition;
public SkullPlayerEntity(GeyserSession session, long geyserId) { 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 @Override
@@ -74,45 +75,16 @@ public class SkullPlayerEntity extends PlayerEntity {
setFlag(EntityFlag.INVISIBLE, true); // Until the skin is loaded 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) { public void updateSkull(SkullCache.Skull skull) {
skullPosition = skull.getPosition(); 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 // Make skull invisible as we change skins
setFlag(EntityFlag.INVISIBLE, true); setFlag(EntityFlag.INVISIBLE, true);
updateBedrockMetadata(); updateBedrockMetadata();
skullUUID = skull.getUuid(); skullUUID = skull.getUuid();
setTexturesProperty(skull.getTexturesProperty()); texturesProperty = skull.getTexturesProperty();
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
// Delay to minimize split-second "player" pop-in // Delay to minimize split-second "player" pop-in