From 18980c2ca2493f6bbca70adaa2de8f3252b0cd34 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Tue, 2 Jan 2018 00:18:50 -0500 Subject: [PATCH] First touches on multiple skin server support. --- build.gradle | 2 +- build.number | 4 +- .../voxelmodpack/hdskins/HDSkinManager.java | 110 ++++++--------- .../voxelmodpack/hdskins/gui/GuiSkins.java | 101 +++++--------- .../hdskins/mixin/MixinPlayerInfo.java | 20 +-- .../hdskins/skins/AbstractSkinServer.java | 58 ++++++++ .../hdskins/skins/AsyncCacheLoader.java | 42 ++++++ .../hdskins/skins/LegacySkinServer.java | 129 ++++++++++++++++++ .../hdskins/skins/SkinServer.java | 28 ++++ .../hdskins/skins/SkinUploadResponse.java | 33 +++++ .../hdskins/skins/TexturesPayloadBuilder.java | 64 +++++++++ .../hdskins/skins/YggSkinServer.java | 45 ++++++ .../hdskins/skins/package-info.java | 7 + .../upload/IUploadCompleteCallback.java | 6 - .../upload/ThreadMultipartPostUpload.java | 88 ++++-------- .../com/minelittlepony/MineLittlePony.java | 4 + 16 files changed, 527 insertions(+), 214 deletions(-) create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/AbstractSkinServer.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/AsyncCacheLoader.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/LegacySkinServer.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinServer.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinUploadResponse.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/TexturesPayloadBuilder.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/YggSkinServer.java create mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/skins/package-info.java delete mode 100644 src/hdskins/java/com/voxelmodpack/hdskins/upload/IUploadCompleteCallback.java diff --git a/build.gradle b/build.gradle index d206d6e7..d234d2c6 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ apply plugin: 'net.minecraftforge.gradle.liteloader' apply plugin: 'org.spongepowered.mixin' group = 'com.minelittlepony' -version = '1.12.2.1' +version = '1.12.2.2-SNAPSHOT' description = 'Mine Little Pony' targetCompatibility = 1.8 diff --git a/build.number b/build.number index 7297bf3d..db875869 100644 --- a/build.number +++ b/build.number @@ -1,3 +1,3 @@ #Build Number for ANT. Do not edit! -#Fri Oct 06 13:17:50 EDT 2017 -build.number=488 +#Tue Jan 02 00:10:52 EST 2018 +build.number=495 diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java b/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java index af557221..29a58ccb 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java @@ -1,20 +1,21 @@ package com.voxelmodpack.hdskins; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gson.Gson; -import com.google.gson.JsonObject; +import com.google.gson.GsonBuilder; import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mojang.authlib.properties.Property; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; import com.mumfrey.liteloader.core.LiteLoader; import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.voxelmodpack.hdskins.resource.SkinResourceManager; +import com.voxelmodpack.hdskins.skins.LegacySkinServer; +import com.voxelmodpack.hdskins.skins.SkinServer; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.IImageBuffer; import net.minecraft.client.renderer.texture.ITextureObject; @@ -31,6 +32,7 @@ import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -44,12 +46,13 @@ public final class HDSkinManager implements IResourceManagerReloadListener { public static final HDSkinManager INSTANCE = new HDSkinManager(); private static final ResourceLocation LOADING = new ResourceLocation("LOADING"); + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .create(); - private String gatewayUrl = "skinmanager.voxelmodpack.com"; - private String skinUrl = "skins.voxelmodpack.com"; + private List skinServers = Lists.newArrayList(); private boolean enabled = true; - private Map> profileTextures = Maps.newHashMap(); private Map> skinCache = Maps.newHashMap(); private List skinModifiers = Lists.newArrayList(); @@ -57,6 +60,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener { private ExecutorService executor = Executors.newCachedThreadPool(); public HDSkinManager() { + addSkinServer(new LegacySkinServer("http://skins.voxelmodpack.com", "http://skinmanager.voxelmodpack.com")); } public Optional getSkinLocation(GameProfile profile1, final Type type, boolean loadIfAbsent) { @@ -69,21 +73,16 @@ public final class HDSkinManager implements IResourceManagerReloadListener { // try to recreate a broken gameprofile // happens when server sends a random profile with skin and displayname - Property prop = Iterables.getFirst(profile1.getProperties().get("textures"), null); - if (prop != null && Strings.isNullOrEmpty(prop.getValue())) { - JsonObject obj = new Gson().fromJson(new String(Base64.decodeBase64(prop.getValue())), JsonObject.class); - // why are plugins sending a json null? - if (obj != null) { - String name = null; - // this should be optional - if (obj.has("profileName")) { - name = obj.get("profileName").getAsString(); - } - // this is required - if (obj.has("profileId")) { - UUID uuid = UUIDTypeAdapter.fromString(obj.get("profileId").getAsString()); + Property textures = Iterables.getFirst(profile1.getProperties().get("textures"), null); + if (textures != null) { + MinecraftTexturesPayload texturePayload = GSON.fromJson(new String(Base64.decodeBase64(textures.getValue())), MinecraftTexturesPayload.class); + if (texturePayload != null) { + // name is optional + String name = texturePayload.getProfileName(); + UUID uuid = texturePayload.getProfileId(); + // uuid is required + if (uuid != null) profile1 = new GameProfile(uuid, name); - } } } final GameProfile profile = profile1; @@ -94,7 +93,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener { skin = this.skinCache.get(profile.getId()).get(type); if (skin == null) { - if (loadIfAbsent) { + if (loadIfAbsent && getProfileData(profile).containsKey(type)) { skinCache.get(profile.getId()).put(type, LOADING); //noinspection Convert2Lambda executor.submit(() -> loadTexture(profile, type, new SkinAvailableCallback() { @@ -103,17 +102,16 @@ public final class HDSkinManager implements IResourceManagerReloadListener { skinCache.get(profile.getId()).put(type1, location); } })); + } return Optional.empty(); } - return skin == LOADING ? Optional.empty() : Optional.of(skin); - } private void loadTexture(GameProfile profile, final Type type, final SkinAvailableCallback callback) { if (profile.getId() != null) { - Map data = loadProfileData(profile); + Map data = getProfileData(profile); final MinecraftProfileTexture texture = data.get(type); if (texture == null) { return; @@ -150,51 +148,24 @@ public final class HDSkinManager implements IResourceManagerReloadListener { } } - public Optional> getProfileData(GameProfile profile) { - if (!enabled) - return Optional.of(ImmutableMap.of()); - return Optional.ofNullable(this.profileTextures.get(profile.getId())); + public Map getProfileData(GameProfile profile) { + EnumMap textures = Maps.newEnumMap(Type.class); + for (SkinServer server : skinServers) { + Optional profileData = server.getProfileData(profile); + profileData.map(MinecraftTexturesPayload::getTextures).ifPresent(it -> it.forEach(textures::putIfAbsent)); + if (textures.size() == Type.values().length) + break; + } + return textures; } - private Map loadProfileData(final GameProfile profile) { - return getProfileData(profile).orElseGet(() -> { - - String uuid = UUIDTypeAdapter.fromUUID(profile.getId()); - - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (Type type : Type.values()) { - String url = getCustomTextureURLForId(type, uuid); - - builder.put(type, new MinecraftProfileTexture(url, null)); - } - - Map textures = builder.build(); - this.profileTextures.put(profile.getId(), textures); - return textures; - }); + public void addSkinServer(SkinServer skinServer) { + this.skinServers.add(0, skinServer); } - public void setSkinUrl(String skinUrl) { - this.skinUrl = skinUrl; - } - - public void setGatewayURL(String gatewayURL) { - this.gatewayUrl = gatewayURL; - } - - public String getGatewayUrl() { - return String.format("http://%s/", gatewayUrl); - } - - public String getCustomTextureURLForId(Type type, String uuid, boolean gateway) { - String server = gateway ? gatewayUrl : skinUrl; - String path = type.toString().toLowerCase() + "s"; - return String.format("http://%s/%s/%s.png", server, path, uuid); - } - - public String getCustomTextureURLForId(Type type, String uuid) { - return getCustomTextureURLForId(type, uuid, false); + public SkinServer getGatewayServer() { + return this.skinServers.get(0); } public void setEnabled(boolean enabled) { @@ -203,9 +174,9 @@ public final class HDSkinManager implements IResourceManagerReloadListener { public static PreviewTexture getPreviewTexture(ResourceLocation skinResource, GameProfile profile, Type type, ResourceLocation def, @Nullable final SkinAvailableCallback callback) { TextureManager textureManager = Minecraft.getMinecraft().getTextureManager(); - String url = INSTANCE.getCustomTextureURLForId(type, UUIDTypeAdapter.fromUUID(profile.getId()), true); + MinecraftProfileTexture url = INSTANCE.getGatewayServer().getPreviewTexture(type, profile); IImageBuffer buffer = new ImageBufferDownloadHD(); - PreviewTexture skinTexture = new PreviewTexture(url, def, type == Type.SKIN ? new IImageBuffer() { + PreviewTexture skinTexture = new PreviewTexture(url.getUrl(), def, type == Type.SKIN ? new IImageBuffer() { @Override @Nullable public BufferedImage parseUserSkin(BufferedImage image) { @@ -215,7 +186,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener { @Override public void skinAvailable() { if (callback != null) { - callback.skinAvailable(type, skinResource, new MinecraftProfileTexture(url, Maps.newHashMap())); + callback.skinAvailable(type, skinResource, new MinecraftProfileTexture(url.getUrl(), Maps.newHashMap())); } } } : null); @@ -235,7 +206,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener { .flatMap(m -> m.values().stream()) .forEach(textures::deleteTexture); INSTANCE.skinCache.clear(); - INSTANCE.profileTextures.clear(); + INSTANCE.skinServers.forEach(SkinServer::clearCache); } catch (IOException var1) { var1.printStackTrace(); } @@ -246,8 +217,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener { skinModifiers.add(modifier); } - @Nonnull - public ResourceLocation getConvertedSkin(@Nullable ResourceLocation res) { + public ResourceLocation getConvertedSkin(ResourceLocation res) { ResourceLocation loc = resources.getConvertedResource(res); return loc == null ? res : loc; } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java index 426c2e6f..a1f232dc 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java @@ -4,14 +4,13 @@ import static com.mojang.authlib.minecraft.MinecraftProfileTexture.Type.ELYTRA; import static com.mojang.authlib.minecraft.MinecraftProfileTexture.Type.SKIN; import static net.minecraft.client.renderer.GlStateManager.*; -import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.minecraft.MinecraftProfileTexture; -import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.voxelmodpack.hdskins.HDSkinManager; -import com.voxelmodpack.hdskins.upload.ThreadMultipartPostUpload; +import com.voxelmodpack.hdskins.skins.SkinUploadResponse; import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; @@ -33,6 +32,7 @@ import net.minecraft.util.Session; import net.minecraft.util.math.MathHelper; import net.minecraft.util.text.TextFormatting; import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; import org.lwjgl.util.glu.GLU; @@ -44,14 +44,13 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.DoubleBuffer; -import java.util.Locale; -import java.util.Map; +import java.nio.file.Path; +import javax.annotation.Nullable; import javax.imageio.ImageIO; import javax.swing.*; -public class GuiSkins extends GuiScreen { - private static final int MAX_SKIN_DIMENSION = 8192; - private static final String skinServerId = "7853dfddc358333843ad55a2c7485c4aa0380a51"; +public class GuiSkins extends GuiScreen implements FutureCallback { + private static final int MAX_SKIN_DIMENSION = 1024; private int updateCounter = 0; private ResourceLocation viewportTexture; private static final ResourceLocation[] cubemapTextures = { @@ -74,6 +73,7 @@ public class GuiSkins extends GuiScreen { private DoubleBuffer doubleBuffer; + @Nullable private String uploadError; private volatile String skinMessage = I18n.format("hdskins.choose"); private String skinUploadMessage = I18n.format("hdskins.request"); @@ -83,7 +83,6 @@ public class GuiSkins extends GuiScreen { private volatile boolean throttledByMojang; private int refreshCounter = -1; private ThreadOpenFilePNG openFileThread; - private ThreadMultipartPostUpload threadSkinUpload; private final Object skinLock = new Object(); private File pendingSkinFile; private File selectedSkin; @@ -606,7 +605,6 @@ public class GuiSkins extends GuiScreen { Gui.drawRect(0, 0, this.width, this.height, 0xB0000000); this.drawCenteredString(this.fontRenderer, I18n.format("hdskins.failed"), this.width / 2, this.height / 2 - 10, 0xFFFFFF55); this.drawCenteredString(this.fontRenderer, this.uploadError, this.width / 2, this.height / 2 + 2, 0xFFFF5555); - LiteLoaderLogger.warning("Upload Failed: {}", this.uploadError); } depthMask(true); @@ -666,68 +664,43 @@ public class GuiSkins extends GuiScreen { } private void clearUploadedSkin(Session session) { - if (this.registerServerConnection(session, skinServerId)) { - Map sourceData = getClearData(session); - this.uploadError = null; - this.uploadingSkin = true; - this.skinUploadMessage = I18n.format("hdskins.request"); - this.threadSkinUpload = new ThreadMultipartPostUpload(HDSkinManager.INSTANCE.getGatewayUrl(), sourceData, this::onUploadComplete); - this.threadSkinUpload.start(); - } + this.uploadingSkin = true; + this.skinUploadMessage = I18n.format("hdskins.request"); + Futures.addCallback(HDSkinManager.INSTANCE.getGatewayServer().uploadSkin(session, null, this.textureType), this); } - private void uploadSkin(Session session, File skinFile) { - if (this.registerServerConnection(session, skinServerId)) { - Map sourceData = getUploadData(session, skinFile); - this.uploadError = null; - this.uploadingSkin = true; - this.skinUploadMessage = I18n.format("hdskins.upload"); - this.threadSkinUpload = new ThreadMultipartPostUpload(HDSkinManager.INSTANCE.getGatewayUrl(), sourceData, this::onUploadComplete); - this.threadSkinUpload.start(); - } + private void uploadSkin(Session session, @Nullable File skinFile) { + this.uploadingSkin = true; + this.skinUploadMessage = I18n.format("hdskins.upload"); + Path path = skinFile == null ? null : skinFile.toPath(); + Futures.addCallback(HDSkinManager.INSTANCE.getGatewayServer().uploadSkin(session, path, this.textureType), this); } - private Map getData(Session session, String param, Object val) { - return ImmutableMap.of( - "user", session.getUsername(), - "uuid", session.getPlayerID(), - "type", this.textureType.toString().toLowerCase(Locale.US), - param, val); - } - - private Map getClearData(Session session) { - return getData(session, "clear", "1"); - } - - private Map getUploadData(Session session, File skinFile) { - return getData(session, this.textureType.toString().toLowerCase(Locale.US), skinFile); - } - - private void setUploadError(String error) { - this.uploadError = error.startsWith("ERROR: ") ? error.substring(7) : error; + private void setUploadError(@Nullable String error) { + this.uploadError = error != null && error.startsWith("ERROR: ") ? error.substring(7) : error; this.btnUpload.enabled = true; } - private void onUploadComplete(String response) { - LiteLoaderLogger.info("Upload completed with: %s", response); - this.uploadingSkin = false; - this.threadSkinUpload = null; - if (!response.equalsIgnoreCase("OK")) { - this.setUploadError(response); - } else { - this.pendingRemoteSkinRefresh = true; - } + @Override + public void onSuccess(@Nullable SkinUploadResponse result) { + if (result != null) + onUploadComplete(result); } - private boolean registerServerConnection(Session session, String serverId) { - try { - MinecraftSessionService service = Minecraft.getMinecraft().getSessionService(); - service.joinServer(session.getProfile(), session.getToken(), serverId); - return true; - } catch (AuthenticationException var4) { - this.setUploadError(var4.toString()); - var4.printStackTrace(); - return false; + @Override + public void onFailure(Throwable t) { + LogManager.getLogger().warn("Upload failed", t); + this.setUploadError(t.toString()); + this.uploadingSkin = false; + } + + private void onUploadComplete(SkinUploadResponse response) { + LiteLoaderLogger.info("Upload completed with: %s", response); + this.uploadingSkin = false; + if (!"OK".equalsIgnoreCase(response.getMessage())) { + this.setUploadError(response.getMessage()); + } else { + this.pendingRemoteSkinRefresh = true; } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/mixin/MixinPlayerInfo.java b/src/hdskins/java/com/voxelmodpack/hdskins/mixin/MixinPlayerInfo.java index ce5271c7..432c383d 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/mixin/MixinPlayerInfo.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/mixin/MixinPlayerInfo.java @@ -1,6 +1,7 @@ package com.voxelmodpack.hdskins.mixin; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.voxelmodpack.hdskins.HDSkinManager; import net.minecraft.client.network.NetworkPlayerInfo; @@ -53,16 +54,15 @@ public abstract class MixinPlayerInfo { cancellable = true, at = @At("RETURN")) private void getSkinType(CallbackInfoReturnable ci) { - HDSkinManager.INSTANCE.getProfileData(getGameProfile()) - .map(m -> m.get(Type.SKIN)) - .ifPresent(data -> { - String type = data.getMetadata("model"); - if (type == null) - type = "default"; - String type1 = type; - Optional texture = HDSkinManager.INSTANCE.getSkinLocation(getGameProfile(), Type.SKIN, false); + MinecraftProfileTexture skin = HDSkinManager.INSTANCE.getProfileData(getGameProfile()).get(Type.SKIN); + if (skin != null) { + String type = skin.getMetadata("model"); + if (type == null) + type = "default"; + String type1 = type; + Optional texture = HDSkinManager.INSTANCE.getSkinLocation(getGameProfile(), Type.SKIN, false); - texture.ifPresent((res) -> ci.setReturnValue(type1)); - }); + texture.ifPresent((res) -> ci.setReturnValue(type1)); + } } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/AbstractSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/AbstractSkinServer.java new file mode 100644 index 00000000..67c1faac --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/AbstractSkinServer.java @@ -0,0 +1,58 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public abstract class AbstractSkinServer implements SkinServer { + + private static final Logger logger = LogManager.getLogger(); + + protected static final ExecutorService skinDownloadExecutor = Executors.newCachedThreadPool(); + protected static final ListeningExecutorService skinUploadExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + + public static final MinecraftTexturesPayload EMPTY_PAYLOAD = new MinecraftTexturesPayload(); + + private LoadingCache> skins = CacheBuilder.newBuilder() + .initialCapacity(20) + .maximumSize(100) + .expireAfterWrite(4, TimeUnit.HOURS) + .build(AsyncCacheLoader.create(new CacheLoader>() { + + @Override + public Optional load(GameProfile key) { + return loadProfileData(key); + } + }, Optional.empty(), skinDownloadExecutor)); + + protected abstract Optional loadProfileData(GameProfile profile); + + @Override + public final Optional getProfileData(GameProfile profile) { + + boolean was = !skins.asMap().containsKey(profile); + Optional textures = skins.getUnchecked(profile); + // This is the initial value. Refreshing will load it syncronously. + if (was) { + skins.refresh(profile); + } + return textures; + } + + @Override + public void clearCache() { + skins.invalidateAll(); + } + +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/AsyncCacheLoader.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/AsyncCacheLoader.java new file mode 100644 index 00000000..e7547ad5 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/AsyncCacheLoader.java @@ -0,0 +1,42 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.cache.CacheLoader; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableFutureTask; + +import java.util.Map; +import java.util.concurrent.Executor; + +public class AsyncCacheLoader extends CacheLoader { + + public static AsyncCacheLoader create(CacheLoader loader, V placeholder, Executor executor) { + return new AsyncCacheLoader<>(loader, placeholder, executor); + } + + private final CacheLoader loader; + private final V placeholder; + private final Executor executor; + + private AsyncCacheLoader(CacheLoader loader, V placeholder, Executor executor) { + this.executor = executor; + this.placeholder = placeholder; + this.loader = loader; + } + + @Override + public V load(K key) { + return placeholder; + } + + @Override + public ListenableFuture reload(final K key, final V oldValue) { + ListenableFutureTask task = ListenableFutureTask.create(() -> loader.reload(key, oldValue).get()); + executor.execute(task); + return task; + } + + @Override + public Map loadAll(Iterable keys) throws Exception { + return loader.loadAll(keys); + } +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/LegacySkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/LegacySkinServer.java new file mode 100644 index 00000000..b760c91a --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/LegacySkinServer.java @@ -0,0 +1,129 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ListenableFuture; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; +import com.voxelmodpack.hdskins.upload.ThreadMultipartPostUpload; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Session; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; + +public class LegacySkinServer extends AbstractSkinServer { + + private static final String SERVER_ID = "7853dfddc358333843ad55a2c7485c4aa0380a51"; + + private static final Logger logger = LogManager.getLogger(); + + private final String address; + private final String gateway; + + public LegacySkinServer(String address, String gateway) { + this.address = address; + this.gateway = gateway; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public String getGateway() { + return gateway; + } + + @Override + public MinecraftProfileTexture getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile) { + return new MinecraftProfileTexture(getPath(getGateway(), type, profile), null); + } + + @Override + public Optional loadProfileData(GameProfile profile) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { + + String url = getPath(getAddress(), type, profile); + try { + HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + if (urlConnection.getResponseCode() / 100 != 2) { + throw new IOException("Bad response code: " + urlConnection.getResponseCode()); + } + builder.put(type, new MinecraftProfileTexture(url, null)); + logger.info("Found skin for {} at {}", profile.getName(), url); + } catch (IOException e) { + logger.debug("Couldn't find texture at {}. Does it exist?", url, e); + } + + } + + Map map = builder.build(); + if (map.isEmpty()) { + logger.debug("No textures found for {} at {}", profile, this.getAddress()); + return Optional.empty(); + } + + return Optional.of(new TexturesPayloadBuilder() + .profileId(profile.getId()) + .profileName(profile.getName()) + .timestamp(System.currentTimeMillis()) + .isPublic(true) + .textures(map) + .build()); + } + + @Override + public ListenableFuture uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type) { + + return skinUploadExecutor.submit(() -> { + verifyServerConnection(session, SERVER_ID); + + Map data = image == null ? getClearData(session, type) : getUploadData(session, type, image); + ThreadMultipartPostUpload upload = new ThreadMultipartPostUpload(getGateway(), data); + String response = upload.uploadMultipart(); + return new SkinUploadResponse(response.equalsIgnoreCase("OK"), response); + }); + } + + private Map getData(Session session, MinecraftProfileTexture.Type type, String param, Object val) { + return ImmutableMap.of( + "user", session.getUsername(), + "uuid", session.getPlayerID(), + "type", type.toString().toLowerCase(Locale.US), + param, val); + } + + private Map getClearData(Session session, MinecraftProfileTexture.Type type) { + return getData(session, type, "clear", "1"); + } + + private Map getUploadData(Session session, MinecraftProfileTexture.Type type, Path skinFile) { + return getData(session, type, type.toString().toLowerCase(Locale.US), skinFile); + } + + private String getPath(String address, MinecraftProfileTexture.Type type, GameProfile profile) { + String uuid = UUIDTypeAdapter.fromUUID(profile.getId()); + String path = type.toString().toLowerCase() + "s"; + return String.format("%s/%s/%s.png", address, path, uuid); + } + + private void verifyServerConnection(Session session, String serverId) throws AuthenticationException { + MinecraftSessionService service = Minecraft.getMinecraft().getSessionService(); + service.joinServer(session.getProfile(), session.getToken(), serverId); + } + +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinServer.java new file mode 100644 index 00000000..f0050ba3 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinServer.java @@ -0,0 +1,28 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.util.concurrent.ListenableFuture; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import net.minecraft.util.Session; + +import java.nio.file.Path; +import java.util.Optional; +import javax.annotation.Nullable; + +public interface SkinServer { + + String getAddress(); + + String getGateway(); + + Optional getProfileData(GameProfile profile); + + MinecraftProfileTexture getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile); + + ListenableFuture uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type); + + void clearCache(); + + +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinUploadResponse.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinUploadResponse.java new file mode 100644 index 00000000..af0043b5 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/SkinUploadResponse.java @@ -0,0 +1,33 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.base.MoreObjects; + +import javax.annotation.Nullable; + +public class SkinUploadResponse { + + private final boolean success; + private final String message; + + public SkinUploadResponse(boolean success, String message) { + this.success = success; + this.message = message; + } + + public boolean isSuccess() { + return success; + } + + @Nullable + public String getMessage() { + return message; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("success", success) + .add("message", message) + .toString(); + } +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/TexturesPayloadBuilder.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/TexturesPayloadBuilder.java new file mode 100644 index 00000000..d65d27f2 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/TexturesPayloadBuilder.java @@ -0,0 +1,64 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import java.util.Map; +import java.util.UUID; + +/** + * Use this to build a {@link MinecraftTexturesPayload} object. This is + * required because it has no useful constructor. This uses reflection + * via Gson to create a new instance and populate the fields. + */ +public class TexturesPayloadBuilder { + + private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + + private long timestamp; + private UUID profileId; + private String profileName; + private boolean isPublic; + private Map textures; + + public TexturesPayloadBuilder timestamp(long time) { + this.timestamp = time; + return this; + } + + public TexturesPayloadBuilder profileId(UUID uuid) { + this.profileId = uuid; + return this; + } + + public TexturesPayloadBuilder profileName(String name) { + this.profileName = name; + return this; + } + + public TexturesPayloadBuilder isPublic(boolean pub) { + this.isPublic = pub; + return this; + } + + public TexturesPayloadBuilder texture(MinecraftProfileTexture.Type type, MinecraftProfileTexture texture) { + if (textures == null) textures = Maps.newEnumMap(MinecraftProfileTexture.Type.class); + this.textures.put(type, texture); + return this; + } + + public TexturesPayloadBuilder textures(Map textures) { + this.textures = textures; + return this; + } + + public MinecraftTexturesPayload build() { + return gson.fromJson(gson.toJson(this), MinecraftTexturesPayload.class); + } + + +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/YggSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/YggSkinServer.java new file mode 100644 index 00000000..677d662a --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/YggSkinServer.java @@ -0,0 +1,45 @@ +package com.voxelmodpack.hdskins.skins; + +import com.google.common.util.concurrent.ListenableFuture; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import net.minecraft.util.Session; + +import java.nio.file.Path; +import java.util.Optional; +import javax.annotation.Nullable; + +public class YggSkinServer extends AbstractSkinServer { + + private final String baseURL; + + public YggSkinServer(String baseURL) { + this.baseURL = baseURL; + } + + @Override + protected Optional loadProfileData(GameProfile profile) { + return Optional.empty(); + } + + @Override + public String getAddress() { + return null; + } + + @Override + public String getGateway() { + return null; + } + + @Override + public MinecraftProfileTexture getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile) { + return null; + } + + @Override + public ListenableFuture uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type) { + return null; + } +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/package-info.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/package-info.java new file mode 100644 index 00000000..4232b554 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.voxelmodpack.hdskins.skins; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/upload/IUploadCompleteCallback.java b/src/hdskins/java/com/voxelmodpack/hdskins/upload/IUploadCompleteCallback.java deleted file mode 100644 index 1dc27514..00000000 --- a/src/hdskins/java/com/voxelmodpack/hdskins/upload/IUploadCompleteCallback.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.voxelmodpack.hdskins.upload; - -@FunctionalInterface -public interface IUploadCompleteCallback { - void onUploadComplete(String response); -} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/upload/ThreadMultipartPostUpload.java b/src/hdskins/java/com/voxelmodpack/hdskins/upload/ThreadMultipartPostUpload.java index e608a0fb..84edaae2 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/upload/ThreadMultipartPostUpload.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/upload/ThreadMultipartPostUpload.java @@ -1,25 +1,25 @@ package com.voxelmodpack.hdskins.upload; -import com.google.common.io.Files; +import org.apache.commons.io.IOUtils; -import javax.annotation.Nullable; -import java.io.BufferedReader; import java.io.DataOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; import java.util.Map.Entry; +import javax.annotation.Nullable; /** * Uploader for Multipart form data * * @author Adam Mummery-Smith */ -public class ThreadMultipartPostUpload extends Thread { +public class ThreadMultipartPostUpload { protected final Map sourceData; protected final String method; @@ -28,8 +28,6 @@ public class ThreadMultipartPostUpload extends Thread { protected final String urlString; - protected final IUploadCompleteCallback callback; - protected HttpURLConnection httpClient; protected static final String CRLF = "\r\n"; @@ -40,34 +38,18 @@ public class ThreadMultipartPostUpload extends Thread { public String response; - public ThreadMultipartPostUpload(String method, String url, Map sourceData, @Nullable String authorization, IUploadCompleteCallback callback) { + public ThreadMultipartPostUpload(String method, String url, Map sourceData, @Nullable String authorization) { this.method = method; this.urlString = url; this.sourceData = sourceData; this.authorization = authorization; - this.callback = callback; } - public ThreadMultipartPostUpload(String url, Map sourceData, IUploadCompleteCallback callback) { - this("POST", url, sourceData, null, callback); + public ThreadMultipartPostUpload(String url, Map sourceData) { + this("POST", url, sourceData, null); } - public String getResponse() { - return this.response == null ? "" : this.response.trim(); - } - - @Override - public void run() { - try { - this.uploadMultipart(); - } catch (IOException ex) { - ex.printStackTrace(); - } - - this.callback.onUploadComplete(this.getResponse()); - } - - protected void uploadMultipart() throws IOException { + public String uploadMultipart() throws IOException { // open a URL connection URL url = new URL(this.urlString); @@ -88,49 +70,33 @@ public class ThreadMultipartPostUpload extends Thread { this.httpClient.addRequestProperty("Authorization", this.authorization); } - DataOutputStream outputStream = new DataOutputStream(this.httpClient.getOutputStream()); + try (DataOutputStream outputStream = new DataOutputStream(this.httpClient.getOutputStream())) { - for (Entry data : this.sourceData.entrySet()) { - outputStream.writeBytes(twoHyphens + boundary + CRLF); + for (Entry data : this.sourceData.entrySet()) { + outputStream.writeBytes(twoHyphens + boundary + CRLF); - String paramName = data.getKey(); - Object paramData = data.getValue(); + String paramName = data.getKey(); + Object paramData = data.getValue(); - if (paramData instanceof File) { - File uploadFile = (File) paramData; - outputStream.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"; filename=\"" + uploadFile.getName() + "\"" + CRLF + CRLF); + if (paramData instanceof Path) { + Path uploadPath = (Path) paramData; + outputStream.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"; filename=\"" + uploadPath.getFileName() + "\"" + CRLF + CRLF); - Files.asByteSource(uploadFile).copyTo(outputStream); + Files.copy(uploadPath, outputStream); + } else { + outputStream.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"" + CRLF + CRLF); + outputStream.writeBytes(paramData.toString()); + } - } else { - outputStream.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"" + CRLF + CRLF); - outputStream.writeBytes(paramData.toString()); + outputStream.writeBytes(ThreadMultipartPostUpload.CRLF); } - outputStream.writeBytes(ThreadMultipartPostUpload.CRLF); + outputStream.writeBytes(twoHyphens + boundary + twoHyphens + CRLF); } - outputStream.writeBytes(twoHyphens + boundary + twoHyphens + CRLF); - outputStream.flush(); - - InputStream httpStream = this.httpClient.getInputStream(); - - try { - StringBuilder readString = new StringBuilder(); - BufferedReader reader = new BufferedReader(new InputStreamReader(httpStream)); - - String readLine; - while ((readLine = reader.readLine()) != null) { - readString.append(readLine).append("\n"); - } - - reader.close(); - this.response = readString.toString(); - } catch (IOException ex) { - ex.printStackTrace(); + try (InputStream input = this.httpClient.getInputStream()) { + return IOUtils.toString(input, StandardCharsets.UTF_8); } - - outputStream.close(); } } diff --git a/src/main/java/com/minelittlepony/MineLittlePony.java b/src/main/java/com/minelittlepony/MineLittlePony.java index 7a3f34d3..35118e62 100644 --- a/src/main/java/com/minelittlepony/MineLittlePony.java +++ b/src/main/java/com/minelittlepony/MineLittlePony.java @@ -18,6 +18,7 @@ import com.mumfrey.liteloader.core.LiteLoader; import com.mumfrey.liteloader.util.ModUtilities; import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.gui.GuiSkins; +import com.voxelmodpack.hdskins.skins.LegacySkinServer; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.entity.Render; import net.minecraft.client.renderer.entity.RenderManager; @@ -89,6 +90,9 @@ public class MineLittlePony { manager.addSkinModifier(new PonySkinModifier()); // logger.info("Set MineLP skin server URL."); + // This also makes it the default gateway server. + manager.addSkinServer(new LegacySkinServer("http://minelpskins.voxelmodpack.com", "http://minelpskinmanager.voxelmodpack.com")); + RenderManager rm = minecraft.getRenderManager(); this.saveCurrentRenderers(rm); ModUtilities.addRenderer(EntityPonyModel.class, new RenderPonyModel(rm));