From 5ae95433e52dc68bd28620f226965e7868ab9921 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Wed, 6 May 2020 22:50:01 +0100 Subject: [PATCH] Bedrock to Bedrock legacy skin support (#276) * Added legacy skin support for bedrock to bedrock clients * Added bedrock to bedrock cape handling * Added bedrock geometry support * Bedrock skins now work in all auth modes * Tonne of debug info * Added fix to prevent customised skins from being loaded * Added skin size to bedrock client data * Cleaned debugging code * Made bedrock cape take priority over third party * Cut the customised skin image in half to hopefully get it to map * Removed hacky conversion attempt * Fixed bedrock skin caching on load and 1.14.60 support * Cleaned up debug messages * Added linked player ignore --- .../network/session/GeyserSession.java | 10 +++- .../session/auth/BedrockClientData.java | 4 ++ .../connector/utils/SkinProvider.java | 39 ++++++++++++++ .../geysermc/connector/utils/SkinUtils.java | 53 ++++++++++++++++--- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index cf74f923..759a0f3b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -31,10 +31,10 @@ import com.github.steveice10.mc.auth.exception.request.RequestException; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; -import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; import com.github.steveice10.mc.protocol.data.game.world.block.BlockState; -import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; import com.github.steveice10.packetlib.Client; import com.github.steveice10.packetlib.event.session.*; @@ -68,6 +68,7 @@ import org.geysermc.connector.network.translators.Registry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.ChunkUtils; import org.geysermc.connector.utils.LocaleUtils; +import org.geysermc.connector.utils.SkinUtils; import org.geysermc.connector.utils.Toolbox; import org.geysermc.floodgate.util.BedrockData; import org.geysermc.floodgate.util.EncryptionUtil; @@ -338,6 +339,11 @@ public class GeyserSession implements CommandSender { GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); playerEntity.setUsername(profile.getName()); playerEntity.setUuid(profile.getId()); + + // Check if they are not using a linked account + if (!playerEntity.getUuid().toString().startsWith("00000000-0000-0000")) { + SkinUtils.handleBedrockSkin(playerEntity, clientData); + } } Registry.JAVA.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index 8fc56acd..4ef0fe79 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -24,6 +24,10 @@ public class BedrockClientData { private String skinId; @JsonProperty(value = "SkinData") private String skinData; + @JsonProperty(value = "SkinImageHeight") + private int skinImageHeight; + @JsonProperty(value = "SkinImageWidth") + private int skinImageWidth; @JsonProperty(value = "CapeId") private String capeId; @JsonProperty(value = "CapeData") diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java index 7c2b7fc0..15cccf22 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinProvider.java @@ -58,6 +58,9 @@ public class SkinProvider { private static Map cachedCapes = new ConcurrentHashMap<>(); private static Map> requestedCapes = new ConcurrentHashMap<>(); + public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy("geometry.humanoid"); + private static Map cachedGeometry = new ConcurrentHashMap<>(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final int CACHE_INTERVAL = 8 * 60 * 1000; // 8 minutes @@ -164,6 +167,31 @@ public class SkinProvider { return CompletableFuture.completedFuture(officialCape); } + public static CompletableFuture requestBedrockCape(UUID playerID, boolean newThread) { + Cape bedrockCape = cachedCapes.getOrDefault(playerID.toString() + ".Bedrock", EMPTY_CAPE); + return CompletableFuture.completedFuture(bedrockCape); + } + + public static CompletableFuture requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID, boolean newThread) { + SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry); + return CompletableFuture.completedFuture(bedrockGeometry); + } + + public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) { + Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true); + cachedSkins.put(playerID, skin); + } + + public static void storeBedrockCape(UUID playerID, byte[] capeData) { + Cape cape = new Cape(playerID.toString() + ".Bedrock", playerID.toString(), capeData, System.currentTimeMillis(), false); + cachedCapes.put(playerID.toString() + ".Bedrock", cape); + } + + public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) { + SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData)); + cachedGeometry.put(playerID, geometry); + } + private static Skin supplySkin(UUID uuid, String textureUrl) { byte[] skin = EMPTY_SKIN.getSkinData(); try { @@ -285,6 +313,17 @@ public class SkinProvider { private boolean failed; } + @AllArgsConstructor + @Getter + public static class SkinGeometry { + private String geometryName; + private String geometryData; + + public static SkinGeometry getLegacy(String name) { + return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"" + name + "\"}}", ""); + } + } + /* * Sorted by 'priority' */ diff --git a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java index db88f2fa..1d0ee216 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SkinUtils.java @@ -39,6 +39,7 @@ import org.geysermc.common.AuthType; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.session.auth.BedrockClientData; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -52,6 +53,8 @@ public class SkinUtils { GameProfileData data = GameProfileData.from(profile); SkinProvider.Cape cape = SkinProvider.getCachedCape(data.getCapeUrl()); + SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy("geometry.humanoid.custom" + (data.isAlex() ? "Slim" : "")); + return buildEntryManually( profile.getId(), profile.getName(), @@ -60,8 +63,8 @@ public class SkinUtils { SkinProvider.getCachedSkin(profile.getId()).getSkinData(), cape.getCapeId(), cape.getCapeData(), - getLegacySkinGeometry("geometry.humanoid.custom" + (data.isAlex() ? "Slim" : "")), - "" + geometry.getGeometryName(), + geometry.getGeometryData() ); } @@ -74,8 +77,8 @@ public class SkinUtils { SkinProvider.STEVE_SKIN, SkinProvider.EMPTY_CAPE.getCapeId(), SkinProvider.EMPTY_CAPE.getCapeData(), - getLegacySkinGeometry("geometry.humanoid"), - "" + SkinProvider.EMPTY_GEOMETRY.getGeometryName(), + SkinProvider.EMPTY_GEOMETRY.getGeometryData() ); } @@ -151,6 +154,12 @@ public class SkinUtils { SkinProvider.Skin skin = skinAndCape.getSkin(); SkinProvider.Cape cape = skinAndCape.getCape(); + if (cape.isFailed()) { + cape = SkinProvider.getOrDefault(SkinProvider.requestBedrockCape( + entity.getUuid(), false + ), SkinProvider.EMPTY_CAPE, 3); + } + if (cape.isFailed() && SkinProvider.ALLOW_THIRD_PARTY_CAPES) { cape = SkinProvider.getOrDefault(SkinProvider.requestUnofficialCape( cape, entity.getUuid(), @@ -158,6 +167,11 @@ public class SkinUtils { ), SkinProvider.EMPTY_CAPE, SkinProvider.CapeProvider.VALUES.length * 3); } + SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy("geometry.humanoid.custom" + (data.isAlex() ? "Slim" : "")); + geometry = SkinProvider.getOrDefault(SkinProvider.requestBedrockGeometry( + geometry, entity.getUuid(), false + ), geometry, 3); + if (entity.getLastSkinUpdate() < skin.getRequestedOn()) { entity.setLastSkinUpdate(skin.getRequestedOn()); @@ -170,8 +184,8 @@ public class SkinUtils { skin.getSkinData(), cape.getCapeId(), cape.getCapeData(), - getLegacySkinGeometry("geometry.humanoid.custom" + (data.isAlex() ? "Slim" : "")), - "" + geometry.getGeometryName(), + geometry.getGeometryData() ); PlayerListPacket playerRemovePacket = new PlayerListPacket(); @@ -194,6 +208,33 @@ public class SkinUtils { }); } + public static void handleBedrockSkin(PlayerEntity playerEntity, BedrockClientData clientData) { + GameProfileData data = GameProfileData.from(playerEntity.getProfile()); + + GeyserConnector.getInstance().getLogger().info("Registering bedrock skin for " + playerEntity.getUsername() + " (" + playerEntity.getUuid() + ")"); + + try { + byte[] skinBytes = com.github.steveice10.mc.auth.util.Base64.decode(clientData.getSkinData().getBytes("UTF-8")); + byte[] capeBytes = clientData.getCapeData(); + + byte[] geometryNameBytes = com.github.steveice10.mc.auth.util.Base64.decode(clientData.getGeometryName().getBytes("UTF-8")); + byte[] geometryBytes = com.github.steveice10.mc.auth.util.Base64.decode(clientData.getGeometryData().getBytes("UTF-8")); + + if (skinBytes.length <= (128 * 128 * 4)) { + SkinProvider.storeBedrockSkin(playerEntity.getUuid(), data.getSkinUrl(), skinBytes); + } else { + GeyserConnector.getInstance().getLogger().info("Unable to load bedrock skin for '" + playerEntity.getUsername() + "' as they are using a customised skin"); + GeyserConnector.getInstance().getLogger().debug("The size of '" + playerEntity.getUsername() + "' skin is: " + clientData.getSkinImageWidth() + "x" + clientData.getSkinImageHeight()); + } + SkinProvider.storeBedrockGeometry(playerEntity.getUuid(), geometryNameBytes, geometryBytes); + if (!clientData.getCapeId().equals("")) { + SkinProvider.storeBedrockCape(playerEntity.getUuid(), capeBytes); + } + } catch (Exception e) { + throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e); + } + } + /** * Create a basic geometry json for the given name *