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()
);