From 93a74d669df6fb34ece1828b91861c7e0c423034 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 15 Feb 2021 16:36:47 -0500 Subject: [PATCH] Skin and skull fixes (#1923) * Skin and skull fixes - Handle the occasional greater-than-128-px skin - Remove unused Jackson dependency - Update used Jackson dependency - Handle skin downloading on another thread * Other small touchups * Flush after rescaling --- connector/pom.xml | 8 +- .../geysermc/connector/GeyserConnector.java | 3 +- .../geysermc/connector/skin/SkinManager.java | 10 ++- .../geysermc/connector/skin/SkinProvider.java | 75 +++++++++---------- .../connector/skin/SkullSkinManager.java | 45 +++-------- 5 files changed, 58 insertions(+), 83 deletions(-) diff --git a/connector/pom.xml b/connector/pom.xml index 8703c3117..77da3e4f2 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -20,13 +20,7 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.9.8 - compile - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.8 + 2.10.2 compile diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index f86e0b1e6..3494f8c20 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -77,7 +77,8 @@ public class GeyserConnector { .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) + .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); public static final String NAME = "Geyser"; public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java index ae3abc943..fb8336aca 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java @@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.BedrockClientData; @@ -163,7 +164,7 @@ public class SkinManager { geometry = SkinProvider.SkinGeometry.getEars(data.isAlex()); // Store the skin and geometry for the ears - SkinProvider.storeEarSkin(entity.getUuid(), skin); + SkinProvider.storeEarSkin(skin); SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex()); } } @@ -267,7 +268,10 @@ public class SkinManager { return new GameProfileData(skinUrl, capeUrl, isAlex); } catch (Exception exception) { - GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage()); + GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName()); + if (GeyserConnector.getInstance().getConfig().isDebugMode()) { + exception.printStackTrace(); + } return loadBedrockOrOfflineSkin(profile); } } @@ -282,7 +286,7 @@ public class SkinManager { String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); - if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { + if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) { GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); if (session != null) { diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java index 3f236932a..c4d4bc486 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java @@ -79,13 +79,12 @@ public class SkinProvider { .build(); private static final Map> requestedCapes = new ConcurrentHashMap<>(); - public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); private static final Map cachedGeometry = new ConcurrentHashMap<>(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); - public static String EARS_GEOMETRY; - public static String EARS_GEOMETRY_SLIM; - public static SkinGeometry SKULL_GEOMETRY; + public static final String EARS_GEOMETRY; + public static final String EARS_GEOMETRY_SLIM; + public static final SkinGeometry SKULL_GEOMETRY; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -229,15 +228,15 @@ public class SkinProvider { return CompletableFuture.completedFuture(officialCape); } - public static CompletableFuture requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) { + public static CompletableFuture requestEars(String earsUrl, boolean newThread, Skin skin) { if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin); CompletableFuture future; if (newThread) { - future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE) + future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE) .whenCompleteAsync((outSkin, throwable) -> { }); } else { - Skin ears = supplyEars(skin, earsUrl, provider); // blocking + Skin ears = supplyEars(skin, earsUrl); // blocking future = CompletableFuture.completedFuture(ears); } return future; @@ -255,7 +254,7 @@ public class SkinProvider { public static CompletableFuture requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) { for (EarsProvider provider : EarsProvider.VALUES) { Skin skin1 = getOrDefault( - requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin), + requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin), officialSkin, 4 ); if (skin1.isEars()) { @@ -295,12 +294,11 @@ public class SkinProvider { } /** - * Stores the ajusted skin with the ear texture to the cache + * Stores the adjusted skin with the ear texture to the cache * - * @param playerID The UUID to cache it against * @param skin The skin to cache */ - public static void storeEarSkin(UUID playerID, Skin skin) { + public static void storeEarSkin(Skin skin) { cachedSkins.put(skin.getTextureUrl(), skin); } @@ -324,7 +322,7 @@ public class SkinProvider { } private static Cape supplyCape(String capeUrl, CapeProvider provider) { - byte[] cape = new byte[0]; + byte[] cape = EMPTY_CAPE.getCapeData(); try { cape = requestImage(capeUrl, provider); } catch (Exception ignored) {} // just ignore I guess @@ -334,7 +332,7 @@ public class SkinProvider { return new Cape( capeUrl, urlSection[urlSection.length - 1], // get the texture id and use it as cape id - cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(), + cape, System.currentTimeMillis(), cape.length == 0 ); @@ -345,10 +343,9 @@ public class SkinProvider { * * @param existingSkin The players current skin * @param earsUrl The URL to get the ears texture from - * @param provider The ears texture provider * @return The updated skin with ears */ - private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) { + private static Skin supplyEars(Skin existingSkin, String earsUrl) { try { // Get the ears texture BufferedImage ears = ImageIO.read(new URL(earsUrl)); @@ -415,14 +412,15 @@ public class SkinProvider { // if the requested image is a cape if (provider != null) { - while(image.getWidth() > 64) { - image = scale(image); + if (image.getWidth() > 64) { + image = scale(image, 64, 32); + } + } else { + // Very rarely, skins can be larger than Minecraft's default. + // Bedrock will not render anything above a width of 128. + if (image.getWidth() > 128) { + image = scale(image, 128, image.getHeight() / (image.getWidth() / 128)); } - BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB); - Graphics g = newImage.createGraphics(); - g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null); - g.dispose(); - image = newImage; } byte[] data = bufferedImageToImageData(image); @@ -506,12 +504,13 @@ public class SkinProvider { return null; } - private static BufferedImage scale(BufferedImage bufferedImage) { - BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB); + private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { + BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = resized.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null); + g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null); g2.dispose(); + bufferedImage.flush(); return resized; } @@ -579,17 +578,17 @@ public class SkinProvider { @AllArgsConstructor @Getter public static class SkinAndCape { - private Skin skin; - private Cape cape; + private final Skin skin; + private final Cape cape; } @AllArgsConstructor @Getter public static class Skin { private UUID skinOwner; - private String textureUrl; - private byte[] skinData; - private long requestedOn; + private final String textureUrl; + private final byte[] skinData; + private final long requestedOn; private boolean updated; private boolean ears; @@ -603,19 +602,19 @@ public class SkinProvider { @AllArgsConstructor @Getter public static class Cape { - private String textureUrl; - private String capeId; - private byte[] capeData; - private long requestedOn; - private boolean failed; + private final String textureUrl; + private final String capeId; + private final byte[] capeData; + private final long requestedOn; + private final boolean failed; } @AllArgsConstructor @Getter public static class SkinGeometry { - private String geometryName; - private String geometryData; - private boolean failed; + private final String geometryName; + private final String geometryData; + private final boolean failed; /** * Generate generic geometry diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java index 644323a42..562e2c50f 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java @@ -27,65 +27,42 @@ package org.geysermc.connector.skin; import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; -import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; +import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.Collections; -import java.util.UUID; import java.util.function.Consumer; public class SkullSkinManager extends SkinManager { - public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId, - String skinId, byte[] skinData) { + public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) { // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png skinId = skinId + "_skull"; - SerializedSkin serializedSkin = SerializedSkin.of( + return SerializedSkin.of( skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(), "", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId ); - - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); - entry.setName(username); - entry.setEntityId(geyserId); - entry.setSkin(serializedSkin); - entry.setXuid(""); - entry.setPlatformChatId(""); - entry.setTeacher(false); - entry.setTrustedSkin(true); - return entry; } public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, Consumer skinConsumer) { GameProfileData data = GameProfileData.from(entity.getProfile()); - SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false) + SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true) .whenCompleteAsync((skin, throwable) -> { try { if (session.getUpstream().isInitialized()) { - PlayerListPacket.Entry updatedEntry = buildSkullEntryManually( - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData() - ); - - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - // It's a skull. We don't want them in the player list. - PlayerListPacket playerRemovePacket = new PlayerListPacket(); - playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); - playerRemovePacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerRemovePacket); + 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) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);