From 9252af310959b75d55f327161f2a0b51ad490cd0 Mon Sep 17 00:00:00 2001 From: Koncloud <138350973+Koncloud@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:18:36 +0800 Subject: [PATCH] Add support for persona skin between bedrock players --- .../org/geysermc/geyser/api/GeyserApi.java | 8 + .../java/org/geysermc/geyser/GeyserImpl.java | 6 + .../geyser/session/GeyserSession.java | 4 +- .../session/auth/BedrockClientData.java | 64 ++++++- .../geyser/skin/FakeHeadProvider.java | 3 +- .../org/geysermc/geyser/skin/SkinManager.java | 162 +++++++++--------- .../geysermc/geyser/skin/SkinProvider.java | 110 +++--------- .../java/JavaLoginFinishedTranslator.java | 5 +- .../java/entity/JavaAddEntityTranslator.java | 2 +- .../JavaPlayerInfoUpdateTranslator.java | 2 +- 10 files changed, 191 insertions(+), 175 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java index 5c20d06e110..2a4b522fd31 100644 --- a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java +++ b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java @@ -66,6 +66,14 @@ public interface GeyserApi extends GeyserApiBase { @NonNull List onlineConnections(); + /** + * Method to determine if the given online player is a linked Bedrock player. + * + * @param uuid the uuid of the online player + * @return true if the given online player is a Bedrock player + */ + boolean isLinkedPlayer(@NonNull UUID uuid); + /** * Gets the {@link ExtensionManager}. * diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 6a40d5efaf2..6e2d3bd8d0c 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -641,6 +641,12 @@ public boolean isBedrockPlayer(@NonNull UUID uuid) { return connectionByUuid(uuid) != null; } + @Override + public boolean isLinkedPlayer(@NonNull UUID uuid) { + GeyserSession session = connectionByUuid(uuid); + return session != null && session.isLinked(); + } + @Override public boolean sendForm(@NonNull UUID uuid, @NonNull Form form) { Objects.requireNonNull(uuid); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index d54c045dbe7..fe2f8d4dc43 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1142,6 +1142,7 @@ public void disconnect(Component reason) { // Remove from session manager geyser.getSessionManager().removeSession(this); + SkinManager.removeCachedBedrockSkin(this); if (authData != null) { PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); if (task != null) { @@ -2314,7 +2315,8 @@ public UUID javaUuid() { @Override public boolean isLinked() { - return false; //todo + // Java and linked players' uuid version is 4, while bedrock is 0 + return this.javaUuid().version() == 4; } @SuppressWarnings("ConstantConditions") // Need to enforce the parameter annotations diff --git a/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java b/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java index 07dd3849185..a2c6486844b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java +++ b/core/src/main/java/org/geysermc/geyser/session/auth/BedrockClientData.java @@ -30,10 +30,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; +import org.cloudburstmc.protocol.bedrock.data.skin.AnimatedTextureType; +import org.cloudburstmc.protocol.bedrock.data.skin.AnimationData; +import org.cloudburstmc.protocol.bedrock.data.skin.AnimationExpressionType; +import org.cloudburstmc.protocol.bedrock.data.skin.ImageData; +import org.cloudburstmc.protocol.bedrock.data.skin.PersonaPieceData; +import org.cloudburstmc.protocol.bedrock.data.skin.PersonaPieceTintData; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; import org.geysermc.floodgate.util.UiProfile; +import java.util.List; import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @@ -51,7 +58,7 @@ public final class BedrockClientData { @JsonProperty(value = "SkinId") private String skinId; @JsonProperty(value = "SkinData") - private String skinData; + private byte[] skinData; @JsonProperty(value = "SkinImageHeight") private int skinImageHeight; @JsonProperty(value = "SkinImageWidth") @@ -67,13 +74,27 @@ public final class BedrockClientData { @JsonProperty(value = "CapeOnClassicSkin") private boolean capeOnClassicSkin; @JsonProperty(value = "SkinResourcePatch") - private String geometryName; + private byte[] geometryName; @JsonProperty(value = "SkinGeometryData") - private String geometryData; + private byte[] geometryData; @JsonProperty(value = "PersonaSkin") private boolean personaSkin; @JsonProperty(value = "PremiumSkin") private boolean premiumSkin; + @JsonIgnore + private List personaPieces; + @JsonIgnore + private List personaPieceTint; + @JsonIgnore + private List skinAnimations; + @JsonProperty(value = "SkinAnimationData") + private byte[] skinAnimationData; + @JsonProperty(value = "ArmSize") + private String armSize; + @JsonProperty(value = "SkinColor") + private String skinColor; + @JsonProperty(value = "SkinGeometryDataEngineVersion") + private byte[] geometryDataEngineVersion; @JsonProperty(value = "DeviceId") private String deviceId; @@ -98,12 +119,6 @@ public final class BedrockClientData { @JsonProperty(value = "ClientRandomId") private long clientRandomId; - @JsonProperty(value = "ArmSize") - private String armSize; - @JsonProperty(value = "SkinAnimationData") - private String skinAnimationData; - @JsonProperty(value = "SkinColor") - private String skinColor; @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; @JsonProperty(value = "PlayFabId") @@ -128,4 +143,35 @@ public InputMode getDefaultInputMode() { public UiProfile getUiProfile() { return uiProfile != null ? uiProfile : UiProfile.CLASSIC; } + + @JsonProperty(value = "AnimatedImageData") + private void processAnimationData(List animationDataDTO) { + this.skinAnimations = animationDataDTO.stream().map(animation -> new AnimationData(ImageData.of(animation.imageWidth, animation.ImageHeight, animation.image), animation.textureType(), animation.frames())).toList(); + } + + @JsonProperty(value = "PersonaPieces") + private void processPersonaPieceData(List personaPieceDataDTO) { + this.personaPieces = personaPieceDataDTO.stream().map(personaPiece -> new PersonaPieceData(personaPiece.id, personaPiece.type, personaPiece.packId, personaPiece.isDefault, personaPiece.productId)).toList(); + } + + @JsonProperty(value = "PieceTintColors") + private void processPersonaPieceTintData(List personaPieceTintDataDTO) { + this.personaPieceTint = personaPieceTintDataDTO.stream().map(personaPieceTint -> new PersonaPieceTintData(personaPieceTint.type, personaPieceTint.colors)).toList(); + } + + private record AnimationDataDTO(@JsonProperty(value = "Type") AnimatedTextureType textureType, + @JsonProperty(value = "Image") byte[] image, + @JsonProperty(value = "ImageWidth") int imageWidth, + @JsonProperty(value = "ImageHeight") int ImageHeight, + @JsonProperty(value = "Frames") float frames, + @JsonProperty(value = "AnimationExpression") AnimationExpressionType expressionType) {} + + private record PersonaPieceDataDTO(@JsonProperty(value = "PieceId") String id, + @JsonProperty(value = "PieceType") String type, + @JsonProperty(value = "PackId") String packId, + @JsonProperty(value = "IsDefault") boolean isDefault, + @JsonProperty(value = "ProductId") String productId) {} + + private record PersonaPieceTintDataDTO(@JsonProperty(value = "PieceType") String type, + @JsonProperty(value = "Colors") List colors) {} } diff --git a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java index 12f00202515..e8169c39285 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -67,7 +67,8 @@ public class FakeHeadProvider { .build(new CacheLoader<>() { @Override public SkinData load(@NonNull FakeHeadEntry fakeHeadEntry) throws Exception { - SkinData skinData = SkinProvider.getOrDefault(SkinProvider.requestSkinData(fakeHeadEntry.getEntity(), fakeHeadEntry.getSession()), null, 5); + SkinData skinData = SkinManager.getSkinData(SkinProvider.getOrDefault( + SkinProvider.requestSkinData(fakeHeadEntry.getEntity(), fakeHeadEntry.getSession()), null, 5)); if (skinData == null) { throw new Exception("Couldn't load player's original skin"); 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 2f506d10fdc..5a1d33477fe 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -51,7 +51,7 @@ import java.util.Base64; import java.util.List; import java.util.UUID; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; public class SkinManager { @@ -61,30 +61,23 @@ public class SkinManager { * Builds a Bedrock player list entry from our existing, cached Bedrock skin information */ public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) { - // First: see if we have the cached skin texture ID. GameProfileData data = GameProfileData.from(playerEntity); - Skin skin = null; - Cape cape = null; - SkinGeometry geometry = SkinGeometry.WIDE; - if (data != null) { - // GameProfileData is not null = server provided us with textures data to work with. - skin = SkinProvider.getCachedSkin(data.skinUrl()); - cape = SkinProvider.getCachedCape(data.capeUrl()); - geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE; + SerializedSkin serializedSkin = null; + boolean isLinkedBedrockPlayer = GeyserImpl.getInstance().isLinkedPlayer(playerEntity.getUuid()); + + if (!isLinkedBedrockPlayer) { + serializedSkin = SkinProvider.getCachedBedrockSkin(playerEntity.getUuid()); + } else if (data != null) { + Skin skin = SkinProvider.getCachedSkin(data.skinUrl()); + Cape cape = SkinProvider.getCachedCape(data.capeUrl()); + SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE; + if (skin != null) { + serializedSkin = getSerializedSkin(skin, cape ,geometry); + } } - if (skin == null || cape == null) { - // 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 - SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid()); - if (skin == null) { - skin = fallbackSkinData.skin(); - geometry = fallbackSkinData.geometry(); - } - if (cape == null) { - cape = fallbackSkinData.cape(); - } + if (serializedSkin == null) { + serializedSkin = getSerializedSkin(SkinProvider.determineFallbackSkinData(playerEntity.getUuid())); } // Default to white when waypoint colour is unknown, which is the most visible @@ -95,9 +88,7 @@ public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, Pla playerEntity.getUuid(), playerEntity.getUsername(), playerEntity.getGeyserId(), - skin, - cape, - geometry, + serializedSkin, color ); } @@ -105,12 +96,7 @@ public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, Pla /** * With all the information needed, build a Bedrock player entry with translated skin information. */ - public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, - Skin skin, - Cape cape, - SkinGeometry geometry, Color color) { - SerializedSkin serializedSkin = getSkin(session, skin.textureUrl(), skin, cape, geometry); - + public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, SerializedSkin serializedSkin, Color color) { // This attempts to find the XUID of the player so profile images show up for Xbox accounts String xuid = ""; GeyserSession playerSession = GeyserImpl.getInstance().connectionByUuid(uuid); @@ -118,7 +104,7 @@ public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, U // Prefer looking up xuid using the session to catch linked players if (playerSession != null) { xuid = playerSession.getAuthData().xuid(); - } else if (uuid.version() == 0) { + } else if (!GeyserImpl.getInstance().isLinkedPlayer(uuid)) { xuid = Long.toString(uuid.getLeastSignificantBits()); } @@ -144,9 +130,10 @@ public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, U } public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinData skinData) { - Skin skin = skinData.skin(); - Cape cape = skinData.cape(); - SkinGeometry geometry = skinData.geometry(); + sendSkinPacket(session, entity, getSerializedSkin(skinData)); + } + + public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SerializedSkin serializedSkin) { Color color = session.getWaypointCache().getWaypointColor(entity.getUuid()).orElse(Color.WHITE); if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { @@ -155,9 +142,7 @@ public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, Sk entity.getUuid(), entity.getUsername(), entity.getGeyserId(), - skin, - cape, - geometry, + serializedSkin, color ); @@ -169,45 +154,58 @@ public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, Sk PlayerSkinPacket packet = new PlayerSkinPacket(); packet.setUuid(entity.getUuid()); packet.setOldSkinName(""); - packet.setNewSkinName(skin.textureUrl()); - packet.setSkin(getSkin(session, skin.textureUrl(), skin, cape, geometry)); + packet.setNewSkinName(serializedSkin.getSkinId()); + packet.setSkin(serializedSkin); packet.setTrustedSkin(true); session.sendUpstreamPacket(packet); } } - private static SerializedSkin getSkin(GeyserSession session, String skinId, Skin skin, Cape cape, SkinGeometry geometry) { + public static SerializedSkin getSerializedSkin(SkinData skinData) { + return skinData == null ? null : getSerializedSkin(skinData.skin(), skinData.cape(), skinData.geometry()); + } + + public static SerializedSkin getSerializedSkin(Skin skin, Cape cape, SkinGeometry geometry) { + if (skin == null) { + skin = SkinProvider.EMPTY_SKIN; + } + if (cape == null) { + cape = SkinProvider.EMPTY_CAPE; + } + if (geometry == null) { + geometry = SkinGeometry.WIDE; + } return SerializedSkin.builder() - .skinId(skinId) + .skinId(skin.textureUrl()) .skinResourcePatch(geometry.geometryName()) .skinData(ImageData.of(skin.skinData())) .capeData(ImageData.of(cape.capeData())) .geometryData(geometry.geometryData().isBlank() ? GEOMETRY : geometry.geometryData()) .premium(true) .capeId(cape.capeId()) - .fullSkinId(skinId) - .geometryDataEngineVersion(session.getClientData().getGameVersion()) + .fullSkinId(skin.textureUrl()) + .geometryDataEngineVersion("0.0.0") + .persona(false) .build(); } - public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session, - Consumer skinAndCapeConsumer) { - SkinProvider.requestSkinData(entity, session).whenCompleteAsync((skinData, throwable) -> { - if (skinData == null) { - if (skinAndCapeConsumer != null) { - skinAndCapeConsumer.accept(null); - } - - return; - } - - if (skinData.geometry() != null) { - sendSkinPacket(session, entity, skinData); - } + /** + * Fall back to classic skin. + * */ + public static SkinData getSkinData(SerializedSkin serializedSkin) { + return serializedSkin == null ? null : new SkinData( + new Skin(serializedSkin.getSkinId(), serializedSkin.getSkinData().getImage()), + new Cape(serializedSkin.getCapeId(), serializedSkin.getCapeId(), serializedSkin.getCapeData().getImage()), + new SkinGeometry(serializedSkin.getSkinResourcePatch(), serializedSkin.getGeometryData()) + ); + } - if (skinAndCapeConsumer != null) { - skinAndCapeConsumer.accept(new SkinProvider.SkinAndCape(skinData.skin(), skinData.cape())); + public static CompletableFuture requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session) { + return SkinProvider.requestSkinData(entity, session).thenApplyAsync(serializedSkin -> { + if (serializedSkin != null) { + sendSkinPacket(session, entity, serializedSkin); } + return serializedSkin; }); } @@ -218,28 +216,38 @@ public static void handleBedrockSkin(PlayerEntity playerEntity, BedrockClientDat } try { - byte[] skinBytes = Base64.getDecoder().decode(clientData.getSkinData().getBytes(StandardCharsets.UTF_8)); - byte[] capeBytes = clientData.getCapeData(); - - byte[] geometryNameBytes = Base64.getDecoder().decode(clientData.getGeometryName().getBytes(StandardCharsets.UTF_8)); - byte[] geometryBytes = Base64.getDecoder().decode(clientData.getGeometryData().getBytes(StandardCharsets.UTF_8)); - - if (skinBytes.length <= (128 * 128 * 4) && !clientData.isPersonaSkin()) { - SkinProvider.storeBedrockSkin(playerEntity.getUuid(), clientData.getSkinId(), skinBytes); - SkinProvider.storeBedrockGeometry(playerEntity.getUuid(), geometryNameBytes, geometryBytes); - } else if (geyser.getConfig().isDebugMode()) { - geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.skin.bedrock.fail", playerEntity.getUsername())); - geyser.getLogger().debug("The size of '" + playerEntity.getUsername() + "' skin is: " + clientData.getSkinImageWidth() + "x" + clientData.getSkinImageHeight()); - } - - if (!clientData.getCapeId().equals("")) { - SkinProvider.storeBedrockCape(clientData.getCapeId(), capeBytes); - } + // Base64 -> byte[] (Base64 is decoded by jackson) -> String (Decoded, parameter of SerializedSkin) + SerializedSkin serializedSkin = SerializedSkin.of( + clientData.getSkinId(), + clientData.getPlayFabId(), + new String(clientData.getGeometryName()), + ImageData.of(clientData.getSkinImageWidth(), clientData.getSkinImageHeight(), clientData.getSkinData()), + clientData.getSkinAnimations(), + ImageData.of(clientData.getCapeImageWidth(),clientData.getCapeImageHeight(), clientData.getCapeData()), + new String(clientData.getGeometryData()), + new String(clientData.getGeometryDataEngineVersion()), + new String(clientData.getSkinAnimationData()), + clientData.isPremiumSkin(), + clientData.isPersonaSkin(), + clientData.isCapeOnClassicSkin(), + true, + clientData.getCapeId(), + clientData.getSkinId(), + clientData.getArmSize(), + clientData.getSkinColor(), + clientData.getPersonaPieces(), + clientData.getPersonaPieceTint() + ); + SkinProvider.storeBedrockSkin(playerEntity.getUuid(), serializedSkin); } catch (Exception e) { throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e); } } + public static void removeCachedBedrockSkin(GeyserSession session){ + SkinProvider.removeBedrockSkin(session.javaUuid()); + } + public record GameProfileData(String skinUrl, String capeUrl, boolean isAlex) { /** * Generate the GameProfileData from the given CompoundTag representing a GameProfile 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 f3ad0be2ffa..7ae19039959 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -31,9 +31,9 @@ import it.unimi.dsi.fastutil.bytes.ByteArrays; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionSkinApplyEvent; -import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.skin.Cape; import org.geysermc.geyser.api.skin.Skin; import org.geysermc.geyser.api.skin.SkinData; @@ -71,19 +71,10 @@ public class SkinProvider { private static final Cache CACHED_JAVA_SKINS = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) .build(); - - private static final Cache CACHED_BEDROCK_CAPES = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) - .build(); - private static final Cache CACHED_BEDROCK_SKINS = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) - .build(); - + private static final Map CACHED_BEDROCK_SKINS = new ConcurrentHashMap<>(); private static final Map> requestedCapes = new ConcurrentHashMap<>(); private static final Map> requestedSkins = new ConcurrentHashMap<>(); - private static final Map cachedGeometry = new ConcurrentHashMap<>(); - /** * Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and * offline mode players use version 3. @@ -178,71 +169,40 @@ static Skin getCachedSkin(String skinUrl) { return CACHED_JAVA_SKINS.getIfPresent(skinUrl); } - /** - * 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(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. - GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); - if (session != null) { - String skinId = session.getClientData().getSkinId(); - skin = CACHED_BEDROCK_SKINS.getIfPresent(skinId); - String capeId = session.getClientData().getCapeId(); - cape = CACHED_BEDROCK_CAPES.getIfPresent(capeId); - geometry = cachedGeometry.getOrDefault(uuid, geometry); - } - } - - if (skin == null) { - // We don't have a skin for the player right now. Fall back to a default. - ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid); - skin = providedSkin.getData(); - geometry = providedSkin.isSlim() ? SkinGeometry.SLIM : SkinGeometry.WIDE; - } - - if (cape == null) { - cape = EMPTY_CAPE; - } - - return new SkinData(skin, cape, geometry); + static SerializedSkin getCachedBedrockSkin(UUID uuid) { + return CACHED_BEDROCK_SKINS.get(uuid); } /** - * Used as a fallback if an official Java cape doesn't exist for this user. + * If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback. */ - @NonNull - private static Cape getCachedBedrockCape(UUID uuid) { - GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid); - if (session != null) { - String capeId = session.getClientData().getCapeId(); - Cape bedrockCape = CACHED_BEDROCK_CAPES.getIfPresent(capeId); - if (bedrockCape != null) { - return bedrockCape; - } - } - return EMPTY_CAPE; + static SkinData determineFallbackSkinData(UUID uuid) { + ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid); + return new SkinData(providedSkin.getData(), EMPTY_CAPE, providedSkin.isSlim() ? SkinGeometry.SLIM : SkinGeometry.WIDE); } @Nullable static Cape getCachedCape(String capeUrl) { - if (capeUrl == null) { - return null; - } - return CACHED_JAVA_CAPES.getIfPresent(capeUrl); + return capeUrl == null ? null : CACHED_JAVA_CAPES.getIfPresent(capeUrl); } - static CompletableFuture requestSkinData(PlayerEntity entity, GeyserSession session) { + static CompletableFuture requestSkinData(PlayerEntity entity, GeyserSession session) { SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity); + boolean isLinkedBedrockPlayer = GeyserImpl.getInstance().isLinkedPlayer(entity.getUuid()); + boolean isBedrock = GeyserImpl.getInstance().isBedrockPlayer(entity.getUuid()); + + // Linked player should use their linked skin instead of BE skin, + // while unlinked player should use cached Bedrock skin instead of translated JE skin uploaded by ourselves + if (!isLinkedBedrockPlayer) { + return CompletableFuture.completedFuture(getCachedBedrockSkin(entity.getUuid())); + } + if (data == null) { // This player likely does not have a textures property - return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid())); + return CompletableFuture.completedFuture(SkinManager.getSerializedSkin(determineFallbackSkinData(entity.getUuid()))); } + // Request skin for JE and Linked player return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl()) .thenApplyAsync(skinAndCape -> { try { @@ -250,16 +210,7 @@ static CompletableFuture requestSkinData(PlayerEntity entity, GeyserSe Cape cape = skinAndCape.cape(); SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE; - // Whether we should see if this player has a Bedrock skin we should check for on failure of - // any skin property - boolean checkForBedrock = entity.getUuid().version() != 4; - - if (cape.failed() && checkForBedrock) { - cape = getCachedBedrockCape(entity.getUuid()); - } - // Call event to allow extensions to modify the skin, cape and geo - boolean isBedrock = GeyserImpl.getInstance().connectionByUuid(entity.getUuid()) != null; SkinData skinData = new SkinData(skin, cape, geometry); final EventSkinData eventSkinData = new EventSkinData(skinData); GeyserImpl.getInstance().eventBus().fire(new SessionSkinApplyEvent(session, entity.getUsername(), entity.getUuid(), data.isAlex(), isBedrock, skinData) { @@ -284,12 +235,12 @@ public void geometry(@NonNull SkinGeometry newGeometry) { } }); - return eventSkinData.skinData(); + return SkinManager.getSerializedSkin(skin, cape, geometry); } catch (Exception e) { GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); } - return new SkinData(skinAndCape.skin(), skinAndCape.cape(), null); + return SkinManager.getSerializedSkin(skinAndCape.skin(), skinAndCape.cape(), SkinGeometry.WIDE); }); } @@ -364,19 +315,12 @@ private static CompletableFuture requestCape(String capeUrl, boolean newTh return future; } - static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) { - Skin skin = new Skin(skinId, skinData); - CACHED_BEDROCK_SKINS.put(skin.textureUrl(), skin); - } - - static void storeBedrockCape(String capeId, byte[] capeData) { - Cape cape = new Cape(capeId, capeId, capeData); - CACHED_BEDROCK_CAPES.put(capeId, cape); + static void storeBedrockSkin(UUID uuid, SerializedSkin skin) { + CACHED_BEDROCK_SKINS.put(uuid, skin); } - static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) { - SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData)); - cachedGeometry.put(playerID, geometry); + static void removeBedrockSkin(UUID uuid) { + CACHED_BEDROCK_SKINS.remove(uuid); } private static Skin supplySkin(UUID uuid, String textureUrl) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java index 36c1ef19768..b74c982e858 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginFinishedTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.translator.protocol.java; import net.kyori.adventure.key.Key; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.session.GeyserSession; @@ -56,8 +57,8 @@ public void translate(GeyserSession session, ClientboundLoginFinishedPacket pack session.getGeyser().getSessionManager().addSession(playerEntity.getUuid(), session); - // Check if they are not using a linked account - if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + // If they are not using a linked account, cache skin data on the server. + if (remoteAuthType == AuthType.OFFLINE || !session.isLinked()) { SkinManager.handleBedrockSkin(playerEntity, session.getClientData()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java index 3d00ad87adc..2e2ccc8d976 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java @@ -96,7 +96,7 @@ public void translate(GeyserSession session, ClientboundAddEntityPacket packet) // only load skin if we're not in a test environment. // Otherwise, it tries to load various resources if (!EnvironmentUtils.IS_UNIT_TESTING) { - SkinManager.requestAndHandleSkinAndCape(entity, session, null); + SkinManager.requestAndHandleSkinAndCape(entity, session); } return; } 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 1cb5f7a042e..8d81737f375 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 @@ -94,7 +94,7 @@ public void translate(GeyserSession session, ClientboundPlayerInfoUpdatePacket p playerEntity.setTexturesProperty(texturesProperty); if (self) { - SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape -> + SkinManager.requestAndHandleSkinAndCape(playerEntity, session).whenCompleteAsync((skinAndCape, throwable) -> GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername())); } }