mirror of
https://github.com/GravitLauncher/Launcher
synced 2024-11-15 03:31:15 +03:00
[FEATURE][EXPERIMENTAL] Support slim skins
This commit is contained in:
parent
b0538abe63
commit
4dd30faf7e
8 changed files with 139 additions and 20 deletions
|
@ -0,0 +1,44 @@
|
||||||
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.HTTPRequest;
|
||||||
|
import pro.gravit.launcher.Launcher;
|
||||||
|
import pro.gravit.launcher.profiles.Texture;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class JsonTextureProvider extends TextureProvider {
|
||||||
|
public String url;
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
//None
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getCloakTexture(UUID uuid, String username, String client) throws IOException {
|
||||||
|
logger.warn("Ineffective get cloak texture for {}", username);
|
||||||
|
return getTextures(uuid, username, client).cloak;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getSkinTexture(UUID uuid, String username, String client) throws IOException {
|
||||||
|
logger.warn("Ineffective get skin texture for {}", username);
|
||||||
|
return getTextures(uuid, username, client).skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SkinAndCloakTextures getTextures(UUID uuid, String username, String client) {
|
||||||
|
try {
|
||||||
|
var result = HTTPRequest.jsonRequest(null, "GET", new URL(RequestTextureProvider.getTextureURL(url, uuid, username, client)));
|
||||||
|
return Launcher.gsonManager.gson.fromJson(result, SkinAndCloakTextures.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("JsonTextureProvider", e);
|
||||||
|
return new SkinAndCloakTextures(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ private static Texture getTexture(String url, Path local, boolean cloak) throws
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getTextureURL(String url, UUID uuid, String username, String client) {
|
public static String getTextureURL(String url, UUID uuid, String username, String client) {
|
||||||
return CommonHelper.replace(url, "username", IOHelper.urlEncode(username),
|
return CommonHelper.replace(url, "username", IOHelper.urlEncode(username),
|
||||||
"uuid", IOHelper.urlEncode(uuid.toString()), "hash", IOHelper.urlEncode(Launcher.toHash(uuid)),
|
"uuid", IOHelper.urlEncode(uuid.toString()), "hash", IOHelper.urlEncode(Launcher.toHash(uuid)),
|
||||||
"client", IOHelper.urlEncode(client == null ? "unknown" : client));
|
"client", IOHelper.urlEncode(client == null ? "unknown" : client));
|
||||||
|
|
|
@ -17,6 +17,7 @@ public static void registerProviders() {
|
||||||
|
|
||||||
// Auth providers that doesn't do nothing :D
|
// Auth providers that doesn't do nothing :D
|
||||||
providers.register("request", RequestTextureProvider.class);
|
providers.register("request", RequestTextureProvider.class);
|
||||||
|
providers.register("json", JsonTextureProvider.class);
|
||||||
registredProv = true;
|
registredProv = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,4 +30,34 @@ public static void registerProviders() {
|
||||||
|
|
||||||
|
|
||||||
public abstract Texture getSkinTexture(UUID uuid, String username, String client) throws IOException;
|
public abstract Texture getSkinTexture(UUID uuid, String username, String client) throws IOException;
|
||||||
|
|
||||||
|
public static class SkinAndCloakTextures {
|
||||||
|
public final Texture skin;
|
||||||
|
public final Texture cloak;
|
||||||
|
|
||||||
|
public SkinAndCloakTextures(Texture skin, Texture cloak) {
|
||||||
|
this.skin = skin;
|
||||||
|
this.cloak = cloak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkinAndCloakTextures getTextures(UUID uuid, String username, String client) {
|
||||||
|
|
||||||
|
Texture skin;
|
||||||
|
try {
|
||||||
|
skin = getSkinTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
skin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cloak texture
|
||||||
|
Texture cloak;
|
||||||
|
try {
|
||||||
|
cloak = getCloakTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
cloak = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SkinAndCloakTextures(skin, cloak);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import pro.gravit.launcher.events.request.AuthRequestEvent;
|
import pro.gravit.launcher.events.request.AuthRequestEvent;
|
||||||
import pro.gravit.launcher.profiles.ClientProfile;
|
import pro.gravit.launcher.profiles.ClientProfile;
|
||||||
import pro.gravit.launcher.profiles.PlayerProfile;
|
import pro.gravit.launcher.profiles.PlayerProfile;
|
||||||
import pro.gravit.launcher.profiles.Texture;
|
|
||||||
import pro.gravit.launcher.request.auth.AuthRequest;
|
import pro.gravit.launcher.request.auth.AuthRequest;
|
||||||
import pro.gravit.launcher.request.auth.password.*;
|
import pro.gravit.launcher.request.auth.password.*;
|
||||||
import pro.gravit.launchserver.LaunchServer;
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
@ -309,23 +308,10 @@ public PlayerProfile getPlayerProfile(AuthProviderPair pair, User user) {
|
||||||
|
|
||||||
private PlayerProfile getPlayerProfile(UUID uuid, String username, String client, TextureProvider textureProvider) {
|
private PlayerProfile getPlayerProfile(UUID uuid, String username, String client, TextureProvider textureProvider) {
|
||||||
// Get skin texture
|
// Get skin texture
|
||||||
Texture skin;
|
TextureProvider.SkinAndCloakTextures textures = textureProvider.getTextures(uuid, username, client);
|
||||||
try {
|
|
||||||
skin = textureProvider.getSkinTexture(uuid, username, client);
|
|
||||||
} catch (IOException e) {
|
|
||||||
skin = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cloak texture
|
|
||||||
Texture cloak;
|
|
||||||
try {
|
|
||||||
cloak = textureProvider.getCloakTexture(uuid, username, client);
|
|
||||||
} catch (IOException e) {
|
|
||||||
cloak = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return combined profile
|
// Return combined profile
|
||||||
return new PlayerProfile(uuid, username, skin, cloak);
|
return new PlayerProfile(uuid, username, textures.skin, textures.cloak);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthRequest.AuthPasswordInterface decryptPassword(AuthRequest.AuthPasswordInterface password) throws AuthException {
|
public AuthRequest.AuthPasswordInterface decryptPassword(AuthRequest.AuthPasswordInterface password) throws AuthException {
|
||||||
|
|
|
@ -25,9 +25,15 @@ public final class Launcher {
|
||||||
|
|
||||||
public static final String SKIN_DIGEST_PROPERTY = "skinDigest";
|
public static final String SKIN_DIGEST_PROPERTY = "skinDigest";
|
||||||
|
|
||||||
|
public static final String SKIN_METADATA_PROPERTY = "skinMetadata";
|
||||||
|
|
||||||
public static final String CLOAK_URL_PROPERTY = "cloakURL";
|
public static final String CLOAK_URL_PROPERTY = "cloakURL";
|
||||||
|
|
||||||
public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest";
|
public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest";
|
||||||
|
|
||||||
|
public static final String CLOAK_METADATA_PROPERTY = "cloakMetadata";
|
||||||
|
|
||||||
|
|
||||||
// Used to determine from clientside is launched from launcher
|
// Used to determine from clientside is launched from launcher
|
||||||
public static final AtomicBoolean LAUNCHED = new AtomicBoolean(false);
|
public static final AtomicBoolean LAUNCHED = new AtomicBoolean(false);
|
||||||
public static final int PROTOCOL_MAGIC_LEGACY = 0x724724_00 + 24;
|
public static final int PROTOCOL_MAGIC_LEGACY = 0x724724_00 + 24;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class Texture extends StreamObject {
|
public final class Texture extends StreamObject {
|
||||||
|
@ -22,11 +23,14 @@ public final class Texture extends StreamObject {
|
||||||
|
|
||||||
public final byte[] digest;
|
public final byte[] digest;
|
||||||
|
|
||||||
|
public final Map<String, String> metadata;
|
||||||
|
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public Texture(HInput input) throws IOException {
|
public Texture(HInput input) throws IOException {
|
||||||
url = IOHelper.verifyURL(input.readASCII(2048));
|
url = IOHelper.verifyURL(input.readASCII(2048));
|
||||||
digest = input.readByteArray(-DIGEST_ALGO.bytes);
|
digest = input.readByteArray(-DIGEST_ALGO.bytes);
|
||||||
|
metadata = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Texture(String url, boolean cloak) throws IOException {
|
public Texture(String url, boolean cloak) throws IOException {
|
||||||
|
@ -43,6 +47,7 @@ public Texture(String url, boolean cloak) throws IOException {
|
||||||
|
|
||||||
// Get digest of texture
|
// Get digest of texture
|
||||||
digest = SecurityHelper.digest(DIGEST_ALGO, new URL(url));
|
digest = SecurityHelper.digest(DIGEST_ALGO, new URL(url));
|
||||||
|
metadata = null; // May be auto-detect?
|
||||||
}
|
}
|
||||||
|
|
||||||
public Texture(String url, Path local, boolean cloak) throws IOException {
|
public Texture(String url, Path local, boolean cloak) throws IOException {
|
||||||
|
@ -51,12 +56,20 @@ public Texture(String url, Path local, boolean cloak) throws IOException {
|
||||||
IOHelper.readTexture(input, cloak); // Verify texture
|
IOHelper.readTexture(input, cloak); // Verify texture
|
||||||
}
|
}
|
||||||
this.digest = Objects.requireNonNull(SecurityHelper.digest(DIGEST_ALGO, local), "digest");
|
this.digest = Objects.requireNonNull(SecurityHelper.digest(DIGEST_ALGO, local), "digest");
|
||||||
|
this.metadata = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Texture(String url, byte[] digest) {
|
public Texture(String url, byte[] digest) {
|
||||||
this.url = IOHelper.verifyURL(url);
|
this.url = IOHelper.verifyURL(url);
|
||||||
this.digest = Objects.requireNonNull(digest, "digest");
|
this.digest = Objects.requireNonNull(digest, "digest");
|
||||||
|
this.metadata = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Texture(String url, byte[] digest, Map<String, String> metadata) {
|
||||||
|
this.url = url;
|
||||||
|
this.digest = digest;
|
||||||
|
this.metadata = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class MinecraftProfileTexture {
|
public class MinecraftProfileTexture {
|
||||||
|
@ -10,6 +11,7 @@ public class MinecraftProfileTexture {
|
||||||
// Instance
|
// Instance
|
||||||
private final String url;
|
private final String url;
|
||||||
private final String hash;
|
private final String hash;
|
||||||
|
private final Map<String, String> metadata;
|
||||||
|
|
||||||
public MinecraftProfileTexture(String url) {
|
public MinecraftProfileTexture(String url) {
|
||||||
this(url, baseName(url));
|
this(url, baseName(url));
|
||||||
|
@ -18,6 +20,13 @@ public MinecraftProfileTexture(String url) {
|
||||||
public MinecraftProfileTexture(String url, String hash) {
|
public MinecraftProfileTexture(String url, String hash) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
|
this.metadata = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MinecraftProfileTexture(String url, String hash, Map<String, String> metadata) {
|
||||||
|
this.url = url;
|
||||||
|
this.hash = hash;
|
||||||
|
this.metadata = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String baseName(String url) {
|
private static String baseName(String url) {
|
||||||
|
@ -37,7 +46,10 @@ public String getHash() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMetadata(String key) {
|
public String getMetadata(String key) {
|
||||||
return null;
|
if (metadata == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return metadata.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package com.mojang.authlib.yggdrasil;
|
package com.mojang.authlib.yggdrasil;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.mojang.authlib.AuthenticationService;
|
import com.mojang.authlib.AuthenticationService;
|
||||||
import com.mojang.authlib.GameProfile;
|
import com.mojang.authlib.GameProfile;
|
||||||
import com.mojang.authlib.exceptions.AuthenticationException;
|
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
import pro.gravit.utils.helper.LogHelper;
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
import pro.gravit.utils.helper.SecurityHelper;
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
|
@ -29,6 +32,7 @@
|
||||||
|
|
||||||
public class YggdrasilMinecraftSessionService extends BaseMinecraftSessionService {
|
public class YggdrasilMinecraftSessionService extends BaseMinecraftSessionService {
|
||||||
public static final boolean NO_TEXTURES = Boolean.getBoolean("launcher.com.mojang.authlib.noTextures");
|
public static final boolean NO_TEXTURES = Boolean.getBoolean("launcher.com.mojang.authlib.noTextures");
|
||||||
|
private static final Gson gson = new Gson();
|
||||||
|
|
||||||
public YggdrasilMinecraftSessionService(AuthenticationService service) {
|
public YggdrasilMinecraftSessionService(AuthenticationService service) {
|
||||||
super(service);
|
super(service);
|
||||||
|
@ -53,6 +57,10 @@ public static void fillTextureProperties(GameProfile profile, PlayerProfile pp)
|
||||||
if (pp.skin != null) {
|
if (pp.skin != null) {
|
||||||
properties.put(Launcher.SKIN_URL_PROPERTY, new Property(Launcher.SKIN_URL_PROPERTY, pp.skin.url, ""));
|
properties.put(Launcher.SKIN_URL_PROPERTY, new Property(Launcher.SKIN_URL_PROPERTY, pp.skin.url, ""));
|
||||||
properties.put(Launcher.SKIN_DIGEST_PROPERTY, new Property(Launcher.SKIN_DIGEST_PROPERTY, SecurityHelper.toHex(pp.skin.digest), ""));
|
properties.put(Launcher.SKIN_DIGEST_PROPERTY, new Property(Launcher.SKIN_DIGEST_PROPERTY, SecurityHelper.toHex(pp.skin.digest), ""));
|
||||||
|
if (pp.skin.metadata != null) {
|
||||||
|
String metadata = serializeMetadataMap(pp.skin.metadata);
|
||||||
|
properties.put(Launcher.SKIN_METADATA_PROPERTY, new Property(Launcher.SKIN_METADATA_PROPERTY, metadata, ""));
|
||||||
|
}
|
||||||
if (debug) {
|
if (debug) {
|
||||||
LogHelper.debug("fillTextureProperties, Has skin texture for username '%s'", profile.getName());
|
LogHelper.debug("fillTextureProperties, Has skin texture for username '%s'", profile.getName());
|
||||||
}
|
}
|
||||||
|
@ -60,12 +68,29 @@ public static void fillTextureProperties(GameProfile profile, PlayerProfile pp)
|
||||||
if (pp.cloak != null) {
|
if (pp.cloak != null) {
|
||||||
properties.put(Launcher.CLOAK_URL_PROPERTY, new Property(Launcher.CLOAK_URL_PROPERTY, pp.cloak.url, ""));
|
properties.put(Launcher.CLOAK_URL_PROPERTY, new Property(Launcher.CLOAK_URL_PROPERTY, pp.cloak.url, ""));
|
||||||
properties.put(Launcher.CLOAK_DIGEST_PROPERTY, new Property(Launcher.CLOAK_DIGEST_PROPERTY, SecurityHelper.toHex(pp.cloak.digest), ""));
|
properties.put(Launcher.CLOAK_DIGEST_PROPERTY, new Property(Launcher.CLOAK_DIGEST_PROPERTY, SecurityHelper.toHex(pp.cloak.digest), ""));
|
||||||
|
if (pp.cloak.metadata != null) {
|
||||||
|
String metadata = serializeMetadataMap(pp.cloak.metadata);
|
||||||
|
properties.put(Launcher.CLOAK_METADATA_PROPERTY, new Property(Launcher.SKIN_METADATA_PROPERTY, metadata, ""));
|
||||||
|
}
|
||||||
if (debug) {
|
if (debug) {
|
||||||
LogHelper.debug("fillTextureProperties, Has cloak texture for username '%s'", profile.getName());
|
LogHelper.debug("fillTextureProperties, Has cloak texture for username '%s'", profile.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String serializeMetadataMap(Map<String, String> map) {
|
||||||
|
if (map == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return gson.toJson(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> deserializeMetadataMap(String value) {
|
||||||
|
Type typeOfMap = new TypeToken<Map<String, String>>() {
|
||||||
|
}.getType();
|
||||||
|
return gson.fromJson(value, typeOfMap);
|
||||||
|
}
|
||||||
|
|
||||||
private static void getTexturesMojang(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures, String texturesBase64, GameProfile profile) {
|
private static void getTexturesMojang(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures, String texturesBase64, GameProfile profile) {
|
||||||
// Decode textures payload
|
// Decode textures payload
|
||||||
JsonObject texturesJSON;
|
JsonObject texturesJSON;
|
||||||
|
@ -148,14 +173,16 @@ public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(Ga
|
||||||
// Add skin URL to textures map
|
// Add skin URL to textures map
|
||||||
Property skinURL = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_URL_PROPERTY), null);
|
Property skinURL = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_URL_PROPERTY), null);
|
||||||
Property skinDigest = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_DIGEST_PROPERTY), null);
|
Property skinDigest = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_DIGEST_PROPERTY), null);
|
||||||
|
Property skinMetadata = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_METADATA_PROPERTY), null);
|
||||||
if (skinURL != null && skinDigest != null)
|
if (skinURL != null && skinDigest != null)
|
||||||
textures.put(MinecraftProfileTexture.Type.SKIN, new MinecraftProfileTexture(skinURL.getValue(), skinDigest.getValue()));
|
textures.put(MinecraftProfileTexture.Type.SKIN, new MinecraftProfileTexture(skinURL.getValue(), skinDigest.getValue(), skinMetadata == null ? null : deserializeMetadataMap(skinMetadata.getValue())));
|
||||||
|
|
||||||
// Add cloak URL to textures map
|
// Add cloak URL to textures map
|
||||||
Property cloakURL = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_URL_PROPERTY), null);
|
Property cloakURL = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_URL_PROPERTY), null);
|
||||||
Property cloakDigest = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_DIGEST_PROPERTY), null);
|
Property cloakDigest = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_DIGEST_PROPERTY), null);
|
||||||
|
Property cloakMetadata = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_METADATA_PROPERTY), null);
|
||||||
if (cloakURL != null && cloakDigest != null)
|
if (cloakURL != null && cloakDigest != null)
|
||||||
textures.put(MinecraftProfileTexture.Type.CAPE, new MinecraftProfileTexture(cloakURL.getValue(), cloakDigest.getValue()));
|
textures.put(MinecraftProfileTexture.Type.CAPE, new MinecraftProfileTexture(cloakURL.getValue(), cloakDigest.getValue(), cloakMetadata == null ? null : deserializeMetadataMap(cloakMetadata.getValue())));
|
||||||
|
|
||||||
// Try to find missing textures in textures payload (now always true because launcher is not passing elytra skins)
|
// Try to find missing textures in textures payload (now always true because launcher is not passing elytra skins)
|
||||||
if (textures.size() != MinecraftProfileTexture.PROFILE_TEXTURE_COUNT) {
|
if (textures.size() != MinecraftProfileTexture.PROFILE_TEXTURE_COUNT) {
|
||||||
|
|
Loading…
Reference in a new issue