From 4cdffd47fd0b8df741e84e74d89ae72ffc93f664 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 8 Apr 2024 06:13:03 +0100 Subject: [PATCH] more cleanup --- src/main/java/cc/fascinated/Main.java | 8 +- .../controller/PlayerController.java | 21 +-- .../java/cc/fascinated/model/player/Cape.java | 14 ++ .../cc/fascinated/model/player/Player.java | 9 +- .../java/cc/fascinated/model/player/Skin.java | 125 +++++++++--------- .../cc/fascinated/model/player/SkinPart.java | 69 ---------- .../cc/fascinated/service/PlayerService.java | 4 +- .../service/mojang/model/MojangProfile.java | 46 ++++--- .../java/cc/fascinated/util/PlayerUtils.java | 71 ++++++++++ .../java/cc/fascinated/util/UUIDUtils.java | 2 +- 10 files changed, 186 insertions(+), 183 deletions(-) delete mode 100644 src/main/java/cc/fascinated/model/player/SkinPart.java create mode 100644 src/main/java/cc/fascinated/util/PlayerUtils.java diff --git a/src/main/java/cc/fascinated/Main.java b/src/main/java/cc/fascinated/Main.java index 44beb92..5383feb 100644 --- a/src/main/java/cc/fascinated/Main.java +++ b/src/main/java/cc/fascinated/Main.java @@ -1,7 +1,6 @@ package cc.fascinated; import com.google.gson.Gson; -import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.springframework.boot.SpringApplication; @@ -16,11 +15,8 @@ import java.util.Objects; @SpringBootApplication @Log4j2 public class Main { - @Getter - private static final Gson GSON = new Gson(); - - @Getter - private static final HttpClient CLIENT = HttpClient.newHttpClient(); + public static final Gson GSON = new Gson(); + public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); @SneakyThrows public static void main(String[] args) { diff --git a/src/main/java/cc/fascinated/controller/PlayerController.java b/src/main/java/cc/fascinated/controller/PlayerController.java index ae57fae..834a62d 100644 --- a/src/main/java/cc/fascinated/controller/PlayerController.java +++ b/src/main/java/cc/fascinated/controller/PlayerController.java @@ -3,8 +3,7 @@ package cc.fascinated.controller; import cc.fascinated.service.PlayerService; import cc.fascinated.model.player.Player; import cc.fascinated.model.player.Skin; -import cc.fascinated.model.player.SkinPart; -import lombok.NonNull; +import cc.fascinated.util.PlayerUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.CacheControl; import org.springframework.http.HttpStatus; @@ -13,7 +12,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; @RestController @@ -21,9 +19,6 @@ import java.util.concurrent.TimeUnit; public class PlayerController { private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic(); - @NonNull - private final SkinPart defaultHead = Objects.requireNonNull(Skin.getDefaultHead(), "Default head is null"); - private final PlayerService playerManagerService; @Autowired @@ -48,22 +43,20 @@ public class PlayerController { @PathVariable String id, @RequestParam(required = false, defaultValue = "250") int size) { Player player = playerManagerService.getPlayer(id); - byte[] headBytes = new byte[0]; + byte[] partBytes = new byte[0]; if (player != null) { // The player exists Skin skin = player.getSkin(); - SkinPart skinPart = skin.getPart(part); - if (skinPart != null) { - headBytes = skinPart.getPartData(size); - } + Skin.Parts skinPart = Skin.Parts.fromName(part); + partBytes = PlayerUtils.getSkinPartBytes(skin, skinPart, size); } - if (headBytes == null) { // Fallback to the default head - headBytes = defaultHead.getPartData(size); + if (partBytes == null) { // Fallback to the default head + partBytes = PlayerUtils.getSkinPartBytes(Skin.DEFAULT_SKIN, Skin.Parts.HEAD, size); } return ResponseEntity.ok() .cacheControl(cacheControl) .contentType(MediaType.IMAGE_PNG) - .body(headBytes); + .body(partBytes); } } diff --git a/src/main/java/cc/fascinated/model/player/Cape.java b/src/main/java/cc/fascinated/model/player/Cape.java index 6b490be..cb26d0a 100644 --- a/src/main/java/cc/fascinated/model/player/Cape.java +++ b/src/main/java/cc/fascinated/model/player/Cape.java @@ -1,5 +1,6 @@ package cc.fascinated.model.player; +import com.google.gson.JsonObject; import lombok.AllArgsConstructor; import lombok.Getter; @@ -10,4 +11,17 @@ public class Cape { * The URL of the cape */ private final String url; + + /** + * Gets the cape from a {@link JsonObject}. + * + * @param json the JSON object + * @return the cape + */ + public static Cape fromJson(JsonObject json) { + if (json == null) { + return null; + } + return new Cape(json.get("url").getAsString()); + } } diff --git a/src/main/java/cc/fascinated/model/player/Player.java b/src/main/java/cc/fascinated/model/player/Player.java index 4a3d50e..6c5d578 100644 --- a/src/main/java/cc/fascinated/model/player/Player.java +++ b/src/main/java/cc/fascinated/model/player/Player.java @@ -5,7 +5,6 @@ import cc.fascinated.util.Tuple; import cc.fascinated.util.UUIDUtils; import lombok.Getter; -import java.util.List; import java.util.UUID; @Getter @@ -33,15 +32,9 @@ public class Player { */ private Cape cape; - /** - * The raw properties of the player - */ - private final List rawProperties; - public Player(MojangProfile profile) { - this.uuid = UUID.fromString(UUIDUtils.addUUIDDashes(profile.getId())); + this.uuid = UUID.fromString(UUIDUtils.addUuidDashes(profile.getId())); this.name = profile.getName(); - this.rawProperties = profile.getProperties(); // Get the skin and cape Tuple skinAndCape = profile.getSkinAndCape(); diff --git a/src/main/java/cc/fascinated/model/player/Skin.java b/src/main/java/cc/fascinated/model/player/Skin.java index 1b73873..917e939 100644 --- a/src/main/java/cc/fascinated/model/player/Skin.java +++ b/src/main/java/cc/fascinated/model/player/Skin.java @@ -1,23 +1,22 @@ package cc.fascinated.model.player; -import cc.fascinated.Main; 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.SneakyThrows; import lombok.extern.log4j.Log4j2; -import java.io.InputStream; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.util.HashMap; import java.util.Map; @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 of the skin @@ -27,78 +26,44 @@ public class Skin { /** * The model of the skin */ - private final SkinType model; - - /** - * The bytes of the skin - */ - @JsonIgnore - private final byte[] skinBytes; - - /** - * The skin parts for this skin - */ - @JsonIgnore - private final Map parts = new HashMap<>(); + private final Model model; @JsonProperty("parts") private final Map partUrls = new HashMap<>(); - public Skin(String playerUuid, String url, SkinType model) { + public Skin(String url, Model model) { this.url = url; this.model = model; - this.skinBytes = this.getSkinData(); - - // The skin parts - this.parts.put(SkinPartEnum.HEAD, new SkinPart(this.skinBytes, SkinPartEnum.HEAD)); - - for (Map.Entry entry : this.parts.entrySet()) { - String partName = entry.getKey().name().toLowerCase(); - this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=250"); - } } /** - * Gets the default/fallback head. + * Gets the skin from a {@link JsonObject}. * - * @return the default head + * @param json the JSON object + * @return the skin */ - public static SkinPart getDefaultHead() { - try (InputStream stream = Main.class.getClassLoader().getResourceAsStream("images/default_head.png")) { - if (stream == null) { - return null; - } - byte[] bytes = stream.readAllBytes(); - return new SkinPart(bytes, SkinPartEnum.HEAD); - } catch (Exception ex) { - log.warn("Failed to load default head", ex); + 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 ? "slim" : // Fall back to slim if the model is not found + metadata.get("model").getAsString()); + return new Skin(url, model); } /** - * Gets the skin data from the URL. + * Populates the part URLs for the skin. * - * @return the skin data + * @param playerUuid the player's UUID */ - @SneakyThrows @JsonIgnore - public byte[] getSkinData() { - HttpRequest request = HttpRequest.newBuilder() - .uri(new URI(this.url)) - .GET() - .build(); - - return Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofByteArray()).body(); - } - - /** - * Gets a part from the skin. - * - * @param part the part name - * @return the part - */ - public SkinPart getPart(String part) { - return this.parts.get(SkinPartEnum.valueOf(part.toUpperCase())); + 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 + "?size=250"); + } + return this; } /** @@ -106,7 +71,7 @@ public class Skin { * information about the part. */ @Getter @AllArgsConstructor - public enum SkinPartEnum { + public enum Parts { HEAD(8, 8, 8, 8, 250); @@ -124,13 +89,43 @@ public class Skin { * The scale of the part. */ private final int defaultSize; + + /** + * Gets the skin part from its name. + * + * @param name the name of the part + * @return the skin part + */ + public static Parts fromName(String name) { + for (Parts part : values()) { + if (part.name().equalsIgnoreCase(name)) { + return part; + } + } + return null; + } } /** - * The type of the skin. + * The model of the skin. */ - public enum SkinType { + public enum Model { DEFAULT, - SLIM + 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/player/SkinPart.java b/src/main/java/cc/fascinated/model/player/SkinPart.java deleted file mode 100644 index 6adda60..0000000 --- a/src/main/java/cc/fascinated/model/player/SkinPart.java +++ /dev/null @@ -1,69 +0,0 @@ -package cc.fascinated.model.player; - -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; - -@Getter @Log4j2 -public class SkinPart { - - /** - * The whole skin data. - */ - private final byte[] data; - - /** - * The information about the part. - */ - private final Skin.SkinPartEnum skinPartEnum; - - /** - * The part data from the skin. - */ - private byte[] partBytes; - - public SkinPart(byte[] data, Skin.SkinPartEnum skinPartEnum) { - this.data = data; - this.skinPartEnum = skinPartEnum; - } - - /** - * Gets the part data from the skin. - * - * @return the part data - */ - public byte[] getPartData(int size) { - if (size == -1) { - size = this.skinPartEnum.getDefaultSize(); - } - - try { - BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.data)); - if (image == null) { - return null; - } - // Get the part of the image (e.g. the head) - BufferedImage partImage = image.getSubimage(this.skinPartEnum.getX(), this.skinPartEnum.getY(), this.skinPartEnum.getWidth(), this.skinPartEnum.getHeight()); - - // Scale the image - BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType()); - Graphics2D graphics2D = scaledImage.createGraphics(); - graphics2D.drawImage(partImage, 0, 0, size, size, null); - graphics2D.dispose(); - partImage = scaledImage; - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ImageIO.write(partImage, "png", byteArrayOutputStream); - this.partBytes = byteArrayOutputStream.toByteArray(); - return this.partBytes; - } catch (Exception ex) { - log.error("Failed to read image from skin data.", ex); - return null; - } - } -} diff --git a/src/main/java/cc/fascinated/service/PlayerService.java b/src/main/java/cc/fascinated/service/PlayerService.java index c280654..21b6c76 100644 --- a/src/main/java/cc/fascinated/service/PlayerService.java +++ b/src/main/java/cc/fascinated/service/PlayerService.java @@ -49,7 +49,7 @@ public class PlayerService { UUID uuid = null; if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID try { - uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUUIDDashes(id) : id); + uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id); } catch (Exception ignored) {} } else { // Check if the id is a name uuid = playerNameToUUIDCache.get(id.toUpperCase()); @@ -68,7 +68,7 @@ public class PlayerService { } // Get the profile of the player using their UUID profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ? - UUIDUtils.addUUIDDashes(apiProfile.getId()) : apiProfile.getId()); + UUIDUtils.addUuidDashes(apiProfile.getId()) : apiProfile.getId()); } if (profile == null) { // The player cannot be found using their name or UUID log.info("Player with id {} could not be found", id); diff --git a/src/main/java/cc/fascinated/service/mojang/model/MojangProfile.java b/src/main/java/cc/fascinated/service/mojang/model/MojangProfile.java index 7df3f3d..dc69d13 100644 --- a/src/main/java/cc/fascinated/service/mojang/model/MojangProfile.java +++ b/src/main/java/cc/fascinated/service/mojang/model/MojangProfile.java @@ -4,12 +4,14 @@ import cc.fascinated.Main; import cc.fascinated.model.player.Cape; import cc.fascinated.model.player.Skin; import cc.fascinated.util.Tuple; +import cc.fascinated.util.UUIDUtils; import com.google.gson.JsonObject; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import java.util.ArrayList; +import java.util.Base64; import java.util.List; @Getter @NoArgsConstructor @@ -36,36 +38,35 @@ public class MojangProfile { * @return the skin and cape of the player */ public Tuple getSkinAndCape() { - ProfileProperty textureProperty = getTextureProperty(); + ProfileProperty textureProperty = getProfileProperty("textures"); if (textureProperty == null) { return null; } - // Decode the texture property - String decoded = new String(java.util.Base64.getDecoder().decode(textureProperty.getValue())); + JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property + JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object - // Parse the decoded JSON - JsonObject json = Main.getGSON().fromJson(decoded, JsonObject.class); - JsonObject texturesJson = json.getAsJsonObject("textures"); - JsonObject skinJson = texturesJson.getAsJsonObject("SKIN"); - JsonObject capeJson = texturesJson.getAsJsonObject("CAPE"); - JsonObject metadataJson = skinJson.get("metadata").getAsJsonObject(); + return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()), + Cape.fromJson(texturesJson.getAsJsonObject("CAPE"))); + } - Skin skin = new Skin(id, skinJson.get("url").getAsString(), - Skin.SkinType.valueOf(metadataJson.get("model").getAsString().toUpperCase())); - Cape cape = new Cape(capeJson.get("url").getAsString()); - - return new Tuple<>(skin, cape); + /** + * Gets the formatted UUID of the player. + * + * @return the formatted UUID + */ + public String getFormattedUuid() { + return id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id; } /** - * Get the texture property of the player. + * Get a profile property for the player * - * @return the texture property + * @return the profile property */ - public ProfileProperty getTextureProperty() { + public ProfileProperty getProfileProperty(String name) { for (ProfileProperty property : properties) { - if (property.getName().equals("textures")) { + if (property.getName().equals(name)) { return property; } } @@ -89,6 +90,15 @@ public class MojangProfile { */ private String signature; + /** + * Decodes the value for this property. + * + * @return the decoded value + */ + public String getDecodedValue() { + return new String(Base64.getDecoder().decode(this.value)); + } + /** * Check if the property is signed. * diff --git a/src/main/java/cc/fascinated/util/PlayerUtils.java b/src/main/java/cc/fascinated/util/PlayerUtils.java new file mode 100644 index 0000000..d1e7071 --- /dev/null +++ b/src/main/java/cc/fascinated/util/PlayerUtils.java @@ -0,0 +1,71 @@ +package cc.fascinated.util; + +import cc.fascinated.Main; +import cc.fascinated.model.player.Skin; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +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; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@UtilityClass @Log4j2 +public class PlayerUtils { + + /** + * Gets the skin data from the URL. + * + * @return the skin data + */ + @SneakyThrows + @JsonIgnore + public static byte[] getSkinData(String url) { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(url)) + .GET() + .build(); + + return Main.HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray()).body(); + } + + /** + * Gets the part data from the skin. + * + * @return the part data + */ + public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) { + if (size == -1) { + size = part.getDefaultSize(); + } + + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(PlayerUtils.getSkinData(skin.getUrl()))); + if (image == null) { + return null; + } + // Get the part of the image (e.g. the head) + BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight()); + + // Scale the image + BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType()); + Graphics2D graphics2D = scaledImage.createGraphics(); + graphics2D.drawImage(partImage, 0, 0, size, size, null); + graphics2D.dispose(); + partImage = scaledImage; + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(partImage, "png", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } catch (Exception ex) { + log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex); + return null; + } + } +} diff --git a/src/main/java/cc/fascinated/util/UUIDUtils.java b/src/main/java/cc/fascinated/util/UUIDUtils.java index 5d42eac..c20e71c 100644 --- a/src/main/java/cc/fascinated/util/UUIDUtils.java +++ b/src/main/java/cc/fascinated/util/UUIDUtils.java @@ -11,7 +11,7 @@ public class UUIDUtils { * @param idNoDashes the UUID without dashes * @return the UUID with dashes */ - public static String addUUIDDashes(String idNoDashes) { + public static String addUuidDashes(String idNoDashes) { StringBuilder idBuff = new StringBuilder(idNoDashes); idBuff.insert(20, '-'); idBuff.insert(16, '-');