diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java index d02d2d3..b79aa32 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java @@ -11,7 +11,7 @@ import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; import com.skcraft.launcher.auth.microsoft.model.TokenResponse; import com.skcraft.launcher.auth.microsoft.model.XboxAuthorization; -import com.skcraft.launcher.auth.skin.VisageSkinService; +import com.skcraft.launcher.auth.skin.MinecraftSkinService; import com.skcraft.launcher.util.HttpRequest; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -103,10 +103,9 @@ if (previous != null && previous.getAvatarImage() != null) { session.setAvatarImage(previous.getAvatarImage()); } else { - session.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid())); + session.setAvatarImage(MinecraftSkinService.fetchSkinHead(profile)); } - return session; } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java index 874d159..8d39800 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -7,7 +7,9 @@ package com.skcraft.launcher.auth; import com.fasterxml.jackson.annotation.*; -import com.skcraft.launcher.auth.skin.VisageSkinService; +import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer; +import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; +import com.skcraft.launcher.auth.skin.MinecraftSkinService; import com.skcraft.launcher.util.HttpRequest; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -60,7 +62,10 @@ if (previous != null && previous.getAvatarImage() != null) { profile.setAvatarImage(previous.getAvatarImage()); } else { - profile.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid())); + McProfileResponse skinProfile = MinecraftServicesAuthorizer + .getUserProfile("Bearer " + response.getAccessToken()); + + profile.setAvatarImage(MinecraftSkinService.fetchSkinHead(skinProfile)); } return profile; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java index 1f8bc80..528fbbf 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java @@ -28,8 +28,13 @@ public static McProfileResponse getUserProfile(McAuthResponse auth) throws IOException, InterruptedException, AuthenticationException { + return getUserProfile(auth.getAuthorization()); + } + + public static McProfileResponse getUserProfile(String authorization) + throws IOException, InterruptedException, AuthenticationException { return HttpRequest.get(MC_SERVICES_PROFILE) - .header("Authorization", auth.getAuthorization()) + .header("Authorization", authorization) .execute() .expectResponseCodeOr(200, req -> { McServicesError error = req.returnContent().asJson(McServicesError.class); diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java index 7c22645..f2446d9 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java @@ -1,12 +1,28 @@ package com.skcraft.launcher.auth.microsoft.model; +import com.beust.jcommander.internal.Lists; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import java.util.List; + @Data @JsonIgnoreProperties(ignoreUnknown = true) public class McProfileResponse { @JsonProperty("id") private String uuid; private String name; + private List skins = Lists.newArrayList(); + + public Skin getActiveSkin() { + return skins.stream().filter(skin -> skin.getState().equals("ACTIVE")).findFirst().orElse(null); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Skin { + private String state; + private String url; + private String variant; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java new file mode 100644 index 0000000..831155e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java @@ -0,0 +1,30 @@ +package com.skcraft.launcher.auth.skin; + +import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; +import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; + +import java.io.IOException; +import java.util.logging.Level; + +@Log +public class MinecraftSkinService { + static byte[] downloadSkin(String textureUrl) throws IOException, InterruptedException { + return HttpRequest.get(HttpRequest.url(textureUrl)) + .execute() + .expectResponseCode(200) + .returnContent() + .asBytes(); + } + + public static byte[] fetchSkinHead(McProfileResponse profile) throws InterruptedException { + try { + byte[] skin = downloadSkin(profile.getActiveSkin().getUrl()); + + return SkinProcessor.renderHead(skin); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to download or process skin.", e); + return null; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java new file mode 100644 index 0000000..7622329 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java @@ -0,0 +1,27 @@ +package com.skcraft.launcher.auth.skin; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SkinProcessor { + public static byte[] renderHead(byte[] skinData) throws IOException { + BufferedImage skin = ImageIO.read(new ByteArrayInputStream(skinData)); + + BufferedImage result = new BufferedImage(32, 32, BufferedImage.TYPE_INT_RGB); + Graphics graphics = result.getGraphics(); + + // Draw bottom head layer + graphics.drawImage(skin, 0, 0, 32, 32, 8, 8, 16, 16, null); + // Draw top head layer + graphics.drawImage(skin, 0, 0, 32, 32, 40, 8, 48, 16, null); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(result, "png", outputStream); + + return outputStream.toByteArray(); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java index 2f59cd3..2c7e770 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java @@ -1,12 +1,15 @@ package com.skcraft.launcher.auth.skin; import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; import javax.annotation.Nullable; import java.io.IOException; +import java.util.logging.Level; import static com.skcraft.launcher.util.HttpRequest.url; +@Log public class VisageSkinService { @Nullable public static byte[] fetchSkinHead(String uuid) throws InterruptedException { @@ -19,6 +22,7 @@ .returnContent() .asBytes(); } catch (IOException e) { + log.log(Level.WARNING, "Failed to download or process skin from Visage.", e); return null; } }