From f63d1cc3ece6f0ad5ce461a25d57411efaa71069 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 11 Apr 2024 06:10:37 +0100 Subject: [PATCH] add head endpoint and finish the body renderer --- .../java/cc.fascinated/common/ImageUtils.java | 14 +++ .../model/cache/CachedPlayer.java | 4 +- .../java/cc.fascinated/model/player/Skin.java | 103 ++++++++++++++++-- .../cc.fascinated/service/PlayerService.java | 3 +- .../service/skin/SkinRenderer.java | 55 ++++++++-- .../service/skin/impl/BodyRenderer.java | 55 ++++++++++ ...{SquareRenderer.java => HeadRenderer.java} | 33 ++---- .../skin/impl/IsometricHeadRenderer.java | 27 ++--- 8 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java rename src/main/java/cc.fascinated/service/skin/impl/{SquareRenderer.java => HeadRenderer.java} (57%) diff --git a/src/main/java/cc.fascinated/common/ImageUtils.java b/src/main/java/cc.fascinated/common/ImageUtils.java index 565425c..4345be0 100644 --- a/src/main/java/cc.fascinated/common/ImageUtils.java +++ b/src/main/java/cc.fascinated/common/ImageUtils.java @@ -24,4 +24,18 @@ public class ImageUtils { graphics.dispose(); return scaled; } + + /** + * Flip an image. + * + * @param src the source image + * @return the flipped image + */ + public static BufferedImage flip(@NotNull final BufferedImage src) { + BufferedImage flipped = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = flipped.createGraphics(); + graphics.drawImage(src, src.getWidth(), 0, 0, src.getHeight(), 0, 0, src.getWidth(), src.getHeight(), null); + graphics.dispose(); + return flipped; + } } diff --git a/src/main/java/cc.fascinated/model/cache/CachedPlayer.java b/src/main/java/cc.fascinated/model/cache/CachedPlayer.java index 32ca42c..3d40a95 100644 --- a/src/main/java/cc.fascinated/model/cache/CachedPlayer.java +++ b/src/main/java/cc.fascinated/model/cache/CachedPlayer.java @@ -27,8 +27,8 @@ public final class CachedPlayer extends Player implements Serializable { */ private long cached; - public CachedPlayer(UUID uniqueId, String username, Skin skin, Cape cape, MojangProfile.ProfileProperty[] rawProperties, long cached) { - super(uniqueId, username, skin, cape, rawProperties); + public CachedPlayer(UUID uniqueId, String trimmedUniqueId, String username, Skin skin, Cape cape, MojangProfile.ProfileProperty[] rawProperties, long cached) { + super(uniqueId, trimmedUniqueId, username, skin, cape, rawProperties); this.cached = cached; } } \ No newline at end of file diff --git a/src/main/java/cc.fascinated/model/player/Skin.java b/src/main/java/cc.fascinated/model/player/Skin.java index 4ab4fd9..66e97a8 100644 --- a/src/main/java/cc.fascinated/model/player/Skin.java +++ b/src/main/java/cc.fascinated/model/player/Skin.java @@ -4,7 +4,8 @@ import cc.fascinated.common.PlayerUtils; import cc.fascinated.config.Config; import cc.fascinated.exception.impl.BadRequestException; import cc.fascinated.service.skin.SkinRenderer; -import cc.fascinated.service.skin.impl.SquareRenderer; +import cc.fascinated.service.skin.impl.BodyRenderer; +import cc.fascinated.service.skin.impl.HeadRenderer; import cc.fascinated.service.skin.impl.IsometricHeadRenderer; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -14,6 +15,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Map; @@ -36,6 +40,11 @@ public class Skin { */ private Model model; + /** + * The legacy status of the skin + */ + private boolean isLegacy = false; + /** * The skin image for the skin */ @@ -53,6 +62,14 @@ public class Skin { this.model = model; this.skinImage = PlayerUtils.getSkinImage(url); + if (this.skinImage != null) { + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.skinImage)); + if (image.getWidth() == 64 && image.getHeight() == 32) { // Using the old skin format + this.isLegacy = true; + } + } catch (Exception ignored) {} + } } /** @@ -91,17 +108,14 @@ public class Skin { */ @Getter @AllArgsConstructor public enum Parts { + FACE(new HeadRenderer()), + HEAD(new IsometricHeadRenderer()), + BODY(new BodyRenderer()); /** - * Head parts + * The skin part renderer for the part. */ - HEAD(new SquareRenderer(8, 8, 8)), - HEAD_ISOMETRIC(new IsometricHeadRenderer()); - - /** - * The skin part parser for the part. - */ - private final SkinRenderer skinPartParser; + private final SkinRenderer skinRenderer; /** * Gets the name of the part. @@ -129,6 +143,77 @@ public class Skin { } } + @AllArgsConstructor @Getter + public enum PartPosition { + /** + * Skin postions + */ + HEAD(8, 8, 8, 8, new LegacyPartPositionData(8, 8, false)), + HEAD_TOP(8, 0, 8, 8, null), + HEAD_FRONT(8, 8, 8, 8, null), + HEAD_RIGHT(0, 8, 8, 8, null), + + BODY(20, 20, 8, 12, new LegacyPartPositionData(20, 20, false)), + BODY_BACK(20, 36, 8, 12, null), + BODY_LEFT(32, 52, 8, 12, null), + BODY_RIGHT(44, 20, 8, 12, null), + + RIGHT_ARM(44, 20, 4, 12, new LegacyPartPositionData(44, 20, false)), + LEFT_ARM(36, 52, 4, 12, new LegacyPartPositionData(43, 20, true)), + + RIGHT_LEG(4, 20, 4, 12, new LegacyPartPositionData(4, 20, false)), + LEFT_LEG(20, 52, 4, 12, new LegacyPartPositionData(3, 20, true)), + + /** + * Skin overlay (layer) positions + */ + HEAD_OVERLAY(40, 8, 8, 8, null), + // todo: finish these below + HEAD_OVERLAY_TOP(40, 0, 40, 0, null), + HEAD_OVERLAY_FRONT(40, 8, 40, 8, null), + HEAD_OVERLAY_RIGHT(32, 8, 32, 8, null), + + BODY_OVERLAY(20, 32, 20, 12, null), + BODY_OVERLAY_BACK(20, 36, 20, 12, null), + BODY_OVERLAY_LEFT(32, 52, 32, 12, null), + BODY_OVERLAY_RIGHT(44, 32, 44, 12, null), + + RIGHT_ARM_OVERLAY(44, 32, 44, 12, null), + LEFT_ARM_OVERLAY(36, 52, 36, 12, null), + + RIGHT_LEG_OVERLAY(4, 32, 4, 12, null), + LEFT_LEG_OVERLAY(20, 52, 20, 12, null); + + /** + * The x, and y position of the part. + */ + private final int x, y; + + /** + * The width and height of the part. + */ + private final int width, height; + + /* + * The part position data for legacy skins. + * This can be null to use the default position. + */ + private final LegacyPartPositionData legacyData; + } + + @AllArgsConstructor @Getter + public static class LegacyPartPositionData { + /** + * The x, and y position of the part. + */ + private int x, y; + + /** + * Should the part be flipped? + */ + private boolean flipped; + } + /** * The model of the skin. */ diff --git a/src/main/java/cc.fascinated/service/PlayerService.java b/src/main/java/cc.fascinated/service/PlayerService.java index 5de50a5..7af6da2 100644 --- a/src/main/java/cc.fascinated/service/PlayerService.java +++ b/src/main/java/cc.fascinated/service/PlayerService.java @@ -71,6 +71,7 @@ public class PlayerService { Tuple skinAndCape = mojangProfile.getSkinAndCape(); CachedPlayer player = new CachedPlayer( uuid, // Player UUID + UUIDUtils.removeDashes(uuid), // Trimmed UUID mojangProfile.getName(), // Player Name skinAndCape.getLeft(), // Skin skinAndCape.getRight(), // Cape @@ -134,7 +135,7 @@ public class PlayerService { } long before = System.currentTimeMillis(); - byte[] skinPartBytes = part.getSkinPartParser().renderPart(player.getSkin(), part.getName(), renderOverlay, size); + byte[] skinPartBytes = part.getSkinRenderer().renderPart(player.getSkin(), part.getName(), renderOverlay, size); log.info("Took {}ms to render skin part {} for player: {}", System.currentTimeMillis() - before, part.getName(), player.getUniqueId()); CachedPlayerSkinPart skinPart = new CachedPlayerSkinPart( key, diff --git a/src/main/java/cc.fascinated/service/skin/SkinRenderer.java b/src/main/java/cc.fascinated/service/skin/SkinRenderer.java index be21246..3606a1f 100644 --- a/src/main/java/cc.fascinated/service/skin/SkinRenderer.java +++ b/src/main/java/cc.fascinated/service/skin/SkinRenderer.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.extern.log4j.Log4j2; import javax.imageio.ImageIO; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -15,21 +16,47 @@ import java.io.ByteArrayOutputStream; @AllArgsConstructor @Getter @Log4j2 public abstract class SkinRenderer { + /** + * Gets the skin image. + * + * @param skin the skin + * @return the skin image + */ + public BufferedImage getSkinImage(Skin skin) { + try { + return ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); + } catch (Exception ex) { + return null; + } + } + /** * Gets the skin part image. * * @param skin the skin - * @param x the x position - * @param y the y position - * @param width the width - * @param height the height + * @param position the part position information * @param scale the scale * @return the skin part image */ - public BufferedImage getSkinPart(Skin skin, int x, int y, int width, int height, double scale) { + public BufferedImage getSkinPart(Skin skin, Skin.PartPosition position, double scale) { try { - BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); - BufferedImage part = skinImage.getSubimage(x, y, width, height); + BufferedImage skinImage = this.getSkinImage(skin); + if (skinImage == null) { + return null; + } + BufferedImage part; + Skin.LegacyPartPositionData legacyData = position.getLegacyData(); + if (skin.isLegacy() && legacyData != null) { + part = skinImage.getSubimage(legacyData.getX(), legacyData.getY(), position.getWidth(), position.getHeight()); + if (legacyData.isFlipped()) { + part = ImageUtils.flip(part); + } + } else { + part = skinImage.getSubimage(position.getX(), position.getY(), position.getWidth(), position.getHeight()); + } + if (part == null) { + return null; + } return ImageUtils.resize(part, scale); } catch (Exception ex) { return null; @@ -56,6 +83,20 @@ public abstract class SkinRenderer { } } + /** + * Applies an overlay (skin layer) to the head part. + * + * @param graphics the graphics + * @param part the part + */ + public void applyOverlay(Graphics2D graphics, BufferedImage part) { + if (part == null) { + return; + } + graphics.drawImage(part, 0, 0, null); + graphics.dispose(); + } + /** * Renders a skin part. * diff --git a/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java new file mode 100644 index 0000000..217b13d --- /dev/null +++ b/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java @@ -0,0 +1,55 @@ +package cc.fascinated.service.skin.impl; + +import cc.fascinated.common.ImageUtils; +import cc.fascinated.model.player.Skin; +import cc.fascinated.service.skin.SkinRenderer; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +@Getter @Log4j2 +public class BodyRenderer extends SkinRenderer { + + private static final int WIDTH = 16; + private static final int HEIGHT = 32; + + @Override + public byte[] renderPart(Skin skin, String partName, boolean renderOverlay, int size) { + log.info("Getting {} part bytes for {} with size {}", partName, skin.getUrl(), size); + try { + BufferedImage outputImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = outputImage.createGraphics(); + + // Get all the required body parts + BufferedImage head = this.getSkinPart(skin, Skin.PartPosition.HEAD, 1); + BufferedImage body = this.getSkinPart(skin, Skin.PartPosition.BODY, 1); + BufferedImage rightArm = this.getSkinPart(skin, Skin.PartPosition.RIGHT_ARM, 1); + BufferedImage leftArm = this.getSkinPart(skin, Skin.PartPosition.LEFT_ARM, 1); + BufferedImage rightLeg = this.getSkinPart(skin, Skin.PartPosition.RIGHT_LEG, 1); + BufferedImage leftLeg = this.getSkinPart(skin, Skin.PartPosition.LEFT_LEG, 1); + + if (renderOverlay) { // Render the skin layers + Graphics2D overlayGraphics = head.createGraphics(); + + applyOverlay(overlayGraphics, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY, 1)); + } + + // Draw the body + graphics.drawImage(head, 4, 0, null); + graphics.drawImage(body, 4, 8, null); + graphics.drawImage(rightArm, 0, 8, null); + graphics.drawImage(leftArm, 12, 8, null); + graphics.drawImage(rightLeg, 4, 20, null); + graphics.drawImage(leftLeg, 8, 20, null); + + graphics.dispose(); // Clean up + return super.getBytes(ImageUtils.resize(outputImage, (double) size / HEIGHT), skin, partName); + } catch (Exception ex) { + log.error("Failed to get {} part bytes for {}", partName, skin.getUrl(), ex); + throw new RuntimeException("Failed to get " + partName + " part for " + skin.getUrl()); + } + } +} \ No newline at end of file diff --git a/src/main/java/cc.fascinated/service/skin/impl/SquareRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java similarity index 57% rename from src/main/java/cc.fascinated/service/skin/impl/SquareRenderer.java rename to src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java index bdd3e92..d53920f 100644 --- a/src/main/java/cc.fascinated/service/skin/impl/SquareRenderer.java +++ b/src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java @@ -10,34 +10,11 @@ import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; @Getter @Log4j2 -public class SquareRenderer extends SkinRenderer { - - /** - * The x and y position of the part. - */ - private final int x, y; - - /** - * The width and height of the part. - */ - private final int widthAndHeight; - - /** - * Constructs a new {@link SquareRenderer}. - * - * @param x the x position of the part - * @param y the y position of the part - * @param widthAndHeight the width and height of the part - */ - public SquareRenderer(int x, int y, int widthAndHeight) { - this.x = x; - this.y = y; - this.widthAndHeight = widthAndHeight; - } +public class HeadRenderer extends SkinRenderer { @Override public byte[] renderPart(Skin skin, String partName, boolean renderOverlay, int size) { - double scale = (double) size / this.widthAndHeight; + double scale = (double) size / 8d; log.info("Getting {} part bytes for {} with size {} and scale {}", partName, skin.getUrl(), size, scale); try { @@ -45,7 +22,11 @@ public class SquareRenderer extends SkinRenderer { Graphics2D graphics = outputImage.createGraphics(); graphics.setTransform(AffineTransform.getScaleInstance(scale, scale)); - graphics.drawImage(this.getSkinPart(skin, this.x, this.y, this.widthAndHeight, this.widthAndHeight, 1), 0, 0, null); + graphics.drawImage(this.getSkinPart(skin, Skin.PartPosition.HEAD, 1), 0, 0, null); + + if (renderOverlay) { // Render the skin layers + applyOverlay(outputImage.createGraphics(), this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY, 1)); + } return super.getBytes(outputImage, skin, partName); } catch (Exception ex) { diff --git a/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java index d1937aa..c70f5e6 100644 --- a/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java +++ b/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java @@ -13,7 +13,7 @@ import java.awt.image.BufferedImage; @Getter @Log4j2 public class IsometricHeadRenderer extends SkinRenderer { - private static final double SKEW_A = 26d / 45d; // 0.57777777 + private static final double SKEW_A = 26d / 45d; // 0.57777777 private static final double SKEW_B = SKEW_A * 2d; // 1.15555555 /** @@ -36,19 +36,19 @@ public class IsometricHeadRenderer extends SkinRenderer { Graphics2D graphics = outputImage.createGraphics(); // Get all the required head parts - BufferedImage headTop = ImageUtils.resize(this.getSkinPart(skin, 8, 0, 8, 8, 1), scale); - BufferedImage headFront = ImageUtils.resize(this.getSkinPart(skin, 8, 8, 8, 8, 1), scale); - BufferedImage headRight = ImageUtils.resize(this.getSkinPart(skin, 0, 8, 8, 8, 1), scale); + BufferedImage headTop = ImageUtils.resize(this.getSkinPart(skin, Skin.PartPosition.HEAD_TOP, 1), scale); + BufferedImage headFront = ImageUtils.resize(this.getSkinPart(skin, Skin.PartPosition.HEAD_FRONT, 1), scale); + BufferedImage headRight = ImageUtils.resize(this.getSkinPart(skin, Skin.PartPosition.HEAD_RIGHT, 1), scale); - if (renderOverlay) { + if (renderOverlay) { // Render the skin layers Graphics2D headGraphics = headTop.createGraphics(); - applyOverlay(headGraphics,this.getSkinPart(skin, 40, 0, 8, 8, 1)); + applyOverlay(headGraphics, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY, 1)); headGraphics = headFront.createGraphics(); - applyOverlay(headGraphics, this.getSkinPart(skin, 16, 8, 8, 8, 1)); + applyOverlay(headGraphics, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_FRONT, 1)); headGraphics = headRight.createGraphics(); - applyOverlay(headGraphics, this.getSkinPart(skin, 32, 8, 8, 8, 1)); + applyOverlay(headGraphics, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_RIGHT, 1)); } // Draw the head @@ -77,17 +77,6 @@ public class IsometricHeadRenderer extends SkinRenderer { } } - /** - * Applies an overlay (skin layer) to the head part. - * - * @param graphics the graphics - * @param part the part - */ - private void applyOverlay(Graphics2D graphics, BufferedImage part) { - graphics.drawImage(part, 0, 0, null); - graphics.dispose(); - } - /** * Draws a part of the head. *