From 6d617216c7f26772ddcc25f3e8fcad520d9f54c1 Mon Sep 17 00:00:00 2001
From: NekoCaffeine <nekocaffeine@qq.com>
Date: Mon, 17 Jan 2022 00:04:54 +0800
Subject: [PATCH] fix #1130

---
 .../minecraft/skin/fx/SkinAnimation.java      |  1 -
 .../mickey/minecraft/skin/fx/SkinCanvas.java  | 54 ++++++++++++-----
 .../mickey/minecraft/skin/fx/SkinCube.java    | 58 +++++++++----------
 .../mickey/minecraft/skin/fx/SkinHelper.java  | 44 +++++++-------
 .../ui/account/OfflineAccountSkinPane.java    | 11 ++--
 5 files changed, 97 insertions(+), 71 deletions(-)

diff --git a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinAnimation.java b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinAnimation.java
index 798922a36..f7f49b234 100644
--- a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinAnimation.java
+++ b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinAnimation.java
@@ -12,7 +12,6 @@ public class SkinAnimation {
     protected int weight, left;
     protected List<SkinTransition> transitions;
 
-    @Deprecated
     public SkinAnimation() {
         this.transitions = new ArrayList<>();
     }
diff --git a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java
index 81215599b..94b2504fb 100644
--- a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java
+++ b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java
@@ -4,6 +4,7 @@ import javafx.scene.*;
 import javafx.scene.image.Image;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.input.ScrollEvent;
+import javafx.scene.paint.Color;
 import javafx.scene.paint.Material;
 import javafx.scene.paint.PhongMaterial;
 import javafx.scene.shape.Shape3D;
@@ -11,6 +12,8 @@ import javafx.scene.transform.Rotate;
 import javafx.scene.transform.Scale;
 import javafx.scene.transform.Translate;
 
+import org.jetbrains.annotations.Nullable;
+
 public class SkinCanvas extends Group {
 
     public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
@@ -22,7 +25,7 @@ public class SkinCanvas extends Group {
     public static final SkinCube STEVEN_LARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
     public static final SkinCube STEVEN_RARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
 
-    protected Image srcSkin, skin;
+    protected Image srcSkin, skin, srcCape, cape;
     protected boolean isSlim;
 
     protected double preW, preH;
@@ -45,6 +48,8 @@ public class SkinCanvas extends Group {
     public final SkinCube llegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 16F / 64F, 48F / 64F, 0F, false);
     public final SkinCube rlegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 0F, 16F / 64F, 0F, false);
 
+    public final SkinCube capeCube = new SkinCube(10, 16, 1, 22F / 64F, 17F / 32F, 0F, 0F, 0F, false);
+
     public final SkinGroup head = new SkinGroup(
             new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.X_AXIS),
             new Rotate(0, Rotate.Y_AXIS),
@@ -82,6 +87,13 @@ public class SkinCanvas extends Group {
             rlegOuter, rlegInside
     );
 
+    public final SkinGroup capeGroup = new SkinGroup(
+            new Rotate(0, 0, -capeCube.getHeight() / 2, 0, Rotate.X_AXIS),
+            new Rotate(0, Rotate.Y_AXIS),
+            new Rotate(0, Rotate.Z_AXIS),
+            capeCube
+    );
+
     protected PerspectiveCamera camera = new PerspectiveCamera(true);
 
     protected Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
@@ -90,10 +102,10 @@ public class SkinCanvas extends Group {
     protected Translate translate = new Translate(0, 0, -80);
     protected Scale scale = new Scale(1, 1);
 
-    protected SkinAnimationPlayer animationplayer = new SkinAnimationPlayer();
+    protected SkinAnimationPlayer animationPlayer = new SkinAnimationPlayer();
 
-    public SkinAnimationPlayer getAnimationplayer() {
-        return animationplayer;
+    public SkinAnimationPlayer getAnimationPlayer() {
+        return animationPlayer;
     }
 
     public Image getSrcSkin() {
@@ -104,20 +116,22 @@ public class SkinCanvas extends Group {
         return skin;
     }
 
-    public void updateSkin(Image skin, boolean isSlim) {
+    public void updateSkin(Image skin, boolean isSlim, final @Nullable Image cape) {
         if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) {
             this.srcSkin = skin;
             this.skin = SkinHelper.x32Tox64(skin);
+            this.srcCape = cape;
+            this.cape = cape == null ? null : cape.getWidth() < 256 ? SkinHelper.enlarge(cape, 4, 8) : cape;
             int multiple = Math.max((int) (1024 / skin.getWidth()), 1);
             if (multiple > 1)
-                this.skin = SkinHelper.enlarge(this.skin, multiple);
+                this.skin = SkinHelper.enlarge(this.skin, multiple, multiple);
             if (this.isSlim != isSlim)
-                updateSkinModel(isSlim);
+                updateSkinModel(isSlim, cape != null);
             bindMaterial(root);
         }
     }
 
-    protected void updateSkinModel(boolean isSlim) {
+    protected void updateSkinModel(boolean isSlim, boolean hasCape) {
         this.isSlim = isSlim;
         FunctionHelper.alwaysB(SkinMultipleCubes::setWidth, isSlim ? 3 : 4, larmOuter, rarmOuter);
         FunctionHelper.alwaysB(SkinCube::setWidth, isSlim ? 3D : 4D, larmInside, rarmInside);
@@ -134,6 +148,8 @@ public class SkinCanvas extends Group {
 
         larm.getZRotate().setPivotX(-larmInside.getWidth() / 2);
         rarm.getZRotate().setPivotX(+rarmInside.getWidth() / 2);
+
+        capeGroup.setVisible(hasCape);
     }
 
     public SkinCanvas(double preW, double preH) {
@@ -149,17 +165,17 @@ public class SkinCanvas extends Group {
         init();
     }
 
-    protected Material createMaterial() {
+    protected Material createMaterial(final Image image) {
         PhongMaterial material = new PhongMaterial();
-        material.setDiffuseMap(skin);
+        material.setDiffuseMap(image);
         return material;
     }
 
     protected void bindMaterial(Group group) {
-        Material material = createMaterial();
+        Material material = createMaterial(skin);
         for (Node node : group.getChildren())
             if (node instanceof Shape3D)
-                ((Shape3D) node).setMaterial(material);
+                ((Shape3D) node).setMaterial(node == capeCube ? createMaterial(cape) : material);
             else if (node instanceof SkinMultipleCubes)
                 ((SkinMultipleCubes) node).updateSkin(skin);
             else if (node instanceof Group)
@@ -178,6 +194,11 @@ public class SkinCanvas extends Group {
         lleg.setTranslateY(+(bodyInside.getHeight() + llegInside.getHeight()) / 2);
         rleg.setTranslateY(+(bodyInside.getHeight() + rlegInside.getHeight()) / 2);
 
+        capeGroup.setTranslateY(+(capeCube.getHeight() - bodyOuter.getHeight()) / 2);
+        capeGroup.setTranslateZ(-(bodyInside.getDepth() + bodyOuter.getDepth()) / 2);
+
+        capeGroup.getTransforms().addAll(new Rotate(180, Rotate.Y_AXIS), new Rotate(10, Rotate.X_AXIS));
+
         root.getTransforms().addAll(xRotate);
 
         root.getChildren().addAll(
@@ -186,15 +207,20 @@ public class SkinCanvas extends Group {
                 larm,
                 rarm,
                 lleg,
-                rleg
+                rleg,
+                capeGroup
         );
-        updateSkin(skin, false);
+        updateSkin(skin, false, null);
 
         return root;
     }
 
     protected SubScene createSubScene() {
         Group group = new Group();
+
+        AmbientLight light = new AmbientLight(Color.WHITE);
+        group.getChildren().add(light);
+
         group.getChildren().add(createPlayerModel());
         group.getTransforms().add(zRotate);
 
diff --git a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCube.java b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCube.java
index 226ffca3e..efd000b12 100644
--- a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCube.java
+++ b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCube.java
@@ -1,9 +1,9 @@
 package moe.mickey.minecraft.skin.fx;
 
-import javafx.scene.image.Image;
 import javafx.scene.shape.Mesh;
 import javafx.scene.shape.MeshView;
 import javafx.scene.shape.TriangleMesh;
+
 import org.apache.commons.lang3.ArrayUtils;
 
 public class SkinCube extends MeshView {
@@ -20,61 +20,60 @@ public class SkinCube extends MeshView {
             width /= 2F;
             height /= 2F;
             depth /= 2F;
-
             return new float[]{
-                    -width, -height, depth,        // P0
-                    width, -height, depth,        // P1
-                    -width, height, depth,        // P2
-                    width, height, depth,        // P3
-                    -width, -height, -depth,    // P4
-                    width, -height, -depth,    // P5
-                    -width, height, -depth,    // P6
-                    width, height, -depth        // P7
+                    -width, -height, +depth, // P0
+                    +width, -height, +depth, // P1
+                    -width, +height, +depth, // P2
+                    +width, +height, +depth, // P3
+                    -width, -height, -depth, // P4
+                    +width, -height, -depth, // P5
+                    -width, +height, -depth, // P6
+                    +width, +height, -depth  // P7
             };
         }
 
         public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
-                                              float startX, float startY, boolean isSlim) {
+                float startX, float startY, boolean isSlim) {
             float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
                     top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
                     bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
             return new float[]{
                     top_x, top_y,                                    // T0  ---
-                    top_x + half_width, top_y,                        // T1   |
-                    top_x + half_width * 2, top_y,                    // T2  ---
-                    bottom_x, middle_y,                                // T3  ---
-                    bottom_x + half_depth, middle_y,                // T4   |
+                    top_x + half_width, top_y,                       // T1   |
+                    top_x + half_width * 2, top_y,                   // T2  ---
+                    bottom_x, middle_y,                              // T3  ---
+                    bottom_x + half_depth, middle_y,                 // T4   |
                     bottom_x + half_depth + half_width, middle_y,    // T5   |
-                    bottom_x + scaleX - arm4, middle_y,                // T6   |
-                    bottom_x + scaleX, middle_y,                    // T7  ---
-                    bottom_x, bottom_y,                                // T8  ---
-                    bottom_x + half_depth, bottom_y,                // T9   |
+                    bottom_x + scaleX - arm4, middle_y,              // T6   |
+                    bottom_x + scaleX, middle_y,                     // T7  ---
+                    bottom_x, bottom_y,                              // T8  ---
+                    bottom_x + half_depth, bottom_y,                 // T9   |
                     bottom_x + half_depth + half_width, bottom_y,    // T10  |
-                    bottom_x + scaleX - arm4, bottom_y,                // T11  |
-                    bottom_x + scaleX, bottom_y                        // T12 ---
+                    bottom_x + scaleX - arm4, bottom_y,              // T11  |
+                    bottom_x + scaleX, bottom_y                      // T12 ---
             };
         }
 
         public static int[] createFaces() {
-            int[] faces = new int[]{
+            int[] faces = {
                     // TOP
                     5, 0, 4, 1, 0, 5,    //P5,T0, P4,T1, P0,T5
                     5, 0, 0, 5, 1, 4,    //P5,T0, P0,T5, P1,T4
                     // RIGHT
-                    0, 5, 4, 6, 6, 11,    //P0,T4 ,P4,T3, P6,T8
-                    0, 5, 6, 11, 2, 10,    //P0,T4 ,P6,T8, P2,T9
+                    0, 5, 4, 6, 6, 11,   //P0,T4 ,P4,T3, P6,T8
+                    0, 5, 6, 11, 2, 10,  //P0,T4 ,P6,T8, P2,T9
                     // FRONT
-                    1, 4, 0, 5, 2, 10,    //P1,T5, P0,T4, P2,T9
-                    1, 4, 2, 10, 3, 9,    //P1,T5, P2,T9, P3,T10
+                    1, 4, 0, 5, 2, 10,   //P1,T5, P0,T4, P2,T9
+                    1, 4, 2, 10, 3, 9,   //P1,T5, P2,T9, P3,T10
                     // LEFT
                     5, 3, 1, 4, 3, 9,    //P5,T6, P1,T5, P3,T10
                     5, 3, 3, 9, 7, 8,    //P5,T6, P3,T10,P7,T11
                     // BACK
-                    4, 6, 5, 7, 7, 12,    //P4,T6, P5,T7, P7,T12
-                    4, 6, 7, 12, 6, 11,    //P4,T6, P7,T12,P6,T11
+                    4, 6, 5, 7, 7, 12,   //P4,T6, P5,T7, P7,T12
+                    4, 6, 7, 12, 6, 11,  //P4,T6, P7,T12,P6,T11
                     // BOTTOM
                     3, 5, 2, 6, 6, 2,    //P3,T2, P2,T1, P6,T5
-                    3, 5, 6, 2, 7, 1    //P3,T2, P6,T5, P7,T6
+                    3, 5, 6, 2, 7, 1     //P3,T2, P6,T5, P7,T6
             };
 
             int[] copy = faces.clone();
@@ -91,7 +90,6 @@ public class SkinCube extends MeshView {
 
     private double width, height, depth;
     private boolean isSlim;
-    private Image skin;
     private Mesh model;
 
     public SkinCube(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, float enlarge, boolean isSlim) {
diff --git a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinHelper.java b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinHelper.java
index 83f41e18c..5125f3e0a 100644
--- a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinHelper.java
+++ b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinHelper.java
@@ -10,12 +10,12 @@ public final class SkinHelper {
     private SkinHelper() {
     }
 
-    public static class PixelCopyer {
+    public static class PixelCopier {
 
         protected Image srcImage;
         protected WritableImage newImage;
 
-        public PixelCopyer(Image srcImage, WritableImage newImage) {
+        public PixelCopier(Image srcImage, WritableImage newImage) {
             this.srcImage = srcImage;
             this.newImage = newImage;
         }
@@ -70,53 +70,53 @@ public final class SkinHelper {
     }
 
     public static Image x32Tox64(Image srcSkin) {
-        if (srcSkin.getHeight() == 64)
+        if (srcSkin.getHeight() == srcSkin.getWidth())
             return srcSkin;
 
         WritableImage newSkin = new WritableImage((int) srcSkin.getWidth(), (int) srcSkin.getHeight() * 2);
-        PixelCopyer copyer = new PixelCopyer(srcSkin, newSkin);
+        PixelCopier copier = new PixelCopier(srcSkin, newSkin);
         // HEAD & HAT
-        copyer.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
+        copier.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
         // LEFT-LEG
-        x32Tox64(copyer, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
+        x32Tox64(copier, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
         // RIGHT-LEG
-        copyer.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
+        copier.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
         // BODY
-        copyer.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
+        copier.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
         // LEFT-ARM
-        x32Tox64(copyer, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
+        x32Tox64(copier, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
         // RIGHT-ARM
-        copyer.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
+        copier.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
 
         return newSkin;
     }
 
-    static void x32Tox64(PixelCopyer copyer, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
+    static void x32Tox64(PixelCopier copier, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
         // TOP
-        copyer.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
+        copier.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
         // BOTTOM
-        copyer.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
+        copier.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
         // INS
-        copyer.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
+        copier.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
         // OUTS
-        copyer.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
+        copier.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
         // FRONT
-        copyer.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
+        copier.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
         // BACK
-        copyer.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
+        copier.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
     }
 
-    public static Image enlarge(Image srcSkin, int multiple) {
-        WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multiple, (int) srcSkin.getHeight() * multiple);
+    public static Image enlarge(Image srcSkin, int multipleX, int multipleY) {
+        WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multipleX, (int) srcSkin.getHeight() * multipleY);
         PixelReader reader = srcSkin.getPixelReader();
         PixelWriter writer = newSkin.getPixelWriter();
 
         for (int x = 0, lenX = (int) srcSkin.getWidth(); x < lenX; x++)
             for (int y = 0, lenY = (int) srcSkin.getHeight(); y < lenY; y++)
-                for (int mx = 0; mx < multiple; mx++)
-                    for (int my = 0; my < multiple; my++) {
+                for (int mx = 0; mx < multipleX; mx++)
+                    for (int my = 0; my < multipleY; my++) {
                         int argb = reader.getArgb(x, y);
-                        writer.setArgb(x * multiple + mx, y * multiple + my, argb);
+                        writer.setArgb(x * multipleX + mx, y * multipleY + my, argb);
                     }
 
         return newSkin;
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java
index 54e0259d9..2e4049f8d 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/OfflineAccountSkinPane.java
@@ -76,7 +76,7 @@ public class OfflineAccountSkinPane extends StackPane {
         canvasPane.setPrefWidth(300);
         canvasPane.setPrefHeight(300);
         pane.setCenter(canvas);
-        canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
+        canvas.getAnimationPlayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
         canvas.enableRotation(.5);
 
         canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
@@ -136,11 +136,14 @@ public class OfflineAccountSkinPane extends StackPane {
                             LOG.log(Level.WARNING, "Failed to load skin", exception);
                             Controllers.showToast(i18n("message.failed"));
                         } else {
-                            if (result == null || result.getSkin() == null) {
-                                canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
+                            if (result == null || result.getSkin() == null && result.getCape() == null) {
+                                canvas.updateSkin(getDefaultTexture(), isDefaultSlim(), null);
                                 return;
                             }
-                            canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
+                            canvas.updateSkin(
+                                    result.getSkin() != null ? new Image(result.getSkin().getInputStream()) : getDefaultTexture(),
+                                    result.getModel() == TextureModel.ALEX,
+                                    result.getCape() != null ? new Image(result.getCape().getInputStream()) : null);
                         }
                     }).start();
         }, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());