From 16403673d30cbbeb226a9466b304f3c870a41f4d Mon Sep 17 00:00:00 2001 From: Rainnny7 Date: Sat, 6 Apr 2024 17:23:22 -0400 Subject: [PATCH] Add Skin and Cape textures to players --- src/main/java/me/braydon/mc/common/Tuple.java | 24 +++++++++ src/main/java/me/braydon/mc/model/Cape.java | 30 ++++++++++++ src/main/java/me/braydon/mc/model/Player.java | 10 ++++ src/main/java/me/braydon/mc/model/Skin.java | 43 ++++++++++++++++ .../braydon/mc/model/cache/CachedPlayer.java | 7 ++- .../mc/model/token/MojangProfileToken.java | 49 +++++++++++++++++++ .../me/braydon/mc/service/MojangService.java | 10 +++- 7 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/main/java/me/braydon/mc/common/Tuple.java create mode 100644 src/main/java/me/braydon/mc/model/Cape.java diff --git a/src/main/java/me/braydon/mc/common/Tuple.java b/src/main/java/me/braydon/mc/common/Tuple.java new file mode 100644 index 0000000..c70e474 --- /dev/null +++ b/src/main/java/me/braydon/mc/common/Tuple.java @@ -0,0 +1,24 @@ +package me.braydon.mc.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * A simple tuple utility. + * + * @author Braydon + */ +@NoArgsConstructor @AllArgsConstructor @Setter @Getter +public class Tuple { + /** + * The left value. + */ + private L left; + + /** + * The right value. + */ + private R right; +} \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/Cape.java b/src/main/java/me/braydon/mc/model/Cape.java new file mode 100644 index 0000000..5da18dd --- /dev/null +++ b/src/main/java/me/braydon/mc/model/Cape.java @@ -0,0 +1,30 @@ +package me.braydon.mc.model; + +import com.google.gson.JsonObject; +import lombok.*; + +/** + * A cape for a {@link Player}. + * + * @author Braydon + */ +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString +public final class Cape { + /** + * The texture URL of this cape. + */ + @NonNull private final String url; + + /** + * Build a cape from the given Json object. + * + * @param jsonObject the json object to build from + * @return the built cape + */ + public static Cape fromJsonObject(JsonObject jsonObject) { + if (jsonObject == null) { // No object to parse + return null; + } + return new Cape(jsonObject.get("url").getAsString()); + } +} \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/Player.java b/src/main/java/me/braydon/mc/model/Player.java index c538897..4113a74 100644 --- a/src/main/java/me/braydon/mc/model/Player.java +++ b/src/main/java/me/braydon/mc/model/Player.java @@ -26,6 +26,16 @@ public class Player { */ @NonNull private String username; + /** + * The skin of this player. + */ + @NonNull private Skin skin; + + /** + * The cape of this player, null if none. + */ + private Cape cape; + /** * The profile actions this player has, null if none. */ diff --git a/src/main/java/me/braydon/mc/model/Skin.java b/src/main/java/me/braydon/mc/model/Skin.java index d474320..26eaa0f 100644 --- a/src/main/java/me/braydon/mc/model/Skin.java +++ b/src/main/java/me/braydon/mc/model/Skin.java @@ -1,7 +1,50 @@ package me.braydon.mc.model; +import com.google.gson.JsonObject; +import lombok.*; + /** + * A skin for a {@link Player}. + * * @author Braydon */ +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString public final class Skin { + public static final Skin DEFAULT_STEVE = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", Model.DEFAULT); + + /** + * The texture URL of this skin. + */ + @NonNull private final String url; + + /** + * The model of this skin. + */ + @NonNull private final Model model; + + /** + * Build a skin from the given Json object. + * + * @param jsonObject the json object to build from + * @return the built skin + */ + public static Skin fromJsonObject(JsonObject jsonObject) { + if (jsonObject == null) { // No object to parse + return null; + } + Model model = Model.DEFAULT; // The skin model + + JsonObject metadataJsonObject = jsonObject.getAsJsonObject("metadata"); + if (metadataJsonObject != null) { // Parse the skin model + model = Model.valueOf(metadataJsonObject.get("model").getAsString().toUpperCase()); + } + return new Skin(jsonObject.get("url").getAsString(), model); + } + + /** + * Possible models for a skin. + */ + public enum Model { + SLIM, DEFAULT + } } \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java b/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java index c38ad51..b29a44a 100644 --- a/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java +++ b/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java @@ -4,8 +4,10 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.ToString; +import me.braydon.mc.model.Cape; import me.braydon.mc.model.Player; import me.braydon.mc.model.ProfileAction; +import me.braydon.mc.model.Skin; import org.springframework.data.redis.core.RedisHash; import java.io.Serializable; @@ -26,8 +28,9 @@ public final class CachedPlayer extends Player implements Serializable { */ private long cached; - public CachedPlayer(@NonNull UUID uniqueId, @NonNull String username, ProfileAction[] profileActions, long cached) { - super(uniqueId, username, profileActions); + public CachedPlayer(@NonNull UUID uniqueId, @NonNull String username, + @NonNull Skin skin, Cape cape, ProfileAction[] profileActions, long cached) { + super(uniqueId, username, skin, cape, profileActions); this.cached = cached; } } \ No newline at end of file diff --git a/src/main/java/me/braydon/mc/model/token/MojangProfileToken.java b/src/main/java/me/braydon/mc/model/token/MojangProfileToken.java index 00e5e9a..bbb43a4 100644 --- a/src/main/java/me/braydon/mc/model/token/MojangProfileToken.java +++ b/src/main/java/me/braydon/mc/model/token/MojangProfileToken.java @@ -1,7 +1,14 @@ package me.braydon.mc.model.token; +import com.google.gson.JsonObject; import lombok.*; +import me.braydon.mc.RESTfulMC; +import me.braydon.mc.common.Tuple; +import me.braydon.mc.model.Cape; import me.braydon.mc.model.ProfileAction; +import me.braydon.mc.model.Skin; + +import java.util.Base64; /** * A token representing a Mojang user profile. @@ -31,6 +38,37 @@ public final class MojangProfileToken { */ @NonNull private ProfileAction[] profileActions; + public Tuple getSkinAndCape() { + ProfileProperty textures = getPropertyByName("textures"); // Get the profile textures + if (textures == null) { // No profile textures + return new Tuple<>(); + } + JsonObject jsonObject = RESTfulMC.GSON.fromJson(textures.getDecodedValue(), JsonObject.class); // Get the Json object + JsonObject texturesJsonObject = jsonObject.getAsJsonObject("textures"); // Get the textures object + + // Return the tuple containing the skin and cape + return new Tuple<>( + Skin.fromJsonObject(texturesJsonObject.getAsJsonObject("SKIN")), + Cape.fromJsonObject(texturesJsonObject.getAsJsonObject("CAPE")) + ); + } + + /** + * Get the profile property + * with the given name. + * + * @param name the property name + * @return the profile property, null if none + */ + public ProfileProperty getPropertyByName(@NonNull String name) { + for (ProfileProperty property : properties) { + if (property.getName().equalsIgnoreCase(name)) { + return property; + } + } + return null; + } + /** * A property of a Mojang profile. */ @@ -51,6 +89,17 @@ public final class MojangProfileToken { */ private String signature; + /** + * Get the decoded Base64 + * value of this property. + * + * @return the decoded value + */ + @NonNull + public String getDecodedValue() { + return new String(Base64.getDecoder().decode(value)); + } + /** * Is this property signed? * diff --git a/src/main/java/me/braydon/mc/service/MojangService.java b/src/main/java/me/braydon/mc/service/MojangService.java index 5e16fe0..94ba091 100644 --- a/src/main/java/me/braydon/mc/service/MojangService.java +++ b/src/main/java/me/braydon/mc/service/MojangService.java @@ -2,13 +2,16 @@ package me.braydon.mc.service; import lombok.NonNull; import lombok.extern.log4j.Log4j2; +import me.braydon.mc.common.Tuple; import me.braydon.mc.common.UUIDUtils; import me.braydon.mc.common.web.JsonWebException; import me.braydon.mc.common.web.JsonWebRequest; import me.braydon.mc.exception.impl.BadRequestException; import me.braydon.mc.exception.impl.ResourceNotFoundException; +import me.braydon.mc.model.Cape; import me.braydon.mc.model.Player; import me.braydon.mc.model.ProfileAction; +import me.braydon.mc.model.Skin; import me.braydon.mc.model.cache.CachedPlayer; import me.braydon.mc.model.cache.CachedPlayerName; import me.braydon.mc.model.token.MojangProfileToken; @@ -60,10 +63,10 @@ public final class MojangService { * and then return the response. *

* - * @param query the query to search for the player by + * @param query the query to search for the player by * @param bypassCache should the cache be bypassed? * @return the player - * @throws BadRequestException if the UUID is malformed + * @throws BadRequestException if the UUID is malformed * @throws ResourceNotFoundException if the player is not found */ @NonNull @@ -100,11 +103,14 @@ public final class MojangService { MojangProfileToken token = JsonWebRequest.makeRequest( UUID_TO_PROFILE.formatted(uuid), HttpMethod.GET ).execute(MojangProfileToken.class); + Tuple skinAndCape = token.getSkinAndCape(); // Get the skin and cape ProfileAction[] profileActions = token.getProfileActions(); // Build our player model, cache it, and then return it CachedPlayer player = new CachedPlayer( uuid, token.getName(), + skinAndCape.getLeft() == null ? Skin.DEFAULT_STEVE : skinAndCape.getLeft(), + skinAndCape.getRight(), profileActions.length == 0 ? null : profileActions, System.currentTimeMillis() );