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 c2af2e36b..369436b21 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 @@ -26,17 +26,20 @@ package org.geysermc.geyser.entity.type.player; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.GameType; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.data.command.CommandPermission; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket; +import lombok.Getter; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkullSkinManager; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -46,6 +49,12 @@ import java.util.concurrent.TimeUnit; */ public class SkullPlayerEntity extends PlayerEntity { + @Getter + private UUID skullUUID; + + @Getter + private Vector3i skullPosition; + public SkullPlayerEntity(GeyserSession session, long geyserId) { super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null); } @@ -102,11 +111,14 @@ public class SkullPlayerEntity extends PlayerEntity { } public void updateSkull(SkullCache.Skull skull) { - if (!skull.getTexturesProperty().equals(getTexturesProperty())) { + skullPosition = skull.getPosition(); + + if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) { // Make skull invisible as we change skins setFlag(EntityFlag.INVISIBLE, true); updateBedrockMetadata(); + skullUUID = skull.getUuid(); setTexturesProperty(skull.getTexturesProperty()); SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> { diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index bd39143c4..96060fe08 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -78,8 +78,9 @@ public class SkullCache { this.skullRenderDistanceSquared = distance * distance; } - public Skull putSkull(Vector3i position, String texturesProperty, int blockState) { + public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) { Skull skull = skulls.computeIfAbsent(position, Skull::new); + skull.uuid = uuid; if (!texturesProperty.equals(skull.texturesProperty)) { skull.texturesProperty = texturesProperty; skull.skinHash = null; @@ -147,7 +148,7 @@ public class SkullCache { public Skull updateSkull(Vector3i position, int blockState) { Skull skull = skulls.get(position); if (skull != null) { - putSkull(position, skull.texturesProperty, blockState); + putSkull(position, skull.uuid, skull.texturesProperty, blockState); } return skull; } @@ -266,6 +267,7 @@ public class SkullCache { @RequiredArgsConstructor @Data public static class Skull { + private UUID uuid; private String texturesProperty; private String skinHash; 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 e9c3db10e..f77d59bd4 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.auth.BedrockClientData; import org.geysermc.geyser.text.GeyserLocale; @@ -69,7 +70,7 @@ public class SkinManager { // The server either didn't have a texture to send, or we didn't have the texture ID cached. // Let's see if this player is a Bedrock player, and if so, let's pull their skin. // Otherwise, grab the default player skin - SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity); + SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid()); if (skin == null) { skin = fallbackSkinData.skin(); geometry = fallbackSkinData.geometry(); @@ -255,24 +256,28 @@ public class SkinManager { * @return The built GameProfileData */ public static @Nullable GameProfileData from(PlayerEntity entity) { - try { - String texturesProperty = entity.getTexturesProperty(); + String texturesProperty = entity.getTexturesProperty(); + if (texturesProperty == null) { + // Likely offline mode + return null; + } - if (texturesProperty == null) { - // Likely offline mode - return null; - } + try { return loadFromJson(texturesProperty); - } catch (IOException exception) { - GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername()); + } catch (Exception exception) { + if (entity instanceof SkullPlayerEntity skullEntity) { + GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty); + } else { + GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty); + } if (GeyserImpl.getInstance().getConfig().isDebugMode()) { exception.printStackTrace(); } - return null; } + return null; } - public static GameProfileData loadFromJson(String encodedJson) throws IOException { + public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException { JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8)); JsonNode textures = skinObject.get("textures"); @@ -285,14 +290,23 @@ public class SkinManager { return null; } - String skinUrl = skinTexture.get("url").asText().replace("http://", "https://"); + String skinUrl; + JsonNode skinUrlNode = skinTexture.get("url"); + if (skinUrlNode != null && skinUrlNode.isTextual()) { + skinUrl = skinUrlNode.asText().replace("http://", "https://"); + } else { + return null; + } boolean isAlex = skinTexture.has("metadata"); String capeUrl = null; JsonNode capeTexture = textures.get("CAPE"); if (capeTexture != null) { - capeUrl = capeTexture.get("url").asText().replace("http://", "https://"); + JsonNode capeUrlNode = capeTexture.get("url"); + if (capeUrlNode != null && capeUrlNode.isTextual()) { + capeUrl = capeUrlNode.asText().replace("http://", "https://"); + } } return new GameProfileData(skinUrl, capeUrl, isAlex); diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index f4d7eb774..4b700b715 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -26,9 +26,6 @@ package org.geysermc.geyser.skin; import com.fasterxml.jackson.databind.JsonNode; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import it.unimi.dsi.fastutil.bytes.ByteArrays; @@ -172,14 +169,13 @@ public class SkinProvider { /** * If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback. */ - static SkinData determineFallbackSkinData(PlayerEntity entity) { + static SkinData determineFallbackSkinData(UUID uuid) { Skin skin = null; Cape cape = null; SkinGeometry geometry = SkinGeometry.WIDE; if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) { // Let's see if this player is a Bedrock player, and if so, let's pull their skin. - UUID uuid = entity.getUuid(); GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); if (session != null) { String skinId = session.getClientData().getSkinId(); @@ -192,7 +188,7 @@ public class SkinProvider { if (skin == null) { // We don't have a skin for the player right now. Fall back to a default. - ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid()); + ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid); skin = providedSkin.getData(); geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE; } @@ -232,7 +228,7 @@ public class SkinProvider { SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity); if (data == null) { // This player likely does not have a textures property - return CompletableFuture.completedFuture(determineFallbackSkinData(entity)); + return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid())); } return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl()) 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 2759b1408..7f1605561 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkullSkinManager.java @@ -29,11 +29,12 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.type.player.SkullPlayerEntity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import java.util.Collections; +import java.util.function.BiConsumer; import java.util.function.Consumer; public class SkullSkinManager extends SkinManager { @@ -48,28 +49,37 @@ public class SkullSkinManager extends SkinManager { ); } - public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, + public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session, Consumer skinConsumer) { + BiConsumer applySkin = (skin, throwable) -> { + try { + PlayerSkinPacket packet = new PlayerSkinPacket(); + packet.setUuid(entity.getUuid()); + packet.setOldSkinName(""); + packet.setNewSkinName(skin.getTextureUrl()); + packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); + packet.setTrustedSkin(true); + session.sendUpstreamPacket(packet); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); + } + + if (skinConsumer != null) { + skinConsumer.accept(skin); + } + }; + GameProfileData data = GameProfileData.from(entity); - - SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true) - .whenCompleteAsync((skin, throwable) -> { - try { - PlayerSkinPacket packet = new PlayerSkinPacket(); - packet.setUuid(entity.getUuid()); - packet.setOldSkinName(""); - packet.setNewSkinName(skin.getTextureUrl()); - packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); - packet.setTrustedSkin(true); - session.sendUpstreamPacket(packet); - } catch (Exception e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); - } - - if (skinConsumer != null) { - skinConsumer.accept(skin); - } - }); + 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 + SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.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/level/block/entity/SkullBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java index 00835cd1a..074bd0e6d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/SkullBlockEntityTranslator.java @@ -39,7 +39,9 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.skin.SkinProvider; +import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -59,41 +61,52 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements builder.put("SkullType", skullVariant); } - private static CompletableFuture getTextures(CompoundTag tag) { - CompoundTag owner = tag.get("SkullOwner"); - if (owner != null) { - CompoundTag properties = owner.get("Properties"); - if (properties == null) { - if (owner.get("Id") instanceof IntArrayTag uuidTag) { - int[] uuidAsArray = uuidTag.getValue(); - // thank u viaversion - UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL), - (long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL)); - if (uuid.version() == 4) { - String uuidString = uuid.toString().replace("-", ""); - return SkinProvider.requestTexturesFromUUID(uuidString); - } - } - if (owner.get("Name") instanceof StringTag nameTag) { - // Fall back to username if UUID was missing or was an offline mode UUID - return SkinProvider.requestTexturesFromUsername(nameTag.getValue()); - } - return CompletableFuture.completedFuture(null); - } - - ListTag textures = properties.get("textures"); - LinkedHashMap tag1 = (LinkedHashMap) textures.get(0).getValue(); - StringTag texture = (StringTag) tag1.get("Value"); - return CompletableFuture.completedFuture(texture.getValue()); + private static UUID getUUID(CompoundTag owner) { + if (owner.get("Id") instanceof IntArrayTag uuidTag && uuidTag.length() == 4) { + int[] uuidAsArray = uuidTag.getValue(); + // thank u viaversion + return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL), + (long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL)); } - return CompletableFuture.completedFuture(null); + // Convert username to an offline UUID + String username = null; + if (owner.get("Name") instanceof StringTag nameTag) { + username = nameTag.getValue().toLowerCase(Locale.ROOT); + } + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8)); + } + + private static CompletableFuture getTextures(CompoundTag owner, UUID uuid) { + CompoundTag properties = owner.get("Properties"); + if (properties == null) { + if (uuid != null && uuid.version() == 4) { + String uuidString = uuid.toString().replace("-", ""); + return SkinProvider.requestTexturesFromUUID(uuidString); + } else if (owner.get("Name") instanceof StringTag nameTag) { + // Fall back to username if UUID was missing or was an offline mode UUID + return SkinProvider.requestTexturesFromUsername(nameTag.getValue()); + } + return CompletableFuture.completedFuture(null); + } + + ListTag textures = properties.get("textures"); + LinkedHashMap tag1 = (LinkedHashMap) textures.get(0).getValue(); + StringTag texture = (StringTag) tag1.get("Value"); + return CompletableFuture.completedFuture(texture.getValue()); } public static int translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) { - CompletableFuture texturesFuture = getTextures(tag); + CompoundTag owner = tag.get("SkullOwner"); + if (owner == null) { + session.getSkullCache().removeSkull(blockPosition); + return -1; + } + UUID uuid = getUUID(owner); + + CompletableFuture texturesFuture = getTextures(owner, uuid); if (texturesFuture.isDone()) { try { - SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, texturesFuture.get(), blockState); + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesFuture.get(), blockState); return skull.getCustomRuntimeId(); } catch (InterruptedException | ExecutionException e) { session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag); @@ -111,17 +124,18 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements return; } if (session.getEventLoop().inEventLoop()) { - putSkull(session, blockPosition, texturesProperty, blockState); + putSkull(session, blockPosition, uuid, texturesProperty, blockState); } else { - session.executeInEventLoop(() -> putSkull(session, blockPosition, texturesProperty, blockState)); + session.executeInEventLoop(() -> putSkull(session, blockPosition, uuid, texturesProperty, blockState)); } }); + // We don't have the textures yet, so we can't determine if a custom block was defined for this skull return -1; } - private static void putSkull(GeyserSession session, Vector3i blockPosition, String texturesProperty, int blockState) { - SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState); + private static void putSkull(GeyserSession session, Vector3i blockPosition, UUID uuid, String texturesProperty, int blockState) { + SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState); if (skull.getCustomRuntimeId() != -1) { UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket(); updateBlockPacket.setDataLayer(0);