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 e0e8b94d8..893d0cbdd 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -149,6 +149,7 @@ 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.MannequinEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -239,6 +240,7 @@ public final class EntityDefinitions { public static final EntityDefinition MAGMA_CUBE; public static final EntityDefinition MANGROVE_BOAT; public static final EntityDefinition MANGROVE_CHEST_BOAT; + public static final EntityDefinition MANNEQUIN; public static final EntityDefinition MINECART; public static final EntityDefinition MOOSHROOM; public static final EntityDefinition MULE; @@ -662,10 +664,15 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BYTE, AvatarEntity::setSkinVisibility) .build(); + MANNEQUIN = EntityDefinition.inherited(MannequinEntity::new, avatarEntityBase) + .type(EntityType.MANNEQUIN) + .addTranslator(MetadataTypes.RESOLVABLE_PROFILE, MannequinEntity::setProfile) + .addTranslator(null) // Immovable + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, MannequinEntity::setDescription) + .build(); + PLAYER = EntityDefinition.inherited(null, avatarEntityBase) .type(EntityType.PLAYER) - .height(1.8f).width(0.6f) - .offset(1.62f) .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 index b2fd4f0c8..7239f23b2 100644 --- 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 @@ -44,12 +44,16 @@ 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.skin.SkinManager; +import org.geysermc.geyser.skin.SkullSkinManager; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.ChunkUtils; +import org.geysermc.mcprotocollib.auth.GameProfile; 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.player.ResolvableProfile; import java.util.Collections; import java.util.List; @@ -88,7 +92,7 @@ public class AvatarEntity extends LivingEntity { BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer); } - public AvatarEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, + 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; @@ -223,6 +227,32 @@ public class AvatarEntity extends LivingEntity { return bedPosition; } + public void setSkin(ResolvableProfile profile, boolean cape, Runnable after) { + SkinManager.resolveProfile(profile).thenAccept(resolved -> setSkin(resolved, cape, after)); + } + + public void setSkin(GameProfile profile, boolean cape, Runnable after) { + GameProfile.Property textures = profile.getProperty("textures"); + if (textures != null) { + setSkin(textures.getValue(), cape, after); + } else { + setSkin((String) null, cape, after); + } + } + + public void setSkin(String texturesProperty, boolean cape, Runnable after) { + if (Objects.equals(texturesProperty, this.texturesProperty)) { + return; + } + + this.texturesProperty = texturesProperty; + if (cape) { + SkinManager.requestAndHandleSkinAndCape(this, session, skin -> after.run()); + } else { + SkullSkinManager.requestAndHandleSkin(this, session, skin -> after.run()); + } + } + public void setSkinVisibility(ByteEntityMetadata entityMetadata) { // OptionalPack usage for toggling skin bits // In Java Edition, a bit being set means that part should be enabled diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java new file mode 100644 index 000000000..29edbd623 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/MannequinEntity.java @@ -0,0 +1,51 @@ +/* + * 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.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile; + +import java.util.Optional; +import java.util.UUID; + +public class MannequinEntity extends AvatarEntity { + + public MannequinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw, "Mannequin"); // TODO from translation + } + + public void setProfile(EntityMetadata entityMetadata) { + setSkin(entityMetadata.getValue(), true, () -> {}); + } + + public void setDescription(EntityMetadata, ?> entityMetadata) { + + } +} 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 dbefa0b95..2819d9486 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 @@ -84,13 +84,11 @@ public class SkullPlayerEntity extends AvatarEntity { updateBedrockMetadata(); skullUUID = skull.getUuid(); - texturesProperty = skull.getTexturesProperty(); - - SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { + setSkin(skull.getTexturesProperty(), false, () -> session.scheduleInEventLoop(() -> { // Delay to minimize split-second "player" pop-in setFlag(EntityFlag.INVISIBLE, false); updateBedrockMetadata(); - }, 250, TimeUnit.MILLISECONDS))); + }, 250, TimeUnit.MILLISECONDS)); } else { // Just a rotation/position change setFlag(EntityFlag.INVISIBLE, false); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index d89faf4bb..dd943e984 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -67,6 +67,8 @@ public class SkinManager { .expireAfterAccess(1, TimeUnit.HOURS) .build(); private static final UUID EMPTY_UUID = new UUID(0L, 0L); + public static final GameProfile EMPTY_PROFILE = new GameProfile((UUID) null, null); + public static final ResolvableProfile EMPTY_RESOLVABLE_PROFILE = new ResolvableProfile(EMPTY_PROFILE, null, null, null, null, false); static final String GEOMETRY = new String(FileUtils.readAllBytes("bedrock/geometries/geo.json"), StandardCharsets.UTF_8); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java index 41e4025f1..47fb17197 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java @@ -31,6 +31,7 @@ import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.skin.Skin; import org.geysermc.geyser.api.skin.SkinData; +import org.geysermc.geyser.entity.type.player.AvatarEntity; import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -55,7 +56,7 @@ public class SkullSkinManager extends SkinManager { .build(); } - public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session, + public static void requestAndHandleSkin(AvatarEntity entity, GeyserSession session, Consumer skinConsumer) { BiConsumer applySkin = (skin, throwable) -> { try { @@ -77,11 +78,13 @@ public class SkullSkinManager extends SkinManager { GameProfileData data = GameProfileData.from(entity); if (data == null) { - GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() + - " with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID()); - // No texture available, fallback using the UUID - SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID()); - applySkin.accept(fallback.skin(), null); + if (entity instanceof SkullPlayerEntity skullEntity) { + GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + skullEntity.getSkullPosition() + + " with texture value: " + entity.getTexturesProperty() + " and UUID: " + skullEntity.getSkullUUID()); + // No texture available, fallback using the UUID + SkinData fallback = SkinProvider.determineFallbackSkinData(skullEntity.getSkullUUID()); + applySkin.accept(fallback.skin(), null); + } } else { SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) .whenCompleteAsync(applySkin); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java index 1cb5f7a04..db7ad390b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java @@ -91,11 +91,10 @@ public class JavaPlayerInfoUpdateTranslator extends PacketTranslator - GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); + playerEntity.setSkin(profile, true, + () -> GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); } } }