Add Skin and Cape textures to players

This commit is contained in:
Braydon 2024-04-06 17:23:22 -04:00
parent 035b86920a
commit 16403673d3
7 changed files with 169 additions and 4 deletions

@ -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<L, R> {
/**
* The left value.
*/
private L left;
/**
* The right value.
*/
private R right;
}

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

@ -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.
*/

@ -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
}
}

@ -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;
}
}

@ -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<Skin, Cape> 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?
*

@ -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.
* </p>
*
* @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<Skin, Cape> 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()
);