diff --git a/src/main/java/cc.fascinated/common/ImageUtils.java b/src/main/java/cc.fascinated/common/ImageUtils.java deleted file mode 100644 index 4345be0..0000000 --- a/src/main/java/cc.fascinated/common/ImageUtils.java +++ /dev/null @@ -1,41 +0,0 @@ -package cc.fascinated.common; - -import jakarta.validation.constraints.NotNull; -import lombok.extern.log4j.Log4j2; - -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.image.BufferedImage; - -@Log4j2 -public class ImageUtils { - - /** - * Resize an image. - * - * @param src the source image - * @param scale the scale factor - * @return the scaled image - */ - public static BufferedImage resize(@NotNull final BufferedImage src, final double scale) { - BufferedImage scaled = new BufferedImage((int) (src.getWidth() * scale), (int) (src.getHeight() * scale), BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = scaled.createGraphics(); - graphics.drawImage(src, AffineTransform.getScaleInstance(scale, scale), null); - 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/player/Skin.java b/src/main/java/cc.fascinated/model/player/Skin.java deleted file mode 100644 index 5f3921d..0000000 --- a/src/main/java/cc.fascinated/model/player/Skin.java +++ /dev/null @@ -1,233 +0,0 @@ -package cc.fascinated.model.player; - -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.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; -import com.google.gson.JsonObject; -import lombok.AllArgsConstructor; -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; - -@AllArgsConstructor @NoArgsConstructor -@Getter @Log4j2 -public class Skin { - /** - * The default skin, usually used when the skin is not found. - */ - public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", - Model.DEFAULT); - - /** - * The URL for the skin - */ - private String url; - - /** - * The model for the skin - */ - private Model model; - - /** - * The legacy status of the skin - */ - private boolean isLegacy = false; - - /** - * The skin image for the skin - */ - @JsonIgnore - private byte[] skinImage; - - /** - * The part URLs of the skin - */ - @JsonProperty("parts") - private Map partUrls = new HashMap<>(); - - public Skin(String url, Model model) { - this.url = url; - 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) {} - } - } - - /** - * Gets the skin from a {@link JsonObject}. - * - * @param json the JSON object - * @return the skin - */ - public static Skin fromJson(JsonObject json) { - if (json == null) { - return null; - } - String url = json.get("url").getAsString(); - JsonObject metadata = json.getAsJsonObject("metadata"); - Model model = Model.fromName(metadata == null ? "default" : // Fall back to slim if the model is not found - metadata.get("model").getAsString()); - return new Skin(url, model); - } - - /** - * Populates the part URLs for the skin. - * - * @param playerUuid the player's UUID - */ - public Skin populatePartUrls(String playerUuid) { - for (Parts part : Parts.values()) { - String partName = part.name().toLowerCase(); - this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid); - } - return this; - } - - /** - * The skin part enum that contains the - * information about the part. - */ - @Getter @AllArgsConstructor - public enum Parts { - FACE(new HeadRenderer()), - HEAD(new IsometricHeadRenderer()), - BODY(new BodyRenderer()); - - /** - * The skin part renderer for the part. - */ - private final SkinRenderer renderer; - - /** - * Gets the name of the part. - * - * @return the name of the part - */ - public String getName() { - return this.name().toLowerCase(); - } - - /** - * Gets the skin part from its name. - * - * @param name the name of the part - * @return the skin part - * @throws BadRequestException if the part is not found - */ - public static Parts fromName(String name) throws BadRequestException { - for (Parts part : values()) { - if (part.name().equalsIgnoreCase(name)) { - return part; - } - } - throw new BadRequestException("Invalid part name: " + name); - } - } - - @AllArgsConstructor @Getter - public enum PartPosition { - /** - * Skin positions - */ - HEAD_FACE(8, 8, 8, 8, null), - HEAD_TOP(8, 0, 8, 8, null), - HEAD_LEFT(0, 8, 8, 8, null), - - BODY_FRONT(20, 20, 8, 12, null), - 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, null), - LEFT_ARM(36, 52, 4, 12, new LegacySkinPosition(44, 20, true)), - - RIGHT_LEG(4, 20, 4, 12, null), - LEFT_LEG(20, 52, 4, 12, new LegacySkinPosition(4, 20, true)), - - /** - * Skin overlay (layer) positions - */ - HEAD_OVERLAY_FACE(40, 8, 8, 8, null), - HEAD_OVERLAY_TOP(40, 0, 8, 8, null), - HEAD_OVERLAY_LEFT(48, 8, 8, 8, null), - - BODY_OVERLAY_FRONT(20, 36, 8, 12, null), - - RIGHT_ARM_OVERLAY(44, 36, 8, 12, null), - LEFT_ARM_OVERLAY(52, 52, 8, 12, null), - - RIGHT_LEG_OVERLAY(4, 36, 4, 12, null), - LEFT_LEG_OVERLAY(4, 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 legacy skin position of the part. - */ - private final LegacySkinPosition legacySkinPosition; - } - - @AllArgsConstructor @Getter - public static class LegacySkinPosition { - - /** - * The x, and y position of the part. - */ - private final int x, y; - - /* - * Should the part be flipped horizontally? - */ - private final boolean flipped; - } - - /** - * The model of the skin. - */ - public enum Model { - DEFAULT, - SLIM; - - /** - * Gets the model from its name. - * - * @param name the name of the model - * @return the model - */ - public static Model fromName(String name) { - for (Model model : values()) { - if (model.name().equalsIgnoreCase(name)) { - return model; - } - } - return null; - } - } -} diff --git a/src/main/java/cc.fascinated/service/skin/SkinRenderer.java b/src/main/java/cc.fascinated/service/skin/SkinRenderer.java deleted file mode 100644 index 160a416..0000000 --- a/src/main/java/cc.fascinated/service/skin/SkinRenderer.java +++ /dev/null @@ -1,111 +0,0 @@ -package cc.fascinated.service.skin; - -import cc.fascinated.common.ImageUtils; -import cc.fascinated.exception.impl.BadRequestException; -import cc.fascinated.model.player.Skin; -import lombok.AllArgsConstructor; -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; - -@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 position the part position information - * @param scale the scale - * @return the skin part image - */ - public BufferedImage getSkinPart(Skin skin, Skin.PartPosition position, double scale) { - try { - BufferedImage skinImage = this.getSkinImage(skin); - if (skinImage == null) { - return null; - } - int width = skin.getModel() == Skin.Model.SLIM && position.name().contains("ARM") ? position.getWidth() - 1 : position.getWidth(); - BufferedImage part; - Skin.LegacySkinPosition legacySkinPosition = position.getLegacySkinPosition(); - if (legacySkinPosition != null) { - part = skinImage.getSubimage(legacySkinPosition.getX(), legacySkinPosition.getY(), width, position.getHeight()); - if (legacySkinPosition.isFlipped()) { - part = ImageUtils.flip(part); - } - } else { - part = skinImage.getSubimage(position.getX(), position.getY(), width, position.getHeight()); - } - return ImageUtils.resize(part, scale); - } catch (Exception ex) { - return null; - } - } - - /** - * Gets the bytes of an image. - * - * @param image the image - * @param skin the skin - * @param partName the part name - * @return the bytes - */ - public byte[] getBytes(BufferedImage image, Skin skin, String partName) throws BadRequestException { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - ImageIO.write(image, "png", outputStream); - // Cleanup - outputStream.flush(); - log.info("Successfully got {} part bytes for {}", partName, skin.getUrl()); - return outputStream.toByteArray(); - } catch (Exception ex) { - throw new BadRequestException("Failed to get " + partName + " part bytes for " + skin.getUrl()); - } - } - - /** - * Applies an overlay (skin layer) to the head part. - * - * @param originalImage the original image - * @param part the part - */ - public void applyOverlay(BufferedImage originalImage, BufferedImage part) { - if (part == null) { - return; - } - try { - Graphics2D graphics = originalImage.createGraphics(); - graphics.drawImage(part, 0, 0, null); - graphics.dispose(); - } catch (Exception ignored) {} // We can safely ignore this - } - - /** - * Renders a skin part. - * - * @param skin the skin - * @param partName the skin part name - * @param renderOverlay whether to render the overlay - * @param size the output size - * @return the skin part image - */ - public abstract byte[] renderPart(Skin skin, String partName, boolean renderOverlay, int size) throws BadRequestException; -} diff --git a/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java deleted file mode 100644 index 12d4d07..0000000 --- a/src/main/java/cc.fascinated/service/skin/impl/BodyRenderer.java +++ /dev/null @@ -1,53 +0,0 @@ -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.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); - - BufferedImage outputImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = outputImage.createGraphics(); - - // Get all the required body parts - BufferedImage face = this.getSkinPart(skin, Skin.PartPosition.HEAD_FACE, 1); - BufferedImage body = this.getSkinPart(skin, Skin.PartPosition.BODY_FRONT, 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 - applyOverlay(face, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_FACE, 1)); - applyOverlay(body, this.getSkinPart(skin, Skin.PartPosition.BODY_OVERLAY_FRONT, 1)); - applyOverlay(rightArm, this.getSkinPart(skin, Skin.PartPosition.RIGHT_ARM_OVERLAY, 1)); - applyOverlay(leftArm, this.getSkinPart(skin, Skin.PartPosition.LEFT_ARM_OVERLAY, 1)); - applyOverlay(rightLeg, this.getSkinPart(skin, Skin.PartPosition.RIGHT_LEG_OVERLAY, 1)); - applyOverlay(leftLeg, this.getSkinPart(skin, Skin.PartPosition.LEFT_LEG_OVERLAY, 1)); - } - - // Draw the body - graphics.drawImage(face, 4, 0, null); - graphics.drawImage(body, 4, 8, null); - graphics.drawImage(rightArm, skin.getModel() == Skin.Model.SLIM ? 1 : 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); - } -} \ No newline at end of file diff --git a/src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java deleted file mode 100644 index 614df3f..0000000 --- a/src/main/java/cc.fascinated/service/skin/impl/HeadRenderer.java +++ /dev/null @@ -1,34 +0,0 @@ -package cc.fascinated.service.skin.impl; - -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 HeadRenderer extends SkinRenderer { - - @Override - public byte[] renderPart(Skin skin, String partName, boolean renderOverlay, int size) { - double scale = (double) size / 8d; - log.info("Getting {} part bytes for {} with size {} and scale {}", partName, skin.getUrl(), size, scale); - - BufferedImage skinPart = this.getSkinPart(skin, Skin.PartPosition.HEAD_FACE, scale); - if (!renderOverlay) { - return super.getBytes(skinPart, skin, partName); - } - - BufferedImage outputImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = outputImage.createGraphics(); - graphics.drawImage(skinPart, 0, 0, null); - - // Apply the skin overlays - applyOverlay(outputImage, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_FACE, scale)); - - return super.getBytes(outputImage, skin, partName); - } -} diff --git a/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java b/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java deleted file mode 100644 index 1ff62c6..0000000 --- a/src/main/java/cc.fascinated/service/skin/impl/IsometricHeadRenderer.java +++ /dev/null @@ -1,74 +0,0 @@ -package cc.fascinated.service.skin.impl; - -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 IsometricHeadRenderer extends SkinRenderer { - - private static final double SKEW_A = 26d / 45d; // 0.57777777 - private static final double SKEW_B = SKEW_A * 2d; // 1.15555555 - - /** - * The head transforms - */ - private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0); - private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A); - private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D); - - @Override - public byte[] renderPart(Skin skin, String partName, boolean renderOverlay, int size) { - double scale = (size / 8d) / 2.5; - log.info("Getting {} part bytes for {} with size {} and scale {}", partName, skin.getUrl(), size, scale); - - double zOffset = scale * 3.5d; - double xOffset = scale * 2d; - BufferedImage outputImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = outputImage.createGraphics(); - - // Get all the required head parts - BufferedImage headTop = this.getSkinPart(skin, Skin.PartPosition.HEAD_TOP, scale); - BufferedImage headFront = this.getSkinPart(skin, Skin.PartPosition.HEAD_FACE, scale); - BufferedImage headLeft = this.getSkinPart(skin, Skin.PartPosition.HEAD_LEFT, scale); - - if (renderOverlay) { // Render the skin layers - applyOverlay(headTop, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_TOP, scale)); - applyOverlay(headFront, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_FACE, scale)); - applyOverlay(headLeft, this.getSkinPart(skin, Skin.PartPosition.HEAD_OVERLAY_LEFT, scale)); - } - - // Draw the top of the left - drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2); - - // Draw the face of the head - double x = xOffset + 8 * scale; - drawPart(graphics, headFront, FACE_TRANSFORM, x, x + zOffset - 0.5, headFront.getWidth(), headFront.getHeight()); - - // Draw the left side of the head - drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight()); - - return super.getBytes(outputImage, skin, partName); - } - - /** - * Draws a part of the head. - * - * @param graphics the graphics - * @param part the part - * @param transform the transform - * @param x the x position - * @param y the y position - * @param width the width - * @param height the height - */ - private void drawPart(Graphics2D graphics, BufferedImage part, AffineTransform transform, double x, double y, int width, int height) { - graphics.setTransform(transform); - graphics.drawImage(part, (int) x, (int) y, width, height, null); - } -} \ No newline at end of file diff --git a/src/main/java/cc.fascinated/Main.java b/src/main/java/cc/fascinated/Main.java similarity index 100% rename from src/main/java/cc.fascinated/Main.java rename to src/main/java/cc/fascinated/Main.java diff --git a/src/main/java/cc.fascinated/common/ColorUtils.java b/src/main/java/cc/fascinated/common/ColorUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/ColorUtils.java rename to src/main/java/cc/fascinated/common/ColorUtils.java diff --git a/src/main/java/cc.fascinated/common/DNSUtils.java b/src/main/java/cc/fascinated/common/DNSUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/DNSUtils.java rename to src/main/java/cc/fascinated/common/DNSUtils.java diff --git a/src/main/java/cc.fascinated/common/EnumUtils.java b/src/main/java/cc/fascinated/common/EnumUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/EnumUtils.java rename to src/main/java/cc/fascinated/common/EnumUtils.java diff --git a/src/main/java/cc.fascinated/common/ExpiringSet.java b/src/main/java/cc/fascinated/common/ExpiringSet.java similarity index 100% rename from src/main/java/cc.fascinated/common/ExpiringSet.java rename to src/main/java/cc/fascinated/common/ExpiringSet.java diff --git a/src/main/java/cc.fascinated/common/IPUtils.java b/src/main/java/cc/fascinated/common/IPUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/IPUtils.java rename to src/main/java/cc/fascinated/common/IPUtils.java diff --git a/src/main/java/cc/fascinated/common/ImageUtils.java b/src/main/java/cc/fascinated/common/ImageUtils.java new file mode 100644 index 0000000..c3db34f --- /dev/null +++ b/src/main/java/cc/fascinated/common/ImageUtils.java @@ -0,0 +1,58 @@ +package cc.fascinated.common; + +import jakarta.validation.constraints.NotNull; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@Log4j2 +public class ImageUtils { + /** + * Scale the given image to the provided size. + * + * @param image the image to scale + * @param size the size to scale the image to + * @return the scaled image + */ + public static BufferedImage resize(BufferedImage image, double size) { + BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = scaled.createGraphics(); + graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null); + graphics.dispose(); + return scaled; + } + + /** + * Flip the given image. + * + * @param image the image to flip + * @return the flipped image + */ + public static BufferedImage flip(@NotNull final BufferedImage image) { + BufferedImage flipped = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = flipped.createGraphics(); + graphics.drawImage(image, image.getWidth(), 0, 0, image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), null); + graphics.dispose(); + return flipped; + } + + /** + * Convert an image to bytes. + * + * @param image the image to convert + * @return the image as bytes + */ + @SneakyThrows + public static byte[] imageToBytes(BufferedImage image) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", outputStream); + return outputStream.toByteArray(); + } + } +} diff --git a/src/main/java/cc.fascinated/common/JavaMinecraftVersion.java b/src/main/java/cc/fascinated/common/JavaMinecraftVersion.java similarity index 100% rename from src/main/java/cc.fascinated/common/JavaMinecraftVersion.java rename to src/main/java/cc/fascinated/common/JavaMinecraftVersion.java diff --git a/src/main/java/cc.fascinated/common/PlayerUtils.java b/src/main/java/cc/fascinated/common/PlayerUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/PlayerUtils.java rename to src/main/java/cc/fascinated/common/PlayerUtils.java diff --git a/src/main/java/cc.fascinated/common/ServerUtils.java b/src/main/java/cc/fascinated/common/ServerUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/ServerUtils.java rename to src/main/java/cc/fascinated/common/ServerUtils.java diff --git a/src/main/java/cc.fascinated/common/Tuple.java b/src/main/java/cc/fascinated/common/Tuple.java similarity index 100% rename from src/main/java/cc.fascinated/common/Tuple.java rename to src/main/java/cc/fascinated/common/Tuple.java diff --git a/src/main/java/cc.fascinated/common/UUIDUtils.java b/src/main/java/cc/fascinated/common/UUIDUtils.java similarity index 100% rename from src/main/java/cc.fascinated/common/UUIDUtils.java rename to src/main/java/cc/fascinated/common/UUIDUtils.java diff --git a/src/main/java/cc.fascinated/common/WebRequest.java b/src/main/java/cc/fascinated/common/WebRequest.java similarity index 100% rename from src/main/java/cc.fascinated/common/WebRequest.java rename to src/main/java/cc/fascinated/common/WebRequest.java diff --git a/src/main/java/cc.fascinated/common/packet/MinecraftBedrockPacket.java b/src/main/java/cc/fascinated/common/packet/MinecraftBedrockPacket.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/MinecraftBedrockPacket.java rename to src/main/java/cc/fascinated/common/packet/MinecraftBedrockPacket.java diff --git a/src/main/java/cc.fascinated/common/packet/MinecraftJavaPacket.java b/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/MinecraftJavaPacket.java rename to src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java diff --git a/src/main/java/cc.fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java b/src/main/java/cc/fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java rename to src/main/java/cc/fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java diff --git a/src/main/java/cc.fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java b/src/main/java/cc/fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java rename to src/main/java/cc/fascinated/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java diff --git a/src/main/java/cc.fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java rename to src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java diff --git a/src/main/java/cc.fascinated/common/packet/impl/java/JavaPacketStatusInStart.java b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java similarity index 100% rename from src/main/java/cc.fascinated/common/packet/impl/java/JavaPacketStatusInStart.java rename to src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java diff --git a/src/main/java/cc/fascinated/common/renderer/IsometricSkinRenderer.java b/src/main/java/cc/fascinated/common/renderer/IsometricSkinRenderer.java new file mode 100644 index 0000000..a93b8ad --- /dev/null +++ b/src/main/java/cc/fascinated/common/renderer/IsometricSkinRenderer.java @@ -0,0 +1,27 @@ +package cc.fascinated.common.renderer; + +import cc.fascinated.model.skin.ISkinPart; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +public abstract class IsometricSkinRenderer extends SkinRenderer { + + /** + * Draw a part onto the texture. + * + * @param graphics the graphics to draw to + * @param partImage the part image to draw + * @param transform the transform to apply + * @param x the x position to draw at + * @param y the y position to draw at + * @param width the part image width + * @param height the part image height + */ + protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform, + double x, double y, int width, int height) { + graphics.setTransform(transform); + graphics.drawImage(partImage, (int) x, (int) y, width, height, null); + } +} diff --git a/src/main/java/cc/fascinated/common/renderer/SkinRenderer.java b/src/main/java/cc/fascinated/common/renderer/SkinRenderer.java new file mode 100644 index 0000000..f93de80 --- /dev/null +++ b/src/main/java/cc/fascinated/common/renderer/SkinRenderer.java @@ -0,0 +1,95 @@ +package cc.fascinated.common.renderer; + +import cc.fascinated.common.ImageUtils; +import cc.fascinated.model.skin.ISkinPart; +import cc.fascinated.model.skin.Skin; +import lombok.SneakyThrows; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; + +public abstract class SkinRenderer { + + /** + * Get the texture of a part of a skin. + * + * @param skin the skin to get the part texture from + * @param part the part of the skin to get + * @param size the size to scale the texture to + * @return the texture of the skin part + */ + @SneakyThrows + public BufferedImage getVanillaSkinPart(Skin skin, ISkinPart.Vanilla part, double size) { + ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part + + // The skin texture is legacy, use legacy coordinates + if (skin.isLegacy() && part.hasLegacyCoordinates()) { + coordinates = part.getLegacyCoordinates(); + } + int width = part.getWidth(); // The width of the part + if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) { + width--; + } + BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture + BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size); + if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) { + partTexture = ImageUtils.flip(partTexture); + } + return partTexture; + } + + /** + * Get the texture of a specific part of the skin. + * + * @param skinImage the skin image to get the part from + * @param x the x position of the part + * @param y the y position of the part + * @param width the width of the part + * @param height the height of the part + * @param size the size to scale the part to + * @return the texture of the skin part + */ + @SneakyThrows + private BufferedImage getSkinPartTexture(BufferedImage skinImage, int x, int y, int width, int height, double size) { + // Create a new BufferedImage for the part of the skin texture + BufferedImage headTexture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + // Crop just the part we want based on our x, y, width, and height + headTexture.getGraphics().drawImage(skinImage, 0, 0, width, height, x, y, x + width, y + height, null); + + // Scale the skin part texture + if (size > 0D) { + headTexture = ImageUtils.resize(headTexture, size); + } + return headTexture; + } + + /** + * Apply an overlay to a texture. + * + * @param graphics the graphics to overlay on + * @param overlayImage the part to overlay + */ + protected void applyOverlay(Graphics2D graphics, BufferedImage overlayImage) { + try { + graphics.drawImage(overlayImage, 0, 0, null); + graphics.dispose(); + } catch (Exception ignored) { + // We can safely ignore this, legacy + // skins don't have overlays + } + } + + /** + * Renders the skin part for the player's skin. + * + * @param skin the player's skin + * @param part the skin part to render + * @param renderOverlays should the overlays be rendered + * @param size the size of the part + * @return the rendered skin part + */ + public abstract BufferedImage render(Skin skin, T part, boolean renderOverlays, int size); +} diff --git a/src/main/java/cc/fascinated/common/renderer/impl/BodyRenderer.java b/src/main/java/cc/fascinated/common/renderer/impl/BodyRenderer.java new file mode 100644 index 0000000..513a89b --- /dev/null +++ b/src/main/java/cc/fascinated/common/renderer/impl/BodyRenderer.java @@ -0,0 +1,42 @@ +package cc.fascinated.common.renderer.impl; + +import cc.fascinated.common.ImageUtils; +import cc.fascinated.common.renderer.SkinRenderer; +import cc.fascinated.model.skin.ISkinPart; +import cc.fascinated.model.skin.Skin; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +import java.awt.*; +import java.awt.image.BufferedImage; + +@AllArgsConstructor @Getter @Log4j2 +public class BodyRenderer extends SkinRenderer { + public static final BodyRenderer INSTANCE = new BodyRenderer(); + + @Override + public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) { + BufferedImage texture = new BufferedImage(16, 32, BufferedImage.TYPE_INT_ARGB); // The texture to return + Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing + + // Get the Vanilla skin parts to draw + BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, -1); + BufferedImage body = getVanillaSkinPart(skin, ISkinPart.Vanilla.BODY_FRONT, -1); + BufferedImage leftArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_ARM_FRONT, -1); + BufferedImage rightArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_ARM_FRONT, -1); + BufferedImage leftLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_LEG_FRONT, -1); + BufferedImage rightLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_LEG_FRONT, -1); + + // Draw the body parts + graphics.drawImage(face, 4, 0, null); + graphics.drawImage(body, 4, 8, null); + graphics.drawImage(leftArm, skin.getModel() == Skin.Model.SLIM ? 1 : 0, 8, null); + graphics.drawImage(rightArm, 12, 8, null); + graphics.drawImage(leftLeg, 8, 20, null); + graphics.drawImage(rightLeg, 4, 20, null); + + graphics.dispose(); + return ImageUtils.resize(texture, (double) size / 32); + } +} diff --git a/src/main/java/cc/fascinated/common/renderer/impl/IsometricHeadRenderer.java b/src/main/java/cc/fascinated/common/renderer/impl/IsometricHeadRenderer.java new file mode 100644 index 0000000..ab23179 --- /dev/null +++ b/src/main/java/cc/fascinated/common/renderer/impl/IsometricHeadRenderer.java @@ -0,0 +1,48 @@ +package cc.fascinated.common.renderer.impl; + +import cc.fascinated.common.renderer.IsometricSkinRenderer; +import cc.fascinated.model.skin.ISkinPart; +import cc.fascinated.model.skin.Skin; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +public class IsometricHeadRenderer extends IsometricSkinRenderer { + public static final IsometricHeadRenderer INSTANCE = new IsometricHeadRenderer(); + + private static final double SKEW_A = 26D / 45D; // 0.57777777 + private static final double SKEW_B = SKEW_A * 2D; // 1.15555555 + + private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0); + private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A); + private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D); + + @Override + public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) { + double scale = (size / 8D) / 2.5; + double zOffset = scale * 3.5D; + double xOffset = scale * 2D; + + BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return + Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing + + // Get the Vanilla skin parts to draw + BufferedImage headTop = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_TOP, scale); + BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, scale); + BufferedImage headLeft = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_LEFT, scale); + + // Draw the top head part + drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2); + + // Draw the face part + double x = xOffset + 8 * scale; + drawPart(graphics, face, FACE_TRANSFORM, x, x + zOffset - 0.5, face.getWidth(), face.getHeight()); + + // Draw the left head part + drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight()); + + graphics.dispose(); + return texture; + } +} diff --git a/src/main/java/cc/fascinated/common/renderer/impl/SquareRenderer.java b/src/main/java/cc/fascinated/common/renderer/impl/SquareRenderer.java new file mode 100644 index 0000000..33d49be --- /dev/null +++ b/src/main/java/cc/fascinated/common/renderer/impl/SquareRenderer.java @@ -0,0 +1,39 @@ +package cc.fascinated.common.renderer.impl; + +import cc.fascinated.common.renderer.SkinRenderer; +import cc.fascinated.model.skin.ISkinPart; +import cc.fascinated.model.skin.Skin; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +import java.awt.*; +import java.awt.image.BufferedImage; + +@AllArgsConstructor @Getter @Log4j2 +public class SquareRenderer extends SkinRenderer { + public static final SquareRenderer INSTANCE = new SquareRenderer(); + + @Override + public BufferedImage render(Skin skin, ISkinPart.Vanilla part, boolean renderOverlays, int size) { + double scale = size / 8D; + BufferedImage partImage = getVanillaSkinPart(skin, part, scale); // Get the part image + if (!renderOverlays) { // Not rendering overlays + return partImage; + } + // Create a new image, draw our skin part texture, and then apply overlays + BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return + Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing + graphics.drawImage(partImage, 0, 0, null); + + // Draw part overlays + ISkinPart.Vanilla[] overlayParts = part.getOverlays(); + if (overlayParts != null) { + for (ISkinPart.Vanilla overlay : overlayParts) { + applyOverlay(graphics, getVanillaSkinPart(skin, overlay, scale)); + } + } + graphics.dispose(); + return texture; + } +} diff --git a/src/main/java/cc.fascinated/config/Config.java b/src/main/java/cc/fascinated/config/Config.java similarity index 100% rename from src/main/java/cc.fascinated/config/Config.java rename to src/main/java/cc/fascinated/config/Config.java diff --git a/src/main/java/cc.fascinated/config/OpenAPIConfiguration.java b/src/main/java/cc/fascinated/config/OpenAPIConfiguration.java similarity index 100% rename from src/main/java/cc.fascinated/config/OpenAPIConfiguration.java rename to src/main/java/cc/fascinated/config/OpenAPIConfiguration.java diff --git a/src/main/java/cc.fascinated/config/RedisConfig.java b/src/main/java/cc/fascinated/config/RedisConfig.java similarity index 100% rename from src/main/java/cc.fascinated/config/RedisConfig.java rename to src/main/java/cc/fascinated/config/RedisConfig.java diff --git a/src/main/java/cc.fascinated/controller/HomeController.java b/src/main/java/cc/fascinated/controller/HomeController.java similarity index 100% rename from src/main/java/cc.fascinated/controller/HomeController.java rename to src/main/java/cc/fascinated/controller/HomeController.java diff --git a/src/main/java/cc.fascinated/controller/PlayerController.java b/src/main/java/cc/fascinated/controller/PlayerController.java similarity index 94% rename from src/main/java/cc.fascinated/controller/PlayerController.java rename to src/main/java/cc/fascinated/controller/PlayerController.java index 361fb7f..85ec0c7 100644 --- a/src/main/java/cc.fascinated/controller/PlayerController.java +++ b/src/main/java/cc/fascinated/controller/PlayerController.java @@ -2,7 +2,7 @@ package cc.fascinated.controller; import cc.fascinated.model.cache.CachedPlayer; import cc.fascinated.model.cache.CachedPlayerName; -import cc.fascinated.model.player.Skin; +import cc.fascinated.model.skin.Skin; import cc.fascinated.service.PlayerService; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -52,7 +52,6 @@ public class PlayerController { @Parameter(description = "Whether to render the skin overlay (skin layers)", example = "false") @RequestParam(required = false, defaultValue = "false") boolean overlay, @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) { CachedPlayer player = playerService.getPlayer(id); - Skin.Parts skinPart = Skin.Parts.fromName(part); String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png"; // Return the part image @@ -60,6 +59,6 @@ public class PlayerController { .cacheControl(cacheControl) .contentType(MediaType.IMAGE_PNG) .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername())) - .body(playerService.getSkinPart(player, skinPart, overlay, size).getBytes()); + .body(playerService.getSkinPart(player, part, overlay, size).getBytes()); } } diff --git a/src/main/java/cc.fascinated/controller/ServerController.java b/src/main/java/cc/fascinated/controller/ServerController.java similarity index 100% rename from src/main/java/cc.fascinated/controller/ServerController.java rename to src/main/java/cc/fascinated/controller/ServerController.java diff --git a/src/main/java/cc.fascinated/exception/ExceptionControllerAdvice.java b/src/main/java/cc/fascinated/exception/ExceptionControllerAdvice.java similarity index 100% rename from src/main/java/cc.fascinated/exception/ExceptionControllerAdvice.java rename to src/main/java/cc/fascinated/exception/ExceptionControllerAdvice.java diff --git a/src/main/java/cc.fascinated/exception/impl/BadRequestException.java b/src/main/java/cc/fascinated/exception/impl/BadRequestException.java similarity index 100% rename from src/main/java/cc.fascinated/exception/impl/BadRequestException.java rename to src/main/java/cc/fascinated/exception/impl/BadRequestException.java diff --git a/src/main/java/cc.fascinated/exception/impl/MojangAPIRateLimitException.java b/src/main/java/cc/fascinated/exception/impl/MojangAPIRateLimitException.java similarity index 100% rename from src/main/java/cc.fascinated/exception/impl/MojangAPIRateLimitException.java rename to src/main/java/cc/fascinated/exception/impl/MojangAPIRateLimitException.java diff --git a/src/main/java/cc.fascinated/exception/impl/RateLimitException.java b/src/main/java/cc/fascinated/exception/impl/RateLimitException.java similarity index 100% rename from src/main/java/cc.fascinated/exception/impl/RateLimitException.java rename to src/main/java/cc/fascinated/exception/impl/RateLimitException.java diff --git a/src/main/java/cc.fascinated/exception/impl/ResourceNotFoundException.java b/src/main/java/cc/fascinated/exception/impl/ResourceNotFoundException.java similarity index 100% rename from src/main/java/cc.fascinated/exception/impl/ResourceNotFoundException.java rename to src/main/java/cc/fascinated/exception/impl/ResourceNotFoundException.java diff --git a/src/main/java/cc.fascinated/log/TransactionLogger.java b/src/main/java/cc/fascinated/log/TransactionLogger.java similarity index 100% rename from src/main/java/cc.fascinated/log/TransactionLogger.java rename to src/main/java/cc/fascinated/log/TransactionLogger.java diff --git a/src/main/java/cc.fascinated/model/cache/CachedMinecraftServer.java b/src/main/java/cc/fascinated/model/cache/CachedMinecraftServer.java similarity index 100% rename from src/main/java/cc.fascinated/model/cache/CachedMinecraftServer.java rename to src/main/java/cc/fascinated/model/cache/CachedMinecraftServer.java diff --git a/src/main/java/cc.fascinated/model/cache/CachedPlayer.java b/src/main/java/cc/fascinated/model/cache/CachedPlayer.java similarity index 96% rename from src/main/java/cc.fascinated/model/cache/CachedPlayer.java rename to src/main/java/cc/fascinated/model/cache/CachedPlayer.java index 3d40a95..5803bb5 100644 --- a/src/main/java/cc.fascinated/model/cache/CachedPlayer.java +++ b/src/main/java/cc/fascinated/model/cache/CachedPlayer.java @@ -3,7 +3,7 @@ package cc.fascinated.model.cache; import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.player.Cape; import cc.fascinated.model.player.Player; -import cc.fascinated.model.player.Skin; +import cc.fascinated.model.skin.Skin; import lombok.Getter; import lombok.Setter; import lombok.ToString; diff --git a/src/main/java/cc.fascinated/model/cache/CachedPlayerName.java b/src/main/java/cc/fascinated/model/cache/CachedPlayerName.java similarity index 100% rename from src/main/java/cc.fascinated/model/cache/CachedPlayerName.java rename to src/main/java/cc/fascinated/model/cache/CachedPlayerName.java diff --git a/src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java b/src/main/java/cc/fascinated/model/cache/CachedPlayerSkinPart.java similarity index 100% rename from src/main/java/cc.fascinated/model/cache/CachedPlayerSkinPart.java rename to src/main/java/cc/fascinated/model/cache/CachedPlayerSkinPart.java diff --git a/src/main/java/cc.fascinated/model/dns/DNSRecord.java b/src/main/java/cc/fascinated/model/dns/DNSRecord.java similarity index 100% rename from src/main/java/cc.fascinated/model/dns/DNSRecord.java rename to src/main/java/cc/fascinated/model/dns/DNSRecord.java diff --git a/src/main/java/cc.fascinated/model/dns/impl/ARecord.java b/src/main/java/cc/fascinated/model/dns/impl/ARecord.java similarity index 100% rename from src/main/java/cc.fascinated/model/dns/impl/ARecord.java rename to src/main/java/cc/fascinated/model/dns/impl/ARecord.java diff --git a/src/main/java/cc.fascinated/model/dns/impl/SRVRecord.java b/src/main/java/cc/fascinated/model/dns/impl/SRVRecord.java similarity index 100% rename from src/main/java/cc.fascinated/model/dns/impl/SRVRecord.java rename to src/main/java/cc/fascinated/model/dns/impl/SRVRecord.java diff --git a/src/main/java/cc.fascinated/model/mojang/MojangProfile.java b/src/main/java/cc/fascinated/model/mojang/MojangProfile.java similarity index 98% rename from src/main/java/cc.fascinated/model/mojang/MojangProfile.java rename to src/main/java/cc/fascinated/model/mojang/MojangProfile.java index 2a17c40..089178c 100644 --- a/src/main/java/cc.fascinated/model/mojang/MojangProfile.java +++ b/src/main/java/cc/fascinated/model/mojang/MojangProfile.java @@ -4,7 +4,7 @@ import cc.fascinated.Main; import cc.fascinated.common.Tuple; import cc.fascinated.common.UUIDUtils; import cc.fascinated.model.player.Cape; -import cc.fascinated.model.player.Skin; +import cc.fascinated.model.skin.Skin; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.gson.JsonObject; import lombok.AllArgsConstructor; diff --git a/src/main/java/cc.fascinated/model/mojang/MojangUsernameToUuid.java b/src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java similarity index 100% rename from src/main/java/cc.fascinated/model/mojang/MojangUsernameToUuid.java rename to src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java diff --git a/src/main/java/cc.fascinated/model/player/Cape.java b/src/main/java/cc/fascinated/model/player/Cape.java similarity index 100% rename from src/main/java/cc.fascinated/model/player/Cape.java rename to src/main/java/cc/fascinated/model/player/Cape.java diff --git a/src/main/java/cc.fascinated/model/player/Player.java b/src/main/java/cc/fascinated/model/player/Player.java similarity index 97% rename from src/main/java/cc.fascinated/model/player/Player.java rename to src/main/java/cc/fascinated/model/player/Player.java index 06e5906..5ac567e 100644 --- a/src/main/java/cc.fascinated/model/player/Player.java +++ b/src/main/java/cc/fascinated/model/player/Player.java @@ -3,6 +3,7 @@ package cc.fascinated.model.player; import cc.fascinated.common.Tuple; import cc.fascinated.common.UUIDUtils; import cc.fascinated.model.mojang.MojangProfile; +import cc.fascinated.model.skin.Skin; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.data.annotation.Id; diff --git a/src/main/java/cc.fascinated/model/response/ErrorResponse.java b/src/main/java/cc/fascinated/model/response/ErrorResponse.java similarity index 100% rename from src/main/java/cc.fascinated/model/response/ErrorResponse.java rename to src/main/java/cc/fascinated/model/response/ErrorResponse.java diff --git a/src/main/java/cc.fascinated/model/server/BedrockMinecraftServer.java b/src/main/java/cc/fascinated/model/server/BedrockMinecraftServer.java similarity index 100% rename from src/main/java/cc.fascinated/model/server/BedrockMinecraftServer.java rename to src/main/java/cc/fascinated/model/server/BedrockMinecraftServer.java diff --git a/src/main/java/cc.fascinated/model/server/JavaMinecraftServer.java b/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java similarity index 100% rename from src/main/java/cc.fascinated/model/server/JavaMinecraftServer.java rename to src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java diff --git a/src/main/java/cc.fascinated/model/server/MinecraftServer.java b/src/main/java/cc/fascinated/model/server/MinecraftServer.java similarity index 100% rename from src/main/java/cc.fascinated/model/server/MinecraftServer.java rename to src/main/java/cc/fascinated/model/server/MinecraftServer.java diff --git a/src/main/java/cc/fascinated/model/skin/ISkinPart.java b/src/main/java/cc/fascinated/model/skin/ISkinPart.java new file mode 100644 index 0000000..10b042d --- /dev/null +++ b/src/main/java/cc/fascinated/model/skin/ISkinPart.java @@ -0,0 +1,199 @@ +package cc.fascinated.model.skin; + +import cc.fascinated.common.renderer.SkinRenderer; +import cc.fascinated.common.renderer.impl.BodyRenderer; +import cc.fascinated.common.renderer.impl.IsometricHeadRenderer; +import cc.fascinated.common.renderer.impl.SquareRenderer; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.awt.image.BufferedImage; + +public interface ISkinPart { + Enum[][] TYPES = { Vanilla.values(), Custom.values() }; + + /** + * The name of the part. + * + * @return the part name + */ + String name(); + + /** + * Should this part be hidden from the + * player skin part urls list? + * + * @return whether this part should be hidden + */ + boolean hidden(); + + /** + * Renders the skin part for the skin. + * + * @param skin the skin + * @param renderOverlays should the overlays be rendered + * @param size the size of the part + * @return the rendered skin part + */ + BufferedImage render(Skin skin, boolean renderOverlays, int size); + + /** + * Get a skin part by the given name. + * + * @param name the name of the part + * @return the part, null if none + */ + static ISkinPart getByName(String name) { + name = name.toUpperCase(); + for (Enum[] type : TYPES) { + for (Enum part : type) { + if (!part.name().equals(name)) { + continue; + } + return (ISkinPart) part; + } + } + return null; + } + + @Getter + enum Vanilla implements ISkinPart { + // Overlays + HEAD_OVERLAY_FACE(true, new Coordinates(40, 8), 8, 8), + + // Head + HEAD_TOP(true, new Coordinates(8, 0), 8, 8), + FACE(false, new Coordinates(8, 8), 8, 8, HEAD_OVERLAY_FACE), + HEAD_LEFT(true, new Coordinates(0, 8), 8, 8), + HEAD_RIGHT(true, new Coordinates(16, 8), 8, 8), + HEAD_BOTTOM(true, new Coordinates(16, 0), 8, 8), + HEAD_BACK(true, new Coordinates(24, 8), 8, 8), + + // Body + BODY_FRONT(true, new Coordinates(20, 20), 8, 12), + + // Arms + LEFT_ARM_TOP(true, new Coordinates(36, 48), 4, 4), + RIGHT_ARM_TOP(true, new Coordinates(44, 16), 4, 4), + + LEFT_ARM_FRONT(true, new Coordinates(44, 20), 4, 12), + RIGHT_ARM_FRONT(true, new Coordinates(36, 52), new LegacyCoordinates(44, 20, true), 4, 12), + + // Legs + LEFT_LEG_FRONT(true, new Coordinates(4, 20), 4, 12), // Front + RIGHT_LEG_FRONT(true, new Coordinates(20, 52), new LegacyCoordinates(4, 20, true), 4, 12); // Front + + /** + * Should this part be hidden from the + * player skin part urls list? + */ + private final boolean hidden; + + /** + * The coordinates of the part. + */ + private final Coordinates coordinates; + + /** + * The legacy coordinates of the part. + */ + private final LegacyCoordinates legacyCoordinates; + + /** + * The width and height of the part. + */ + private final int width, height; + + /** + * The overlays of the part. + */ + private final Vanilla[] overlays; + + Vanilla(boolean hidden, Coordinates coordinates, int width, int height, Vanilla... overlays) { + this(hidden, coordinates, null, width, height, overlays); + } + + Vanilla(boolean hidden, Coordinates coordinates, LegacyCoordinates legacyCoordinates, int width, int height, Vanilla... overlays) { + this.hidden = hidden; + this.coordinates = coordinates; + this.legacyCoordinates = legacyCoordinates; + this.width = width; + this.height = height; + this.overlays = overlays; + } + + @Override + public boolean hidden() { + return this.isHidden(); + } + + @Override + public BufferedImage render(Skin skin, boolean renderOverlays, int size) { + return SquareRenderer.INSTANCE.render(skin, this, renderOverlays, size); + } + + /** + * Is this part a front arm? + * + * @return whether this part is a front arm + */ + public boolean isFrontArm() { + return this == LEFT_ARM_FRONT || this == RIGHT_ARM_FRONT; + } + + /** + * Does this part have legacy coordinates? + * + * @return whether this part has legacy coordinates + */ + public boolean hasLegacyCoordinates() { + return legacyCoordinates != null; + } + + @AllArgsConstructor @Getter + public static class Coordinates { + /** + * The X and Y position of the part. + */ + private final int x, y; + } + + @Getter + public static class LegacyCoordinates extends Coordinates { + /** + * Should the part be flipped horizontally? + */ + private final boolean flipped; + + public LegacyCoordinates(int x, int y) { + this(x, y, false); + } + + public LegacyCoordinates(int x, int y, boolean flipped) { + super(x, y); + this.flipped = flipped; + } + } + } + + @AllArgsConstructor @Getter + enum Custom implements ISkinPart { + HEAD(IsometricHeadRenderer.INSTANCE), + BODY(BodyRenderer.INSTANCE); + + /** + * The renderer to use for this part + */ + private final SkinRenderer renderer; + + @Override + public boolean hidden() { + return false; + } + + @Override + public BufferedImage render(Skin skin, boolean renderOverlays, int size) { + return renderer.render(skin, this, renderOverlays, size); + } + } +} diff --git a/src/main/java/cc/fascinated/model/skin/Skin.java b/src/main/java/cc/fascinated/model/skin/Skin.java new file mode 100644 index 0000000..b0f9064 --- /dev/null +++ b/src/main/java/cc/fascinated/model/skin/Skin.java @@ -0,0 +1,128 @@ +package cc.fascinated.model.skin; + +import cc.fascinated.common.PlayerUtils; +import cc.fascinated.config.Config; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +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; + +@AllArgsConstructor @NoArgsConstructor +@Getter @Log4j2 +public class Skin { + /** + * The default skin, usually used when the skin is not found. + */ + public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", + Model.DEFAULT); + + /** + * The URL for the skin + */ + private String url; + + /** + * The model for the skin + */ + private Model model; + + /** + * The legacy status of the skin + */ + private boolean isLegacy = false; + + /** + * The skin image for the skin + */ + @JsonIgnore + private byte[] skinImage; + + /** + * The part URLs of the skin + */ + @JsonProperty("parts") + private Map partUrls = new HashMap<>(); + + public Skin(String url, Model model) { + this.url = url; + 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) {} + } + } + + /** + * Gets the skin from a {@link JsonObject}. + * + * @param json the JSON object + * @return the skin + */ + public static Skin fromJson(JsonObject json) { + if (json == null) { + return null; + } + String url = json.get("url").getAsString(); + JsonObject metadata = json.getAsJsonObject("metadata"); + Model model = Model.fromName(metadata == null ? "default" : // Fall back to slim if the model is not found + metadata.get("model").getAsString()); + return new Skin(url, model); + } + + /** + * Populates the part URLs for the skin. + * + * @param playerUuid the player's UUID + */ + public Skin populatePartUrls(String playerUuid) { + for (Enum[] type : ISkinPart.TYPES) { + for (Enum part : type) { + ISkinPart skinPart = (ISkinPart) part; + if (skinPart.hidden()) { + continue; + } + String partName = part.name().toLowerCase(); + this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid); + } + } + return this; + } + + /** + * The model of the skin. + */ + public enum Model { + DEFAULT, + SLIM; + + /** + * Gets the model from its name. + * + * @param name the name of the model + * @return the model + */ + public static Model fromName(String name) { + for (Model model : values()) { + if (model.name().equalsIgnoreCase(name)) { + return model; + } + } + return null; + } + } +} diff --git a/src/main/java/cc.fascinated/model/token/JavaServerStatusToken.java b/src/main/java/cc/fascinated/model/token/JavaServerStatusToken.java similarity index 100% rename from src/main/java/cc.fascinated/model/token/JavaServerStatusToken.java rename to src/main/java/cc/fascinated/model/token/JavaServerStatusToken.java diff --git a/src/main/java/cc.fascinated/repository/MinecraftServerCacheRepository.java b/src/main/java/cc/fascinated/repository/MinecraftServerCacheRepository.java similarity index 100% rename from src/main/java/cc.fascinated/repository/MinecraftServerCacheRepository.java rename to src/main/java/cc/fascinated/repository/MinecraftServerCacheRepository.java diff --git a/src/main/java/cc.fascinated/repository/PlayerCacheRepository.java b/src/main/java/cc/fascinated/repository/PlayerCacheRepository.java similarity index 100% rename from src/main/java/cc.fascinated/repository/PlayerCacheRepository.java rename to src/main/java/cc/fascinated/repository/PlayerCacheRepository.java diff --git a/src/main/java/cc.fascinated/repository/PlayerNameCacheRepository.java b/src/main/java/cc/fascinated/repository/PlayerNameCacheRepository.java similarity index 100% rename from src/main/java/cc.fascinated/repository/PlayerNameCacheRepository.java rename to src/main/java/cc/fascinated/repository/PlayerNameCacheRepository.java diff --git a/src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java b/src/main/java/cc/fascinated/repository/PlayerSkinPartCacheRepository.java similarity index 100% rename from src/main/java/cc.fascinated/repository/PlayerSkinPartCacheRepository.java rename to src/main/java/cc/fascinated/repository/PlayerSkinPartCacheRepository.java diff --git a/src/main/java/cc.fascinated/service/MojangService.java b/src/main/java/cc/fascinated/service/MojangService.java similarity index 100% rename from src/main/java/cc.fascinated/service/MojangService.java rename to src/main/java/cc/fascinated/service/MojangService.java diff --git a/src/main/java/cc.fascinated/service/PlayerService.java b/src/main/java/cc/fascinated/service/PlayerService.java similarity index 83% rename from src/main/java/cc.fascinated/service/PlayerService.java rename to src/main/java/cc/fascinated/service/PlayerService.java index ffd6e26..c2c404d 100644 --- a/src/main/java/cc.fascinated/service/PlayerService.java +++ b/src/main/java/cc/fascinated/service/PlayerService.java @@ -1,5 +1,6 @@ package cc.fascinated.service; +import cc.fascinated.common.ImageUtils; import cc.fascinated.common.PlayerUtils; import cc.fascinated.common.Tuple; import cc.fascinated.common.UUIDUtils; @@ -14,7 +15,8 @@ import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangUsernameToUuid; import cc.fascinated.model.player.Cape; import cc.fascinated.model.player.Player; -import cc.fascinated.model.player.Skin; +import cc.fascinated.model.skin.ISkinPart; +import cc.fascinated.model.skin.Skin; import cc.fascinated.repository.PlayerCacheRepository; import cc.fascinated.repository.PlayerNameCacheRepository; import cc.fascinated.repository.PlayerSkinPartCacheRepository; @@ -22,6 +24,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.awt.image.BufferedImage; import java.util.Optional; import java.util.UUID; @@ -119,33 +122,41 @@ public class PlayerService { * Gets a skin part from the player's skin. * * @param player the player - * @param part the part of the skin + * @param partName the name of the part * @param renderOverlay whether to render the overlay * @return the skin part */ - public CachedPlayerSkinPart getSkinPart(Player player, Skin.Parts part, boolean renderOverlay, int size) { + public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) { if (size > 512) { log.info("Size {} is too large, setting to 512", size); size = 512; } - log.info("Getting skin part {} for player: {}", part.getName(), player.getUniqueId()); - String key = "%s-%s-%s".formatted(player.getUniqueId(), part.getName(), size); + ISkinPart part = ISkinPart.getByName(partName); // The skin part to get + if (part == null) { // Default to the face + part = ISkinPart.Vanilla.FACE; + log.warn("Invalid skin part {}, defaulting to {}", partName, part.name()); + } + + log.info("Getting skin part {} for player: {}", part.name(), player.getUniqueId()); + String key = "%s-%s-%s-%s".formatted(player.getUniqueId(), part.name(), size, renderOverlay); Optional cache = playerSkinPartCacheRepository.findById(key); // The skin part is cached if (cache.isPresent() && Config.INSTANCE.isProduction()) { - log.info("Skin part {} for player {} is cached", part.getName(), player.getUniqueId()); + log.info("Skin part {} for player {} is cached", part.name(), player.getUniqueId()); return cache.get(); } long before = System.currentTimeMillis(); - byte[] skinPartBytes = part.getRenderer().renderPart(player.getSkin(), part.getName(), renderOverlay, size); - log.info("Took {}ms to render skin part {} for player: {}", System.currentTimeMillis() - before, part.getName(), player.getUniqueId()); + BufferedImage renderedPart = part.render(player.getSkin(), renderOverlay, size); // Render the skin part + log.info("Took {}ms to render skin part {} for player: {}", System.currentTimeMillis() - before, part.name(), player.getUniqueId()); + + byte[] skinPartBytes = ImageUtils.imageToBytes(renderedPart); // Convert the image to bytes CachedPlayerSkinPart skinPart = new CachedPlayerSkinPart( key, skinPartBytes ); - log.info("Fetched skin part {} for player: {}", part.getName(), player.getUniqueId()); + log.info("Fetched skin part {} for player: {}", part.name(), player.getUniqueId()); playerSkinPartCacheRepository.save(skinPart); return skinPart; diff --git a/src/main/java/cc.fascinated/service/ServerService.java b/src/main/java/cc/fascinated/service/ServerService.java similarity index 100% rename from src/main/java/cc.fascinated/service/ServerService.java rename to src/main/java/cc/fascinated/service/ServerService.java diff --git a/src/main/java/cc.fascinated/service/pinger/MinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java similarity index 100% rename from src/main/java/cc.fascinated/service/pinger/MinecraftServerPinger.java rename to src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java diff --git a/src/main/java/cc.fascinated/service/pinger/impl/BedrockMinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/impl/BedrockMinecraftServerPinger.java similarity index 100% rename from src/main/java/cc.fascinated/service/pinger/impl/BedrockMinecraftServerPinger.java rename to src/main/java/cc/fascinated/service/pinger/impl/BedrockMinecraftServerPinger.java diff --git a/src/main/java/cc.fascinated/service/pinger/impl/JavaMinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java similarity index 100% rename from src/main/java/cc.fascinated/service/pinger/impl/JavaMinecraftServerPinger.java rename to src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java diff --git a/src/test/java/cc/fascinated/tests/PlayerControllerTests.java b/src/test/java/cc/fascinated/tests/PlayerControllerTests.java index 0dbf3a4..f278c67 100644 --- a/src/test/java/cc/fascinated/tests/PlayerControllerTests.java +++ b/src/test/java/cc/fascinated/tests/PlayerControllerTests.java @@ -1,7 +1,7 @@ package cc.fascinated.tests; import cc.fascinated.config.TestRedisConfig; -import cc.fascinated.model.player.Skin; +import cc.fascinated.model.skin.Skin; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;