diff --git a/proxy/src/main/java/org/dragonet/proxy/network/session/ProxySession.java b/proxy/src/main/java/org/dragonet/proxy/network/session/ProxySession.java index 4d2df1d10..245623694 100644 --- a/proxy/src/main/java/org/dragonet/proxy/network/session/ProxySession.java +++ b/proxy/src/main/java/org/dragonet/proxy/network/session/ProxySession.java @@ -226,7 +226,7 @@ private void fetchOurSkin() { ImageData skinData = SkinUtils.fetchSkin(this, profile); if (skinData == null) return; - ImageData capeData = SkinUtils.fetchUnofficialCape(profile); + ImageData capeData = SkinUtils.fetchCape(this, profile); if(capeData == null) capeData = ImageData.EMPTY; GameProfile.TextureModel model = null; diff --git a/proxy/src/main/java/org/dragonet/proxy/network/session/cache/PlayerListCache.java b/proxy/src/main/java/org/dragonet/proxy/network/session/cache/PlayerListCache.java index 518409b4b..5fad05f15 100644 --- a/proxy/src/main/java/org/dragonet/proxy/network/session/cache/PlayerListCache.java +++ b/proxy/src/main/java/org/dragonet/proxy/network/session/cache/PlayerListCache.java @@ -47,6 +47,7 @@ public class PlayerListCache implements Cache { private Object2LongMap playerEntityIds = new Object2LongOpenHashMap<>(); private Object2ObjectMap remoteSkinCache = new Object2ObjectOpenHashMap<>(); + private Object2ObjectMap remoteCapeCache = new Object2ObjectOpenHashMap<>(); public void updateDisplayName(GameProfile profile, String displayName) { playerInfo.get(profile.getId()).setDisplayName(displayName); diff --git a/proxy/src/main/java/org/dragonet/proxy/network/translator/java/player/PCSpawnPlayerTranslator.java b/proxy/src/main/java/org/dragonet/proxy/network/translator/java/player/PCSpawnPlayerTranslator.java index a0ed725f3..955b38ff3 100644 --- a/proxy/src/main/java/org/dragonet/proxy/network/translator/java/player/PCSpawnPlayerTranslator.java +++ b/proxy/src/main/java/org/dragonet/proxy/network/translator/java/player/PCSpawnPlayerTranslator.java @@ -61,6 +61,9 @@ public void translate(ProxySession session, ServerSpawnPlayerPacket packet) { cachedPlayer.setJavaUuid(packet.getUuid()); cachedPlayer.setPosition(Vector3f.from(packet.getX(), packet.getY(), packet.getZ())); cachedPlayer.setRotation(Vector3f.from(packet.getYaw(), packet.getPitch(), 0)); + if(cachedPlayer.getProfile().getName() != null) { + cachedPlayer.getMetadata().put(EntityData.NAMETAG, cachedPlayer.getProfile().getName()); + } cachedPlayer.spawn(session); if(session.getProxy().getConfiguration().getRemoteAuthType() == RemoteAuthType.OFFLINE) { @@ -73,7 +76,7 @@ public void translate(ProxySession session, ServerSpawnPlayerPacket packet) { ImageData skinData = SkinUtils.fetchSkin(session, profile); if (skinData == null) return; - ImageData capeData = SkinUtils.fetchUnofficialCape(profile); + ImageData capeData = SkinUtils.fetchCape(session, profile); if(capeData == null) capeData = ImageData.EMPTY; GameProfile.TextureModel model = null; diff --git a/proxy/src/main/java/org/dragonet/proxy/util/SkinUtils.java b/proxy/src/main/java/org/dragonet/proxy/util/SkinUtils.java index b1949ef2d..8da233140 100644 --- a/proxy/src/main/java/org/dragonet/proxy/util/SkinUtils.java +++ b/proxy/src/main/java/org/dragonet/proxy/util/SkinUtils.java @@ -26,6 +26,7 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import net.minidev.json.JSONValue; import org.dragonet.proxy.DragonProxy; import org.dragonet.proxy.network.session.ProxySession; import org.dragonet.proxy.network.session.cache.PlayerListCache; @@ -33,43 +34,31 @@ import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.Collections; import java.util.UUID; @Log4j2 public class SkinUtils { - private static final String NORMAL_RESOURCE_PATCH = "ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiZGVmYXVsdCIgOiAiZ2VvbWV0cnkuaHVtYW5vaWQuY3VzdG9tIgogICB9Cn0K"; - private static final String SLIM_RESOURCE_PATCH = "ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiZGVmYXVsdCIgOiAiZ2VvbWV0cnkuaHVtYW5vaWQuY3VzdG9tU2xpbSIKICAgfQp9"; - private static final SessionService service = new SessionService(); public static ImageData STEVE_SKIN; static { try { - STEVE_SKIN = parseBufferedImage(ImageIO.read(DragonProxy.class.getClassLoader().getResource("skin_steve.png"))); + STEVE_SKIN = parseBufferedImage(ImageIO.read(DragonProxy.class.getClassLoader().getResource("skin_steve.png")), false); } catch (IOException e) { e.printStackTrace(); } } public static SerializedSkin createSkinEntry(ImageData skinImage, GameProfile.TextureModel model, ImageData capeImage) { - // Skin Geometry is hard coded otherwise players will turn invisible if joining with custom models - String skinResourcePatch = NORMAL_RESOURCE_PATCH; - if(model == GameProfile.TextureModel.SLIM) { - skinResourcePatch = SLIM_RESOURCE_PATCH; - } - String randomId = UUID.randomUUID().toString(); return SerializedSkin.of( randomId, - new String(Base64.getDecoder().decode(skinResourcePatch)), + convertLegacyGeometryName((model == GameProfile.TextureModel.SLIM) ? "Slim" : ""), skinImage, Collections.emptyList(), capeImage, @@ -104,7 +93,7 @@ public static ImageData fetchSkin(ProxySession session, GameProfile profile) { } if(texture != null) { try { - ImageData skin = parseBufferedImage(ImageIO.read(new URL(texture.getURL()))); + ImageData skin = parseBufferedImage(ImageIO.read(new URL(texture.getURL())), false); playerListCache.getRemoteSkinCache().put(profile.getId(), skin); // Cache the skin return skin; } catch (IOException e) { @@ -115,23 +104,50 @@ public static ImageData fetchSkin(ProxySession session, GameProfile profile) { } /** - * Checks if a player has an unofficial cape and if so downloads it from + * Checks if a player has a mojang cape or unofficial cape and if so downloads it from * their servers */ - public static ImageData fetchUnofficialCape(GameProfile profile) { - for(CapeServers server : CapeServers.values()) { - try { - URL url = new URL(server.getUrl(profile)); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + public static ImageData fetchCape(ProxySession session, GameProfile profile) { + // TODO: HANDLE RATE LIMITING + PlayerListCache playerListCache = session.getPlayerListCache(); - if (connection.getResponseCode() == 404) { - return null; - } + // Check if the cape is already cached + if(playerListCache.getRemoteCapeCache().containsKey(profile.getId())) { + //log.warn("Retrieving from cache: " + profile.getName()); + return playerListCache.getRemoteCapeCache().get(profile.getId()); + } - return parseBufferedImage(ImageIO.read(connection.getInputStream())); - } catch (IOException e) {} + GameProfile.Texture texture; + try { + texture = profile.getTexture(GameProfile.TextureType.CAPE); + } catch (PropertyException e) { + return null; } - return null; + + if(texture != null) { + try { + ImageData cape = parseBufferedImage(ImageIO.read(new URL(texture.getURL())), false); + playerListCache.getRemoteCapeCache().put(profile.getId(), cape); // Cache the cape + return cape; + } catch (IOException e) { + log.warn("Failed to fetch cape for player " + profile.getName() + ": " + e.getMessage()); + } + } else { + for (CapeServers server : CapeServers.values()) { + try { + URL url = new URL(server.getUrl(profile)); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + if (connection.getResponseCode() != 404) { + log.warn(String.format("%s has cape at %s", profile.getName(), texture.getURL())); + return parseBufferedImage(ImageIO.read(connection.getInputStream()), true); + } + } catch (IOException e) { + log.warn("Failed to fetch cape for player " + profile.getName() + ": " + e.getMessage()); + } + } + } + return ImageData.EMPTY; } @RequiredArgsConstructor @@ -158,8 +174,22 @@ private enum CapeUrlType { USERNAME } - private static ImageData parseBufferedImage(BufferedImage image) { - FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + private static ImageData parseBufferedImage(BufferedImage image, boolean cape) { + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + int bedrockSkinSize = (imageWidth * imageHeight) * 4; + + //Capes need to be 64x32, 128x64 etc otherwise they will render weird. This is an issue i had on MinecratCapes + if(cape) { + imageWidth = 64; + imageHeight = 32; + while((imageWidth < image.getWidth()) || (imageHeight < image.getHeight())) { + imageWidth *= 2; + imageHeight *= 2; + } + } + + FastByteArrayOutputStream out = new FastByteArrayOutputStream(bedrockSkinSize); for(int y = 0; y < image.getHeight(); ++y) { for(int x = 0; x < image.getWidth(); ++x) { Color color = new Color(image.getRGB(x, y), true); @@ -169,7 +199,13 @@ private static ImageData parseBufferedImage(BufferedImage image) { out.write(color.getAlpha()); } } + image.flush(); + return ImageData.of(image.getWidth(), image.getHeight(), out.array); } + + private static String convertLegacyGeometryName(String geometryModel) { + return "{\"geometry\" : {\"default\" : \"geometry.humanoid.custom" + JSONValue.escape(geometryModel) + "\"}}"; + } }