From 3b1f34642689081fd0cbe7008821ccd0b7537290 Mon Sep 17 00:00:00 2001 From: Sollace Date: Mon, 25 Mar 2019 17:33:58 +0200 Subject: [PATCH] Revert "Cleanup sourcesets. hdskins is not here." This reverts commit 2f9d69b386f21c98e681257b7699cdac0963eec2. --- build.gradle | 29 +- .../mcp/MethodsReturnNonnullByDefault.java | 26 + .../client/ForgeHooksClient.java | 20 + .../minelittlepony/hdskins/HDSkinManager.java | 343 ++++++++++ .../com/minelittlepony/hdskins/HDSkins.java | 68 ++ .../hdskins/ISkinCacheClearListener.java | 6 + .../minelittlepony/hdskins/ISkinModifier.java | 17 + .../minelittlepony/hdskins/ISkinParser.java | 25 + .../minelittlepony/hdskins/SkinChooser.java | 127 ++++ .../minelittlepony/hdskins/SkinUploader.java | 291 +++++++++ .../minelittlepony/hdskins/VanillaModels.java | 22 + .../hdskins/ducks/INetworkPlayerInfo.java | 6 + .../hdskins/ducks/package-info.java | 7 + .../minelittlepony/hdskins/gui/CubeMap.java | 231 +++++++ .../hdskins/gui/DummyWorld.java | 98 +++ .../hdskins/gui/EntityPlayerModel.java | 190 ++++++ .../minelittlepony/hdskins/gui/Feature.java | 50 ++ .../minelittlepony/hdskins/gui/GuiSkins.java | 595 ++++++++++++++++++ .../hdskins/gui/HDSkinsConfigPanel.java | 33 + .../hdskins/gui/RenderPlayerModel.java | 219 +++++++ .../hdskins/gui/package-info.java | 7 + .../hdskins/mixin/MixinGuiMainMenu.java | 24 + .../mixin/MixinImageBufferDownload.java | 30 + .../hdskins/mixin/MixinMinecraft.java | 26 + .../hdskins/mixin/MixinNetworkPlayerInfo.java | 117 ++++ .../hdskins/mixin/MixinSkullRenderer.java | 37 ++ .../mixin/MixinThreadDownloadImageData.java | 17 + .../hdskins/mixin/package-info.java | 7 + .../minelittlepony/hdskins/package-info.java | 7 + .../hdskins/resources/AsyncCacheLoader.java | 42 ++ .../hdskins/resources/ImageLoader.java | 69 ++ .../hdskins/resources/LocalTexture.java | 133 ++++ .../hdskins/resources/PreviewTexture.java | 47 ++ .../resources/PreviewTextureManager.java | 47 ++ .../hdskins/resources/SkinData.java | 24 + .../resources/SkinResourceManager.java | 131 ++++ .../hdskins/resources/TextureLoader.java | 14 + .../hdskins/resources/package-info.java | 7 + .../texture/DynamicTextureImage.java | 26 + .../resources/texture/IBufferedTexture.java | 11 + .../texture/ISkinAvailableCallback.java | 12 + .../texture/ImageBufferDownloadHD.java | 89 +++ .../resources/texture/SimpleDrawer.java | 41 ++ .../resources/texture/package-info.java | 7 + .../hdskins/server/BethlehemSkinServer.java | 101 +++ .../hdskins/server/LegacySkinServer.java | 188 ++++++ .../hdskins/server/ServerType.java | 21 + .../hdskins/server/SkinServer.java | 99 +++ .../hdskins/server/SkinServerSerializer.java | 37 ++ .../hdskins/server/SkinUpload.java | 49 ++ .../hdskins/server/SkinUploadResponse.java | 23 + .../hdskins/server/ValhallaSkinServer.java | 216 +++++++ .../hdskins/server/package-info.java | 7 + .../hdskins/upload/FileDropListener.java | 48 ++ .../hdskins/upload/FileDropper.java | 78 +++ .../hdskins/upload/GLWindow.java | 291 +++++++++ .../hdskins/upload/IFileCallback.java | 8 + .../hdskins/upload/IFileDialog.java | 5 + .../hdskins/upload/InternalDialog.java | 38 ++ .../hdskins/upload/ThreadOpenFile.java | 76 +++ .../hdskins/upload/ThreadOpenFilePNG.java | 33 + .../hdskins/upload/ThreadSaveFile.java | 47 ++ .../hdskins/upload/ThreadSaveFilePNG.java | 34 + .../hdskins/upload/package-info.java | 7 + .../hdskins/util/CallableFutures.java | 56 ++ .../com/minelittlepony/hdskins/util/Edge.java | 32 + .../hdskins/util/IndentedToStringStyle.java | 35 ++ .../hdskins/util/MoreHttpResponses.java | 111 ++++ .../hdskins/util/NetClient.java | 81 +++ .../hdskins/util/PlayerUtil.java | 24 + .../hdskins/util/ProfileTextureUtil.java | 39 ++ .../hdskins/util/TexturesPayloadBuilder.java | 50 ++ .../hdskins/util/package-info.java | 7 + .../resources/assets/hdskins/lang/de_de.json | 41 ++ .../resources/assets/hdskins/lang/en_us.json | 41 ++ .../resources/assets/hdskins/lang/fr_fr.json | 38 ++ .../resources/assets/hdskins/lang/ru_ru.json | 41 ++ .../hdskins/textures/cubemaps/cubemap0_0.png | Bin 0 -> 11342 bytes .../hdskins/textures/cubemaps/cubemap0_1.png | Bin 0 -> 12143 bytes .../hdskins/textures/cubemaps/cubemap0_2.png | Bin 0 -> 11219 bytes .../hdskins/textures/cubemaps/cubemap0_3.png | Bin 0 -> 10714 bytes .../hdskins/textures/cubemaps/cubemap0_4.png | Bin 0 -> 10534 bytes .../hdskins/textures/cubemaps/cubemap0_5.png | Bin 0 -> 9282 bytes .../assets/hdskins/textures/mob/noskin.png | Bin 0 -> 1870 bytes src/hdskins/resources/hdskins.mixin.json | 14 + .../hdskins/litemod/LiteModHDSkins.java | 105 ++++ .../hdskins/litemod/package-info.java | 7 + 87 files changed, 5502 insertions(+), 1 deletion(-) create mode 100644 src/api/java/mcp/MethodsReturnNonnullByDefault.java create mode 100644 src/api/java/net/minecraftforge/client/ForgeHooksClient.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/HDSkinManager.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/HDSkins.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/ISkinCacheClearListener.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/ISkinModifier.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/ISkinParser.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/SkinChooser.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/SkinUploader.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/VanillaModels.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/ducks/INetworkPlayerInfo.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/ducks/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/CubeMap.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/DummyWorld.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/EntityPlayerModel.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/Feature.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/GuiSkins.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/HDSkinsConfigPanel.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/RenderPlayerModel.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/gui/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinGuiMainMenu.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinImageBufferDownload.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinMinecraft.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinNetworkPlayerInfo.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinSkullRenderer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinThreadDownloadImageData.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/mixin/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/AsyncCacheLoader.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/ImageLoader.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/LocalTexture.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTexture.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTextureManager.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/SkinData.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/SkinResourceManager.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/TextureLoader.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/DynamicTextureImage.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/IBufferedTexture.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ISkinAvailableCallback.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ImageBufferDownloadHD.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/SimpleDrawer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/resources/texture/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/BethlehemSkinServer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/LegacySkinServer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/ServerType.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/SkinServer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/SkinServerSerializer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/SkinUpload.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/SkinUploadResponse.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/ValhallaSkinServer.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/server/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropListener.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropper.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/GLWindow.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/IFileCallback.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/IFileDialog.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/InternalDialog.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFile.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFilePNG.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFile.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFilePNG.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/upload/package-info.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/CallableFutures.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/Edge.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/IndentedToStringStyle.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/MoreHttpResponses.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/NetClient.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/PlayerUtil.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/ProfileTextureUtil.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/TexturesPayloadBuilder.java create mode 100644 src/hdskins/java/com/minelittlepony/hdskins/util/package-info.java create mode 100644 src/hdskins/resources/assets/hdskins/lang/de_de.json create mode 100644 src/hdskins/resources/assets/hdskins/lang/en_us.json create mode 100644 src/hdskins/resources/assets/hdskins/lang/fr_fr.json create mode 100644 src/hdskins/resources/assets/hdskins/lang/ru_ru.json create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_0.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_1.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_2.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_3.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_4.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_5.png create mode 100644 src/hdskins/resources/assets/hdskins/textures/mob/noskin.png create mode 100644 src/hdskins/resources/hdskins.mixin.json create mode 100644 src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/LiteModHDSkins.java create mode 100644 src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/package-info.java diff --git a/build.gradle b/build.gradle index a02c8f9c..e14c9af0 100644 --- a/build.gradle +++ b/build.gradle @@ -59,11 +59,19 @@ sourceSets { compileClasspath += main.compileClasspath } + hdskins { + // HDSkins. + // TODO: Move to a separate project + compileClasspath += main.compileClasspath + compileClasspath += common.output + ext.refMap = 'hdskins.mixin.refmap.json' + } client { // Client-only code compileClasspath += main.compileClasspath compileClasspath += main.output compileClasspath += common.output + compileClasspath += hdskins.output ext.refMap = 'minelp.mixin.refmap.json' } main { @@ -79,13 +87,24 @@ sourceSets { compileClasspath += main.output compileClasspath += client.output } - + + hdskinslitemod { + compileClasspath += main.compileClasspath + compileClasspath += litemod.output + compileClasspath += hdskins.output + } + fml { compileClasspath += main.compileClasspath compileClasspath += main.output compileClasspath += client.output } + hdskinsfml { + compileClasspath += main.compileClasspath + compileClasspath += litemod.output + compileClasspath += hdskins.output + } } minecraft { @@ -103,6 +122,7 @@ minecraft { mods { minelittlepony { source sourceSets.common + source sourceSets.hdskins source sourceSets.client source sourceSets.main @@ -127,6 +147,10 @@ repositories { dependencies { minecraft 'net.minecraftforge:forge:1.13.2-25.0.90' + // use the same version as httpclient + compile('org.apache.httpcomponents:httpmime:4.3.2') { + transitive = false + } compile('org.spongepowered:mixin:0.7.11-SNAPSHOT') { transitive = false } @@ -140,6 +164,9 @@ jar { from sourceSets.common.output from sourceSets.main.output + from sourceSets.hdskins.output + from sourceSets.hdskinsfml.output + from sourceSets.client.output from sourceSets.fml.output diff --git a/src/api/java/mcp/MethodsReturnNonnullByDefault.java b/src/api/java/mcp/MethodsReturnNonnullByDefault.java new file mode 100644 index 00000000..fb02ffca --- /dev/null +++ b/src/api/java/mcp/MethodsReturnNonnullByDefault.java @@ -0,0 +1,26 @@ +package mcp; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation can be applied to a package, class or method to indicate that + * the method in that element are nonnull by default unless there is: + * + * + */ +@Documented +@Nonnull +@TypeQualifierDefault(ElementType.METHOD) // Note: This is a copy of javax.annotation.ParametersAreNonnullByDefault with target changed to METHOD +@Retention(RetentionPolicy.RUNTIME) +public @interface MethodsReturnNonnullByDefault {} diff --git a/src/api/java/net/minecraftforge/client/ForgeHooksClient.java b/src/api/java/net/minecraftforge/client/ForgeHooksClient.java new file mode 100644 index 00000000..c5baa429 --- /dev/null +++ b/src/api/java/net/minecraftforge/client/ForgeHooksClient.java @@ -0,0 +1,20 @@ +package net.minecraftforge.client; + +import net.minecraft.client.renderer.entity.model.ModelBiped; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.inventory.EntityEquipmentSlot; +import net.minecraft.item.ItemStack; + +// stub +public class ForgeHooksClient { + + public static String getArmorTexture(Entity entity, ItemStack armor, String def, EntityEquipmentSlot slot, String type) { + return def; + } + + public static ModelBiped getArmorModel(EntityLivingBase entity, ItemStack item, EntityEquipmentSlot slot, ModelBiped def) { + return def; + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/HDSkinManager.java b/src/hdskins/java/com/minelittlepony/hdskins/HDSkinManager.java new file mode 100644 index 00000000..63abbd64 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/HDSkinManager.java @@ -0,0 +1,343 @@ +package com.minelittlepony.hdskins; + +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Streams; +import com.minelittlepony.common.util.MoreStreams; +import com.minelittlepony.hdskins.ducks.INetworkPlayerInfo; +import com.minelittlepony.hdskins.gui.GuiSkins; +import com.minelittlepony.hdskins.resources.SkinResourceManager; +import com.minelittlepony.hdskins.resources.TextureLoader; +import com.minelittlepony.hdskins.resources.texture.ImageBufferDownloadHD; +import com.minelittlepony.hdskins.server.BethlehemSkinServer; +import com.minelittlepony.hdskins.server.LegacySkinServer; +import com.minelittlepony.hdskins.server.ServerType; +import com.minelittlepony.hdskins.server.SkinServer; +import com.minelittlepony.hdskins.server.ValhallaSkinServer; +import com.minelittlepony.hdskins.util.CallableFutures; +import com.minelittlepony.hdskins.util.PlayerUtil; +import com.minelittlepony.hdskins.util.ProfileTextureUtil; +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 net.minecraft.client.Minecraft; +import net.minecraft.client.entity.AbstractClientPlayer; +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.client.renderer.texture.ThreadDownloadImageData; +import net.minecraft.client.renderer.texture.ITextureObject; +import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.resources.IResourceManager; +import net.minecraft.resources.IResourceManagerReloadListener; +import net.minecraft.client.resources.SkinManager; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.FileUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +public final class HDSkinManager implements IResourceManagerReloadListener { + + private static final Logger logger = LogManager.getLogger(); + + public static final ExecutorService skinUploadExecutor = Executors.newSingleThreadExecutor(); + public static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8); + public static final CloseableHttpClient httpClient = HttpClients.createSystem(); + + public static final HDSkinManager INSTANCE = new HDSkinManager(); + + private List clearListeners = Lists.newArrayList(); + + private BiMap> skinServerTypes = HashBiMap.create(2); + private List skinServers = Lists.newArrayList(); + + private LoadingCache>> skins = CacheBuilder.newBuilder() + .expireAfterAccess(15, TimeUnit.SECONDS) + .build(CacheLoader.from(this::loadProfileData)); + + private List skinModifiers = Lists.newArrayList(); + private List skinParsers = Lists.newArrayList(); + + private SkinResourceManager resources = new SkinResourceManager(); + + private Function, GuiSkins> skinsGuiFunc = GuiSkins::new; + + private HDSkinManager() { + + // register default skin server types + addSkinServerType(LegacySkinServer.class); + addSkinServerType(ValhallaSkinServer.class); + addSkinServerType(BethlehemSkinServer.class); + } + + public void setSkinsGui(Function, GuiSkins> skinsGuiFunc) { + Preconditions.checkNotNull(skinsGuiFunc, "skinsGuiFunc"); + this.skinsGuiFunc = skinsGuiFunc; + } + + public GuiSkins createSkinsGui() { + return skinsGuiFunc.apply(ImmutableList.copyOf(this.skinServers)); + } + + private CompletableFuture> loadProfileData(GameProfile profile) { + return CompletableFuture.supplyAsync(() -> { + if (profile.getId() == null) { + return Collections.emptyMap(); + } + + Map textureMap = Maps.newEnumMap(Type.class); + + for (SkinServer server : skinServers) { + try { + server.loadProfileData(profile).getTextures().forEach(textureMap::putIfAbsent); + if (textureMap.size() == Type.values().length) { + break; + } + } catch (IOException e) { + logger.trace(e); + } + + } + return textureMap; + }, skinDownloadExecutor); + } + + public CompletableFuture> loadProfileTextures(GameProfile profile) { + try { + // try to recreate a broken gameprofile + // happens when server sends a random profile with skin and displayname + Property textures = Iterables.getFirst(profile.getProperties().get("textures"), null); + + if (textures != null) { + String json = new String(Base64.getDecoder().decode(textures.getValue()), StandardCharsets.UTF_8); + + MinecraftTexturesPayload texturePayload = SkinServer.gson.fromJson(json, MinecraftTexturesPayload.class); + + if (texturePayload != null) { + + String name = texturePayload.getProfileName(); // name is optional + UUID uuid = texturePayload.getProfileId(); + + if (uuid != null) { + profile = new GameProfile(uuid, name); // uuid is required + } + + // probably uses this texture for a reason. Don't mess with it. + if (!texturePayload.getTextures().isEmpty() && texturePayload.getProfileId() == null) { + return CompletableFuture.completedFuture(Collections.emptyMap()); + } + } + } + } catch (Exception e) { + if (profile.getId() == null) { // Something broke server-side probably + logger.warn("{} had a null UUID and was unable to recreate it from texture profile.", profile.getName(), e); + return CompletableFuture.completedFuture(Collections.emptyMap()); + } + } + return skins.getUnchecked(profile); + } + + public void fetchAndLoadSkins(GameProfile profile, SkinManager.SkinAvailableCallback callback) { + loadProfileTextures(profile).thenAcceptAsync(m -> m.forEach((type, pp) -> { + loadTexture(type, pp, (typeIn, location, profileTexture) -> { + parseSkin(profile, typeIn, location, profileTexture) + .thenRun(() -> callback.onSkinTextureAvailable(typeIn, location, profileTexture)); + }); + }), Minecraft.getInstance()::addScheduledTask); + } + + public ResourceLocation loadTexture(Type type, MinecraftProfileTexture texture, @Nullable SkinManager.SkinAvailableCallback callback) { + String skinDir = type.toString().toLowerCase() + "s/"; + + final ResourceLocation resource = new ResourceLocation("hdskins", skinDir + texture.getHash()); + ITextureObject texObj = Minecraft.getInstance().getTextureManager().getTexture(resource); + + //noinspection ConstantConditions + if (texObj != null) { + if (callback != null) { + callback.onSkinTextureAvailable(type, resource, texture); + } + } else { + + // schedule texture loading on the main thread. + TextureLoader.loadTexture(resource, new ThreadDownloadImageData( + new File(HDSkins.getInstance().getAssetsDirectory(), "hd/" + skinDir + texture.getHash().substring(0, 2) + "/" + texture.getHash()), + texture.getUrl(), + DefaultPlayerSkin.getDefaultSkinLegacy(), + new ImageBufferDownloadHD(type, () -> { + if (callback != null) { + callback.onSkinTextureAvailable(type, resource, texture); + } + }))); + } + + return resource; + } + + public Map getTextures(GameProfile profile) { + Map map = new HashMap<>(); + + for (Map.Entry e : loadProfileTextures(profile).getNow(Collections.emptyMap()).entrySet()) { + map.put(e.getKey(), loadTexture(e.getKey(), e.getValue(), null)); + } + + return map; + } + + private void addSkinServerType(Class type) { + Preconditions.checkArgument(!type.isInterface(), "type cannot be an interface"); + Preconditions.checkArgument(!Modifier.isAbstract(type.getModifiers()), "type cannot be abstract"); + + ServerType st = type.getAnnotation(ServerType.class); + + if (st == null) { + throw new IllegalArgumentException("class is not annotated with @ServerType"); + } + + this.skinServerTypes.put(st.value(), type); + } + + public Class getSkinServerClass(String type) { + return this.skinServerTypes.get(type); + } + + void addSkinServer(SkinServer skinServer) { + this.skinServers.add(skinServer); + } + + public void addClearListener(ISkinCacheClearListener listener) { + clearListeners.add(listener); + } + + public void clearSkinCache() { + logger.info("Clearing local player skin cache"); + + FileUtils.deleteQuietly(new File(HDSkins.getInstance().getAssetsDirectory(), "hd")); + + skins.invalidateAll(); + parseSkins(); + clearListeners.removeIf(this::onSkinCacheCleared); + } + + private boolean onSkinCacheCleared(ISkinCacheClearListener callback) { + try { + return !callback.onSkinCacheCleared(); + } catch (Exception e) { + logger.warn("Exception encountered calling skin listener '{}'. It will be removed.", callback.getClass().getName(), e); + return true; + } + } + + public void addSkinModifier(ISkinModifier modifier) { + skinModifiers.add(modifier); + } + + public void addSkinParser(ISkinParser parser) { + skinParsers.add(parser); + } + + public ResourceLocation getConvertedSkin(ResourceLocation res) { + ResourceLocation loc = resources.getConvertedResource(res); + return loc == null ? res : loc; + } + + public void convertSkin(ISkinModifier.IDrawer drawer) { + for (ISkinModifier skin : skinModifiers) { + skin.convertSkin(drawer); + } + } + + public void parseSkins() { + Minecraft mc = Minecraft.getInstance(); + + Streams.concat(getNPCs(mc), getPlayers(mc)) + + // filter nulls + .filter(Objects::nonNull) + .map(INetworkPlayerInfo.class::cast) + .distinct() + + // and clear skins + .forEach(INetworkPlayerInfo::reloadTextures); + + } + + private Stream getNPCs(Minecraft mc) { + return MoreStreams.ofNullable(mc.world) + .flatMap(w -> w.playerEntities.stream()) + .filter(AbstractClientPlayer.class::isInstance) + .map(AbstractClientPlayer.class::cast) + .map(PlayerUtil::getInfo); + } + + private Stream getPlayers(Minecraft mc) { + return MoreStreams.ofNullable(mc.getConnection()) + .flatMap(a -> a.getPlayerInfoMap().stream()); + } + + public CompletableFuture parseSkin(GameProfile profile, Type type, ResourceLocation resource, MinecraftProfileTexture texture) { + + return CallableFutures.scheduleTask(() -> { + + // grab the metadata object via reflection. Object is live. + Map metadata = ProfileTextureUtil.getMetadata(texture); + + boolean wasNull = metadata == null; + + if (wasNull) { + metadata = new HashMap<>(); + } else if (metadata.containsKey("model")) { + // try to reset the model. + metadata.put("model", VanillaModels.of(metadata.get("model"))); + } + + for (ISkinParser parser : skinParsers) { + try { + parser.parse(profile, type, resource, metadata); + } catch (Throwable t) { + logger.error("Exception thrown while parsing skin: ", t); + } + } + + if (wasNull && !metadata.isEmpty()) { + ProfileTextureUtil.setMetadata(texture, metadata); + } + + }); + } + + @Override + public void onResourceManagerReload(IResourceManager resourceManager) { + this.resources.onResourceManagerReload(resourceManager); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/HDSkins.java b/src/hdskins/java/com/minelittlepony/hdskins/HDSkins.java new file mode 100644 index 00000000..7a553a3b --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/HDSkins.java @@ -0,0 +1,68 @@ +package com.minelittlepony.hdskins; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.resources.IReloadableResourceManager; +import net.minecraft.entity.Entity; + +import com.google.gson.annotations.Expose; +import com.minelittlepony.hdskins.gui.EntityPlayerModel; +import com.minelittlepony.hdskins.gui.RenderPlayerModel; +import com.minelittlepony.hdskins.server.SkinServer; +import com.minelittlepony.hdskins.upload.GLWindow; + +import java.io.File; +import java.util.List; +import java.util.function.Function; + +public abstract class HDSkins { + public static final String MOD_NAME = "HD Skins"; + public static final String VERSION = "4.0.0"; + + private static HDSkins instance; + + public static HDSkins getInstance() { + return instance; + } + + public HDSkins() { + instance = this; + } + + @Expose + public List skin_servers = SkinServer.defaultServers; + + @Expose + public boolean experimentalSkinDrop = false; + + @Expose + public String lastChosenFile = ""; + + public void init() { + IReloadableResourceManager irrm = (IReloadableResourceManager) Minecraft.getInstance().getResourceManager(); + irrm.addReloadListener(HDSkinManager.INSTANCE); + } + + public abstract File getAssetsDirectory(); + + public abstract void saveConfig(); + + protected abstract void addRenderer(Class type, Function> renderer); + + public void initComplete() { + addRenderer(EntityPlayerModel.class, RenderPlayerModel::new); + + // register skin servers. + skin_servers.forEach(HDSkinManager.INSTANCE::addSkinServer); + + if (experimentalSkinDrop) { + GLWindow.create(); + } + } + + public void onToggledFullScreen(boolean fullScreen) { + GLWindow.current().refresh(fullScreen); + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/ISkinCacheClearListener.java b/src/hdskins/java/com/minelittlepony/hdskins/ISkinCacheClearListener.java new file mode 100644 index 00000000..f8f2f36d --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/ISkinCacheClearListener.java @@ -0,0 +1,6 @@ +package com.minelittlepony.hdskins; + +@FunctionalInterface +public interface ISkinCacheClearListener { + boolean onSkinCacheCleared(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/ISkinModifier.java b/src/hdskins/java/com/minelittlepony/hdskins/ISkinModifier.java new file mode 100644 index 00000000..6bafb81f --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/ISkinModifier.java @@ -0,0 +1,17 @@ +package com.minelittlepony.hdskins; + +import net.minecraft.client.renderer.texture.NativeImage; + +@FunctionalInterface +public interface ISkinModifier { + + void convertSkin(IDrawer drawer); + + interface IDrawer { + NativeImage getImage(); + + void draw(int scale, + /*destination: */ int dx1, int dy1, int dx2, int dy2, + /*source: */ int sx1, int sy1, int sx2, int sy2); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/ISkinParser.java b/src/hdskins/java/com/minelittlepony/hdskins/ISkinParser.java new file mode 100644 index 00000000..06825979 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/ISkinParser.java @@ -0,0 +1,25 @@ +package com.minelittlepony.hdskins; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; +import net.minecraft.util.ResourceLocation; + +import java.util.Map; + +/** + * SkinParser is used to parse metadata (e.g. trigger pixels) from a texture. + */ +@FunctionalInterface +public interface ISkinParser { + + /** + * Parses the texture for metadata. Any discovered data should be put into + * the metadata Map parameter. + * + * @param profile The profile whose skin is being parsed. + * @param type The texture type + * @param resource The texture location + * @param metadata The metadata previously parsed + */ + void parse(GameProfile profile, Type type, ResourceLocation resource, Map metadata); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/SkinChooser.java b/src/hdskins/java/com/minelittlepony/hdskins/SkinChooser.java new file mode 100644 index 00000000..db3ce94f --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/SkinChooser.java @@ -0,0 +1,127 @@ +package com.minelittlepony.hdskins; + +import com.minelittlepony.hdskins.upload.IFileDialog; +import com.minelittlepony.hdskins.upload.ThreadOpenFilePNG; +import com.minelittlepony.hdskins.upload.ThreadSaveFilePNG; +import com.minelittlepony.hdskins.util.MoreHttpResponses; + +import net.minecraft.client.Minecraft; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import javax.swing.UIManager; + +public class SkinChooser { + + public static final int MAX_SKIN_DIMENSION = 1024; + + public static final String ERR_UNREADABLE = "hdskins.error.unreadable"; + public static final String ERR_EXT = "hdskins.error.ext"; + public static final String ERR_OPEN = "hdskins.error.open"; + public static final String ERR_INVALID = "hdskins.error.invalid"; + + public static final String MSG_CHOOSE = "hdskins.choose"; + + static { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static boolean isPowerOfTwo(int number) { + return number != 0 && (number & number - 1) == 0; + } + + private IFileDialog openFileThread; + + private final SkinUploader uploader; + + + private volatile String status = MSG_CHOOSE; + + + public SkinChooser(SkinUploader uploader) { + this.uploader = uploader; + } + + public boolean pickingInProgress() { + return openFileThread != null; + } + + public String getStatus() { + return status; + } + + public void openBrowsePNG(Minecraft mc, String title) { + openFileThread = new ThreadOpenFilePNG(mc, title, (file, dialogResult) -> { + openFileThread = null; + if (dialogResult == 0) { + selectFile(file); + } + }); + openFileThread.start(); + } + + public void openSavePNG(Minecraft mc, String title) { + openFileThread = new ThreadSaveFilePNG(mc, title, mc.getSession().getUsername() + ".png", (file, dialogResult) -> { + if (dialogResult == 0) { + try (MoreHttpResponses response = uploader.downloadSkin().get()) { + if (response.ok()) { + FileUtils.copyInputStreamToFile(response.getInputStream(), file); + } + } catch (IOException | InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + openFileThread = null; + }); + openFileThread.start(); + } + + public void selectFile(File skinFile) { + status = evaluateAndSelect(skinFile); + } + + @Nullable + private String evaluateAndSelect(File skinFile) { + if (!skinFile.exists()) { + return ERR_UNREADABLE; + } + + if (!FilenameUtils.isExtension(skinFile.getName(), new String[]{"png", "PNG"})) { + return ERR_EXT; + } + + try { + BufferedImage chosenImage = ImageIO.read(skinFile); + + if (chosenImage == null) { + return ERR_OPEN; + } + + if (!acceptsSkinDimensions(chosenImage.getWidth(), chosenImage.getHeight())) { + return ERR_INVALID; + } + + uploader.setLocalSkin(skinFile); + + return null; + } catch (IOException e) { + e.printStackTrace(); + } + + return ERR_OPEN; + } + + protected boolean acceptsSkinDimensions(int w, int h) { + return isPowerOfTwo(w) && w == h * 2 || w == h && w <= MAX_SKIN_DIMENSION && h <= MAX_SKIN_DIMENSION; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/SkinUploader.java b/src/hdskins/java/com/minelittlepony/hdskins/SkinUploader.java new file mode 100644 index 00000000..8548f920 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/SkinUploader.java @@ -0,0 +1,291 @@ +package com.minelittlepony.hdskins; + +import net.minecraft.client.Minecraft; +import net.minecraft.init.Items; +import net.minecraft.inventory.EntityEquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.minelittlepony.hdskins.gui.EntityPlayerModel; +import com.minelittlepony.hdskins.gui.Feature; +import com.minelittlepony.hdskins.resources.PreviewTextureManager; +import com.minelittlepony.hdskins.server.SkinServer; +import com.minelittlepony.hdskins.server.SkinUpload; +import com.minelittlepony.hdskins.util.MoreHttpResponses; +import com.minelittlepony.hdskins.util.NetClient; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +public class SkinUploader implements Closeable { + + private static final Logger logger = LogManager.getLogger(); + + private final Iterator skinServers; + + public static final String ERR_NO_SERVER = "hdskins.error.noserver"; + public static final String ERR_OFFLINE = "hdskins.error.offline"; + + public static final String ERR_MOJANG = "hdskins.error.mojang"; + public static final String ERR_WAIT = "hdskins.error.mojang.wait"; + + public static final String STATUS_FETCH = "hdskins.fetch"; + + private SkinServer gateway; + + private String status; + + private Type skinType; + + private Map skinMetadata = new HashMap(); + + private volatile boolean fetchingSkin = false; + private volatile boolean throttlingNeck = false; + private volatile boolean offline = false; + + private volatile boolean sendingSkin = false; + + private int reloadCounter = 0; + private int retries = 1; + + private final EntityPlayerModel remotePlayer; + private final EntityPlayerModel localPlayer; + + private final Object skinLock = new Object(); + + private File pendingLocalSkin; + private File localSkin; + + private final ISkinUploadHandler listener; + + private final Minecraft mc = Minecraft.getInstance(); + + private static Iterator cycle(List list, Predicate filter) { + return Iterables.cycle(Iterables.filter(list, filter::test)).iterator(); + } + + public SkinUploader(List servers, EntityPlayerModel local, EntityPlayerModel remote, ISkinUploadHandler listener) { + + localPlayer = local; + remotePlayer = remote; + + skinType = Type.SKIN; + skinMetadata.put("model", "default"); + + this.listener = listener; + skinServers = cycle(servers, SkinServer::verifyGateway); + cycleGateway(); + } + + public void cycleGateway() { + if (skinServers.hasNext()) { + gateway = skinServers.next(); + fetchRemote(); + } else { + setError(ERR_NO_SERVER); + } + } + + public String getGateway() { + return gateway == null ? "" : gateway.toString(); + } + + public boolean supportsFeature(Feature feature) { + return gateway != null && gateway.supportsFeature(feature); + } + + protected void setError(String er) { + status = er; + sendingSkin = false; + } + + public void setSkinType(Type type) { + skinType = type; + + ItemStack stack = type == Type.ELYTRA ? new ItemStack(Items.ELYTRA) : ItemStack.EMPTY; + // put on or take off the elytra + localPlayer.setItemStackToSlot(EntityEquipmentSlot.CHEST, stack); + remotePlayer.setItemStackToSlot(EntityEquipmentSlot.CHEST, stack); + + listener.onSkinTypeChanged(type); + } + + public boolean uploadInProgress() { + return sendingSkin; + } + + public boolean downloadInProgress() { + return fetchingSkin; + } + + public boolean isThrottled() { + return throttlingNeck; + } + + public boolean isOffline() { + return offline; + } + + public int getRetries() { + return retries; + } + + public boolean canUpload() { + return !isOffline() && !hasStatus() && !uploadInProgress() && pendingLocalSkin == null && localSkin != null && localPlayer.isUsingLocalTexture(); + } + + public boolean canClear() { + return !isOffline() && !hasStatus() && !downloadInProgress() && remotePlayer.isUsingRemoteTexture(); + } + + public boolean hasStatus() { + return status != null; + } + + public String getStatusMessage() { + return hasStatus() ? status : ""; + } + + public void setMetadataField(String field, String value) { + localPlayer.releaseTextures(); + skinMetadata.put(field, value); + } + + public String getMetadataField(String field) { + return skinMetadata.getOrDefault(field, ""); + } + + public Type getSkinType() { + return skinType; + } + + public boolean tryClearStatus() { + if (!hasStatus() || !uploadInProgress()) { + status = null; + return true; + } + + return false; + } + + public CompletableFuture uploadSkin(String statusMsg) { + sendingSkin = true; + status = statusMsg; + + return gateway.uploadSkin(new SkinUpload(mc.getSession(), skinType, localSkin == null ? null : localSkin.toURI(), skinMetadata)).handle((response, throwable) -> { + if (throwable == null) { + logger.info("Upload completed with: %s", response); + setError(null); + } else { + setError(Throwables.getRootCause(throwable).toString()); + } + + fetchRemote(); + return null; + }); + } + + public CompletableFuture downloadSkin() { + String loc = remotePlayer.getLocal(skinType).getRemote().getUrl(); + + return new NetClient("GET", loc).async(HDSkinManager.skinDownloadExecutor); + } + + protected void fetchRemote() { + fetchingSkin = true; + throttlingNeck = false; + offline = false; + + remotePlayer.reloadRemoteSkin(this, (type, location, profileTexture) -> { + fetchingSkin = false; + listener.onSetRemoteSkin(type, location, profileTexture); + }).handle((a, throwable) -> { + fetchingSkin = false; + + if (throwable != null) { + throwable = throwable.getCause(); + + throwable.printStackTrace(); + + if (throwable instanceof AuthenticationUnavailableException) { + offline = true; + } else if (throwable instanceof AuthenticationException) { + throttlingNeck = true; + } else { + setError(throwable.toString()); + } + } + return a; + }); + } + + @Override + public void close() throws IOException { + localPlayer.releaseTextures(); + remotePlayer.releaseTextures(); + } + + public void setLocalSkin(File skinFile) { + mc.addScheduledTask(localPlayer::releaseTextures); + + synchronized (skinLock) { + pendingLocalSkin = skinFile; + } + } + + public void update() { + localPlayer.updateModel(); + remotePlayer.updateModel(); + + synchronized (skinLock) { + if (pendingLocalSkin != null) { + System.out.println("Set " + skinType + " " + pendingLocalSkin); + localPlayer.setLocalTexture(pendingLocalSkin, skinType); + localSkin = pendingLocalSkin; + pendingLocalSkin = null; + listener.onSetLocalSkin(skinType); + } + } + + if (isThrottled()) { + reloadCounter = (reloadCounter + 1) % (200 * retries); + if (reloadCounter == 0) { + retries++; + fetchRemote(); + } + } + } + + public CompletableFuture loadTextures(GameProfile profile) { + return gateway.getPreviewTextures(profile).thenApply(PreviewTextureManager::new); + } + + public interface ISkinUploadHandler { + default void onSetRemoteSkin(Type type, ResourceLocation location, MinecraftProfileTexture profileTexture) { + } + + default void onSetLocalSkin(Type type) { + } + + default void onSkinTypeChanged(Type newType) { + + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/VanillaModels.java b/src/hdskins/java/com/minelittlepony/hdskins/VanillaModels.java new file mode 100644 index 00000000..289701cb --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/VanillaModels.java @@ -0,0 +1,22 @@ +package com.minelittlepony.hdskins; + +public class VanillaModels { + public static final String SLIM = "slim"; + public static final String DEFAULT = "default"; + + public static String of(String model) { + return model != null && model.contains(SLIM) ? SLIM : DEFAULT; + } + + public static String nonNull(String model) { + return model == null ? DEFAULT : SLIM; + } + + public static boolean isSlim(String model) { + return SLIM.equals(model); + } + + public static boolean isFat(String model) { + return DEFAULT.equals(model); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/ducks/INetworkPlayerInfo.java b/src/hdskins/java/com/minelittlepony/hdskins/ducks/INetworkPlayerInfo.java new file mode 100644 index 00000000..c314b290 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/ducks/INetworkPlayerInfo.java @@ -0,0 +1,6 @@ +package com.minelittlepony.hdskins.ducks; + +public interface INetworkPlayerInfo { + + void reloadTextures(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/ducks/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/ducks/package-info.java new file mode 100644 index 00000000..24ad0ece --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/ducks/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.ducks; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/CubeMap.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/CubeMap.java new file mode 100644 index 00000000..17c7b33e --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/CubeMap.java @@ -0,0 +1,231 @@ +package com.minelittlepony.hdskins.gui; + +import static net.minecraft.client.renderer.GlStateManager.*; + +import org.lwjgl.opengl.GL11; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.GlStateManager.DestFactor; +import net.minecraft.client.renderer.GlStateManager.SourceFactor; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.MathHelper; + +/** + * @deprecated Replace with RenderSkybox + */ +@Deprecated +public class CubeMap { + + private int updateCounter = 0; + + private float lastPartialTick; + + private float zLevel; + + private ResourceLocation viewportTexture; + + private ResourceLocation[] cubemapTextures; + + private final Minecraft mc = Minecraft.getInstance(); + + private final GuiScreen owner; + + public CubeMap(GuiScreen owner) { + this.owner = owner; + } + + public float getDelta(float partialTick) { + return updateCounter + partialTick - lastPartialTick; + } + + public void setSource(String source) { + cubemapTextures = new ResourceLocation[] { + new ResourceLocation(String.format(source, 0)), + new ResourceLocation(String.format(source, 1)), + new ResourceLocation(String.format(source, 2)), + new ResourceLocation(String.format(source, 3)), + new ResourceLocation(String.format(source, 4)), + new ResourceLocation(String.format(source, 5)) + }; + } + + public void init() { + viewportTexture = mc.getTextureManager().getDynamicTextureLocation("skinpanorama", new DynamicTexture(256, 256, true)); + } + + public void update() { + updateCounter++; + } + + public void render(float partialTick, float z) { + zLevel = z; + lastPartialTick = updateCounter + partialTick; + + disableFog(); + mc.entityRenderer.disableLightmap(); + disableAlphaTest(); + renderPanorama(partialTick); + enableAlphaTest(); + } + + private void setupCubemapCamera() { + matrixMode(GL11.GL_PROJECTION); + pushMatrix(); + loadIdentity(); + + // Project.gluPerspective(120, 1, 0.05F, 10); + matrixMode(GL11.GL_MODELVIEW); + pushMatrix(); + loadIdentity(); + } + + private void revertPanoramaMatrix() { + matrixMode(GL11.GL_PROJECTION); + popMatrix(); + matrixMode(GL11.GL_MODELVIEW); + popMatrix(); + } + + private void renderCubeMapTexture(float partialTick) { + this.setupCubemapCamera(); + color4f(1, 1, 1, 1); + rotatef(180, 1, 0, 0); + + enableBlend(); + disableAlphaTest(); + disableCull(); + depthMask(false); + blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO); + byte blendIterations = 8; + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder vb = tessellator.getBuffer(); + + for (int blendPass = 0; blendPass < blendIterations * blendIterations; ++blendPass) { + pushMatrix(); + float offsetX = ((float) (blendPass % blendIterations) / (float) blendIterations - 0.5F) / 64; + float offsetY = ((float) (blendPass / blendIterations) / (float) blendIterations - 0.5F) / 64; + + translatef(offsetX, offsetY, 0); + rotatef(MathHelper.sin(lastPartialTick / 400) * 25 + 20, 1, 0, 0); + rotatef(-lastPartialTick / 10, 0, 1, 0); + + for (int cubeSide = 0; cubeSide < 6; ++cubeSide) { + pushMatrix(); + if (cubeSide == 1) { + rotatef(90, 0, 1, 0); + } + + if (cubeSide == 2) { + rotatef(180, 0, 1, 0); + } + + if (cubeSide == 3) { + rotatef(-90, 0, 1, 0); + } + + if (cubeSide == 4) { + rotatef(90, 1, 0, 0); + } + + if (cubeSide == 5) { + rotatef(-90, 1, 0, 0); + } + + mc.getTextureManager().bindTexture(cubemapTextures[cubeSide]); + + vb.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + + int l = 255 / (blendPass + 1); + + vb.pos(-1, -1, 1).tex(0, 0).color(255, 255, 255, l).endVertex(); + vb.pos(1, -1, 1).tex(1, 0).color(255, 255, 255, l).endVertex(); + vb.pos(1, 1, 1).tex(1, 1).color(255, 255, 255, l).endVertex(); + vb.pos(-1, 1, 1).tex(0, 1).color(255, 255, 255, l).endVertex(); + + tessellator.draw(); + popMatrix(); + } + + popMatrix(); + colorMask(true, true, true, false); + } + + vb.setTranslation(0.0D, 0.0D, 0.0D); + colorMask(true, true, true, true); + depthMask(true); + enableCull(); + enableAlphaTest(); + enableDepthTest(); + revertPanoramaMatrix(); + } + + private void rotateAndBlurCubemap() { + mc.getTextureManager().bindTexture(viewportTexture); + + GL11.glTexParameteri(3553, 10241, 9729); + GL11.glTexParameteri(3553, 10240, 9729); + GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, 256, 256); + enableBlend(); + blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO); + colorMask(true, true, true, false); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder vb = tessellator.getBuffer(); + vb.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + disableAlphaTest(); + + byte blurPasses = 3; + + for (int blurPass = 0; blurPass < blurPasses; ++blurPass) { + float f = 1 / (float)(blurPass + 1); + float var7 = (blurPass - 1) / 256F; + + vb.pos(owner.width, owner.height, zLevel).tex(var7, 1).color(1, 1, 1, f).endVertex(); + vb.pos(owner.width, 0, zLevel).tex(1 + var7, 1).color(1, 1, 1, f).endVertex(); + vb.pos(0, 0, zLevel).tex(1 + var7, 0).color(1, 1, 1, f).endVertex(); + vb.pos(0, owner.height, zLevel).tex(var7, 0).color(1, 1, 1, f).endVertex(); + } + + tessellator.draw(); + enableAlphaTest(); + colorMask(true, true, true, true); + } + + private void renderPanorama(float partialTicks) { + mc.getFramebuffer().unbindFramebuffer(); + + viewport(0, 0, 256, 256); + renderCubeMapTexture(partialTicks); + + for (int tessellator = 0; tessellator < 8; ++tessellator) { + rotateAndBlurCubemap(); + } + + mc.getFramebuffer().bindFramebuffer(true); + + viewport(0, 0, mc.mainWindow.getWidth(), mc.mainWindow.getHeight()); + + float aspect = owner.width > owner.height ? 120F / owner.width : 120F / owner.height; + float uSample = owner.height * aspect / 256F; + float vSample = owner.width * aspect / 256F; + + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder vb = tessellator.getBuffer(); + vb.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); + vb.pos(0, owner.height, zLevel).tex(0.5F - uSample, 0.5F + vSample).endVertex(); + vb.pos(owner.width, owner.height, zLevel).tex(0.5F - uSample, 0.5F - vSample).endVertex(); + vb.pos(owner.width, 0, zLevel).tex(0.5F + uSample, 0.5F - vSample).endVertex(); + vb.pos(0, 0, zLevel).tex(0.5F + uSample, 0.5F + vSample).endVertex(); + tessellator.draw(); + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/DummyWorld.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/DummyWorld.java new file mode 100644 index 00000000..890cfcca --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/DummyWorld.java @@ -0,0 +1,98 @@ +package com.minelittlepony.hdskins.gui; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.fluid.Fluid; +import net.minecraft.init.Blocks; +import net.minecraft.item.crafting.RecipeManager; +import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.tags.NetworkTagManager; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.EmptyTickList; +import net.minecraft.world.GameType; +import net.minecraft.world.ITickList; +import net.minecraft.world.World; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.WorldType; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkPrimer; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.UpgradeData; +import net.minecraft.world.dimension.OverworldDimension; +import net.minecraft.world.storage.WorldInfo; + +public class DummyWorld extends World { + + public static final World INSTANCE = new DummyWorld(); + + private final Chunk chunk = new Chunk(this, new ChunkPrimer(new ChunkPos(0, 0), UpgradeData.EMPTY), 0, 0); + + private DummyWorld() { + super(null, null, + new WorldInfo(new WorldSettings(0, GameType.NOT_SET, false, false, WorldType.DEFAULT), "MpServer"), + new OverworldDimension(), + null, + true); + } + + @Override + protected IChunkProvider createChunkProvider() { + return null; + } + + @Override + public boolean isAreaLoaded(int p_175663_1_, int p_175663_2_, int p_175663_3_, int p_175663_4_, int p_175663_5_, int p_175663_6_, boolean p_175663_7_) { + return true; + } + + @Override + public Chunk getChunk(int chunkX, int chunkZ) { + return chunk; + } + + @Override + public IBlockState getBlockState(BlockPos pos) { + return Blocks.AIR.getDefaultState(); + } + + @Override + public float getBrightness(BlockPos pos) { + return 15; + } + + @Override + public BlockPos getSpawnPoint() { + return BlockPos.ORIGIN; + } + + @Override + public ITickList getPendingBlockTicks() { + return EmptyTickList.get(); + } + + @Override + public ITickList getPendingFluidTicks() { + return EmptyTickList.get(); + } + + @Override + public boolean isChunkLoaded(int var1, int var2, boolean var3) { + return true; + } + + @Override + public Scoreboard getScoreboard() { + return null; + } + + @Override + public RecipeManager getRecipeManager() { + return null; + } + + @Override + public NetworkTagManager getTags() { + return null; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/EntityPlayerModel.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/EntityPlayerModel.java new file mode 100644 index 00000000..20eae9f7 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/EntityPlayerModel.java @@ -0,0 +1,190 @@ +package com.minelittlepony.hdskins.gui; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.minelittlepony.hdskins.SkinUploader; +import com.minelittlepony.hdskins.resources.LocalTexture; +import com.minelittlepony.hdskins.resources.LocalTexture.IBlankSkinSupplier; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.SkinManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.EntityType; +import net.minecraft.inventory.EntityEquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumHandSide; +import net.minecraft.util.ResourceLocation; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@SuppressWarnings("EntityConstructor") +public class EntityPlayerModel extends EntityLivingBase implements IBlankSkinSupplier { + + public static final ResourceLocation NO_SKIN = new ResourceLocation("hdskins", "textures/mob/noskin.png"); + public static final ResourceLocation NO_ELYTRA = new ResourceLocation("textures/entity/elytra.png"); + + private final Map armour = Maps.newEnumMap(ImmutableMap.of( + EntityEquipmentSlot.HEAD, ItemStack.EMPTY, + EntityEquipmentSlot.CHEST, ItemStack.EMPTY, + EntityEquipmentSlot.LEGS, ItemStack.EMPTY, + EntityEquipmentSlot.FEET, ItemStack.EMPTY, + EntityEquipmentSlot.MAINHAND, ItemStack.EMPTY + )); + + protected final LocalTexture skin; + protected final LocalTexture elytra; + + + private final GameProfile profile; + + protected boolean previewThinArms = false; + protected boolean previewSleeping = false; + protected boolean previewRiding = false; + + public EntityPlayerModel(GameProfile gameprofile) { + super(EntityType.PLAYER, DummyWorld.INSTANCE); + + profile = gameprofile; + + skin = new LocalTexture(profile, Type.SKIN, this); + elytra = new LocalTexture(profile, Type.ELYTRA, this); + } + + public CompletableFuture reloadRemoteSkin(SkinUploader uploader, SkinManager.SkinAvailableCallback listener) { + return uploader.loadTextures(profile).thenAcceptAsync(ptm -> { + skin.setRemote(ptm, listener); + elytra.setRemote(ptm, listener); + }, Minecraft.getInstance()::addScheduledTask); // run on main thread + } + + public void setLocalTexture(File skinTextureFile, Type type) { + if (type == Type.SKIN) { + skin.setLocal(skinTextureFile); + } else if (type == Type.ELYTRA) { + elytra.setLocal(skinTextureFile); + } + } + + @Override + public ResourceLocation getBlankSkin(Type type) { + return type == Type.SKIN ? NO_SKIN : NO_ELYTRA; + } + + public boolean isUsingLocalTexture() { + return skin.usingLocal() || elytra.usingLocal(); + } + + public boolean isTextureSetupComplete() { + return skin.uploadComplete() && elytra.uploadComplete(); + } + + public boolean isUsingRemoteTexture() { + return skin.hasRemoteTexture() || elytra.hasRemoteTexture(); + } + + public void releaseTextures() { + skin.clearLocal(); + elytra.clearLocal(); + } + + public LocalTexture getLocal(Type type) { + return type == Type.SKIN ? skin : elytra; + } + + public void setPreviewThinArms(boolean thinArms) { + previewThinArms = thinArms; + } + + public boolean usesThinSkin() { + if (skin.uploadComplete() && skin.getRemote().hasModel()) { + return skin.getRemote().usesThinArms(); + } + + return previewThinArms; + } + + public void setSleeping(boolean sleep) { + previewSleeping = sleep; + } + + public void setRiding(boolean ride) { + previewRiding = ride; + } + + @Override + public Entity getRidingEntity() { + return previewRiding ? this : null; + } + + @Override + public boolean isPlayerSleeping() { + return !previewRiding && previewSleeping; + } + + @Override + public boolean isSneaking() { + return !previewRiding && !previewSleeping && super.isSneaking(); + } + + public void updateModel() { + prevSwingProgress = swingProgress; + if (isSwingInProgress) { + ++swingProgressInt; + if (swingProgressInt >= 8) { + swingProgressInt = 0; + isSwingInProgress = false; + } + } else { + swingProgressInt = 0; + } + + swingProgress = swingProgressInt / 8F; + + motionY *= 0.98; + if (Math.abs(motionY) < 0.003) { + motionY = 0; + } + + if (posY == 0 && isJumping && !previewSleeping && !previewRiding) { + jump(); + } + + + motionY -= 0.08D; + motionY *= 0.9800000190734863D; + + posY += motionY; + + if (posY < 0) { + posY = 0; + } + onGround = posY == 0; + + ticksExisted++; + } + + @Override + public EnumHandSide getPrimaryHand() { + return Minecraft.getInstance().gameSettings.mainHand; + } + + @Override + public Iterable getArmorInventoryList() { + return armour.values(); + } + + @Override + public ItemStack getItemStackFromSlot(EntityEquipmentSlot slotIn) { + return armour.get(slotIn); + } + + @Override + public void setItemStackToSlot(EntityEquipmentSlot slotIn, ItemStack stack) { + armour.put(slotIn, stack); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/Feature.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/Feature.java new file mode 100644 index 00000000..4a4ce0e0 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/Feature.java @@ -0,0 +1,50 @@ +package com.minelittlepony.hdskins.gui; + +/** + * Represents the possible features that a skin server can implement. + */ +public enum Feature { + /** + * Whether a server has write access. + * i.e. If the server allows for users to upload a new skin. + */ + UPLOAD_USER_SKIN, + /** + * Whether a server allows for downloading and saving a user's skin. + * Most servers should support this. + */ + DOWNLOAD_USER_SKIN, + /** + * Whether a server has delete access. + * i.e. If the server allows a user to deleted a previously uploaded skin. + */ + DELETE_USER_SKIN, + /** + * Whether a server can send a full list of skins for a given profile. + * Typically used for servers that keep a record of past uploads + * and/or allow for switching between past skins. + */ + FETCH_SKIN_LIST, + /** + * Whether a server supports thin (Alex) skins or just default (Steve) skins. + * Servers without this will typically fall back to using the player's uuid on the client side. + * + * (unused) + */ + MODEL_VARIANTS, + /** + * Whether a server allows for uploading alternative skin types. i.e. Cape, Elytra, Hats and wears. + */ + MODEL_TYPES, + /** + * Whether a server will accept arbitrary extra metadata values with skin uploads. + * + * (unused) + */ + MODEL_METADATA, + /** + * Whether a server can provide a link to view a user's profile online, + * typically through a web-portal. + */ + LINK_PROFILE +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/GuiSkins.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/GuiSkins.java new file mode 100644 index 00000000..ff393e6d --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/GuiSkins.java @@ -0,0 +1,595 @@ +package com.minelittlepony.hdskins.gui; + +import com.google.common.base.Splitter; +import com.minelittlepony.common.client.gui.Button; +import com.minelittlepony.common.client.gui.GameGui; +import com.minelittlepony.common.client.gui.IGuiAction; +import com.minelittlepony.common.client.gui.IconicButton; +import com.minelittlepony.common.client.gui.IconicToggle; +import com.minelittlepony.common.client.gui.Label; +import com.minelittlepony.common.client.gui.Style; +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.SkinChooser; +import com.minelittlepony.hdskins.SkinUploader; +import com.minelittlepony.hdskins.VanillaModels; +import com.minelittlepony.hdskins.SkinUploader.ISkinUploadHandler; +import com.minelittlepony.hdskins.server.SkinServer; +import com.minelittlepony.hdskins.upload.GLWindow; +import com.minelittlepony.hdskins.util.CallableFutures; +import com.minelittlepony.hdskins.util.Edge; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.util.InputMappings; +import net.minecraft.init.Items; +import net.minecraft.init.SoundEvents; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumHand; +import net.minecraft.util.math.MathHelper; + +import org.lwjgl.BufferUtils; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL11; + +import java.io.IOException; +import java.nio.DoubleBuffer; +import java.util.List; + +import static net.minecraft.client.renderer.GlStateManager.*; + +public class GuiSkins extends GameGui implements ISkinUploadHandler { + + private int updateCounter = 0; + + private Button btnBrowse; + private FeatureButton btnUpload; + private FeatureButton btnDownload; + private FeatureButton btnClear; + + private FeatureSwitch btnModeSteve; + private FeatureSwitch btnModeAlex; + + private FeatureSwitch btnModeSkin; + private FeatureSwitch btnModeElytra; + + protected EntityPlayerModel localPlayer; + protected EntityPlayerModel remotePlayer; + + private DoubleBuffer doubleBuffer; + + private float msgFadeOpacity = 0; + + private double lastMouseX = 0; + + private boolean jumpState = false; + private boolean sneakState = false; + + protected final SkinUploader uploader; + protected final SkinChooser chooser; + + protected final CubeMap panorama; + + private final Edge ctrlKey = new Edge(this::ctrlToggled) { + @Override + protected boolean nextState() { + return GuiScreen.isCtrlKeyDown(); + } + }; + private final Edge jumpKey = new Edge(this::jumpToggled) { + @Override + protected boolean nextState() { + return InputMappings.isKeyDown(GLFW.GLFW_KEY_SPACE); + } + }; + private final Edge sneakKey = new Edge(this::sneakToggled) { + @Override + protected boolean nextState() { + return GuiScreen.isShiftKeyDown(); + } + }; + + public GuiSkins(List servers) { + mc = Minecraft.getInstance(); + GameProfile profile = mc.getSession().getProfile(); + + localPlayer = getModel(profile); + remotePlayer = getModel(profile); + + RenderManager rm = mc.getRenderManager(); + rm.textureManager = mc.getTextureManager(); + rm.options = mc.gameSettings; + rm.renderViewEntity = localPlayer; + + uploader = new SkinUploader(servers, localPlayer, remotePlayer, this); + chooser = new SkinChooser(uploader); + panorama = new CubeMap(this); + initPanorama(); + } + + protected void initPanorama() { + panorama.setSource("hdskins:textures/cubemaps/cubemap0_%d.png"); + } + + protected EntityPlayerModel getModel(GameProfile profile) { + return new EntityPlayerModel(profile); + } + + @Override + public void tick() { + + if (!(InputMappings.isKeyDown(GLFW.GLFW_KEY_LEFT) || InputMappings.isKeyDown(GLFW.GLFW_KEY_RIGHT))) { + updateCounter++; + } + + panorama.update(); + uploader.update(); + + updateButtons(); + } + + @Override + public void initGui() { + GLWindow.current().setDropTargetListener(files -> { + files.stream().findFirst().ifPresent(file -> { + chooser.selectFile(file); + updateButtons(); + }); + }); + + panorama.init(); + + addButton(new Label(width / 2, 10, "hdskins.manager", 0xffffff, true)); + addButton(new Label(34, 34, "hdskins.local", 0xffffff)); + addButton(new Label(width / 2 + 34, 34, "hdskins.server", 0xffffff)); + + addButton(btnBrowse = new Button(width / 2 - 150, height - 27, 90, 20, "hdskins.options.browse", sender -> + chooser.openBrowsePNG(mc, format("hdskins.open.title")))) + .setEnabled(!mc.mainWindow.isFullscreen()); + + addButton(btnUpload = new FeatureButton(width / 2 - 24, height / 2 - 20, 48, 20, "hdskins.options.chevy", sender -> { + if (uploader.canUpload()) { + punchServer("hdskins.upload"); + } + })).setEnabled(uploader.canUpload()) + .setTooltip("hdskins.options.chevy.title"); + + addButton(btnDownload = new FeatureButton(width / 2 - 24, height / 2 + 20, 48, 20, "hdskins.options.download", sender -> { + if (uploader.canClear()) { + chooser.openSavePNG(mc, format("hdskins.save.title")); + } + })).setEnabled(uploader.canClear()) + .setTooltip("hdskins.options.download.title"); + + addButton(btnClear = new FeatureButton(width / 2 + 60, height - 27, 90, 20, "hdskins.options.clear", sender -> { + if (uploader.canClear()) { + punchServer("hdskins.request"); + } + })).setEnabled(uploader.canClear()); + + addButton(new Button(width / 2 - 50, height - 25, 100, 20, "hdskins.options.close", sender -> + mc.displayGuiScreen(new GuiMainMenu()))); + + addButton(btnModeSteve = new FeatureSwitch(width - 25, 32, sender -> switchSkinMode(VanillaModels.DEFAULT))) + .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0x3c5dcb) + .setEnabled(VanillaModels.isSlim(uploader.getMetadataField("model"))) + .setTooltip("hdskins.mode.steve") + .setTooltipOffset(0, 10); + + addButton(btnModeAlex = new FeatureSwitch(width - 25, 51, sender -> switchSkinMode(VanillaModels.SLIM))) + .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0xfff500) + .setEnabled(VanillaModels.isFat(uploader.getMetadataField("model"))) + .setTooltip("hdskins.mode.alex") + .setTooltipOffset(0, 10); + + addButton(btnModeSkin = new FeatureSwitch(width - 25, 75, sender -> uploader.setSkinType(Type.SKIN))) + .setIcon(new ItemStack(Items.LEATHER_CHESTPLATE)) + .setEnabled(uploader.getSkinType() == Type.ELYTRA) + .setTooltip(format("hdskins.mode." + Type.SKIN.name().toLowerCase())) + .setTooltipOffset(0, 10); + + addButton(btnModeElytra = new FeatureSwitch(width - 25, 94, sender -> uploader.setSkinType(Type.ELYTRA))) + .setIcon(new ItemStack(Items.ELYTRA)) + .setEnabled(uploader.getSkinType() == Type.SKIN) + .setTooltip(format("hdskins.mode." + Type.ELYTRA.name().toLowerCase())) + .setTooltipOffset(0, 10); + + addButton(new IconicToggle(width - 25, 118, 3, sender -> { + playSound(SoundEvents.BLOCK_BREWING_STAND_BREW); + + boolean sleep = sender.getValue() == 1; + boolean ride = sender.getValue() == 2; + localPlayer.setSleeping(sleep); + remotePlayer.setSleeping(sleep); + + localPlayer.setRiding(ride); + remotePlayer.setRiding(ride); + })) + .setValue(localPlayer.isPlayerSleeping() ? 1 : 0) + .setStyle(new Style().setIcon(new ItemStack(Items.IRON_BOOTS, 1)).setTooltip("hdskins.mode.stand"), 0) + .setStyle(new Style().setIcon(new ItemStack(Items.CLOCK, 1)).setTooltip("hdskins.mode.sleep"), 1) + .setStyle(new Style().setIcon(new ItemStack(Items.OAK_BOAT, 1)).setTooltip("hdskins.mode.ride"), 2) + .setTooltipOffset(0, 10); + + addButton(new Button(width - 25, height - 65, 20, 20, "?", sender -> { + uploader.cycleGateway(); + playSound(SoundEvents.ENTITY_VILLAGER_YES); + sender.setTooltip(uploader.getGateway()); + })) + .setTooltip(uploader.getGateway()) + .setTooltipOffset(0, 10); + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + try { + uploader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + HDSkinManager.INSTANCE.clearSkinCache(); + + GLWindow.current().clearDropTargetListener(); + } + + @Override + public void onSkinTypeChanged(Type newType) { + playSound(SoundEvents.BLOCK_BREWING_STAND_BREW); + + btnModeSkin.enabled = newType == Type.ELYTRA; + btnModeElytra.enabled = newType == Type.SKIN; + } + + protected void switchSkinMode(String model) { + playSound(SoundEvents.BLOCK_BREWING_STAND_BREW); + + boolean thinArmType = VanillaModels.isSlim(model); + + btnModeSteve.enabled = thinArmType; + btnModeAlex.enabled = !thinArmType; + + uploader.setMetadataField("model", model); + localPlayer.setPreviewThinArms(thinArmType); + remotePlayer.setPreviewThinArms(thinArmType); + } + + protected boolean canTakeEvents() { + return !chooser.pickingInProgress() && uploader.tryClearStatus() && msgFadeOpacity == 0; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + lastMouseX = mouseX; + + if (canTakeEvents() && super.mouseClicked(mouseX, mouseY, button)) { + int bottom = height - 40; + int mid = width / 2; + + if ((mouseX > 30 && mouseX < mid - 30 || mouseX > mid + 30 && mouseX < width - 30) && mouseY > 30 && mouseY < bottom) { + localPlayer.swingArm(EnumHand.MAIN_HAND); + remotePlayer.swingArm(EnumHand.MAIN_HAND); + } + + return true; + } + + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double changeX, double changeY) { + lastMouseX = mouseX; + + if (canTakeEvents() && super.mouseDragged(mouseX, mouseY, button, changeX, changeY)) { + updateCounter -= (lastMouseX - mouseX); + + return true; + } + + return false; + } + + @Override + public boolean keyPressed(int mouseX, int mouseY, int keyCode) { + if (canTakeEvents()) { + if (keyCode == GLFW.GLFW_KEY_LEFT) { + updateCounter -= 5; + } else if (keyCode == GLFW.GLFW_KEY_RIGHT) { + updateCounter += 5; + } + + if (!chooser.pickingInProgress() && !uploader.uploadInProgress()) { + return super.keyPressed(mouseX, mouseY, keyCode); + } + } + + return false; + } + + private void jumpToggled(boolean jumping) { + if (jumping && ctrlKey.getState()) { + jumpState = !jumpState; + } + + jumping |= jumpState; + + localPlayer.setJumping(jumping); + remotePlayer.setJumping(jumping); + } + + private void sneakToggled(boolean sneaking) { + if (sneaking && ctrlKey.getState()) { + sneakState = !sneakState; + } + + sneaking |= sneakState; + + localPlayer.setSneaking(sneaking); + remotePlayer.setSneaking(sneaking); + } + + private void ctrlToggled(boolean ctrl) { + if (ctrl) { + if (sneakKey.getState()) { + sneakState = !sneakState; + } + + if (jumpKey.getState()) { + jumpState = !jumpState; + } + } + } + + @Override + protected void drawContents(int mouseX, int mouseY, float partialTick) { + ctrlKey.update(); + jumpKey.update(); + sneakKey.update(); + + float deltaTime = panorama.getDelta(partialTick); + panorama.render(partialTick, zLevel); + + int bottom = height - 40; + int mid = width / 2; + int horizon = height / 2 + height / 5; + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + drawRect(30, 30, mid - 30, bottom, Integer.MIN_VALUE); + drawRect(mid + 30, 30, width - 30, bottom, Integer.MIN_VALUE); + + drawGradientRect(30, horizon, mid - 30, bottom, 0x80FFFFFF, 0xffffff); + drawGradientRect(mid + 30, horizon, width - 30, bottom, 0x80FFFFFF, 0xffffff); + + super.drawContents(mouseX, mouseY, partialTick); + + enableClipping(bottom); + + float yPos = height * 0.75F; + float xPos1 = width / 4; + float xPos2 = width * 0.75F; + float scale = height / 4; + + renderPlayerModel(localPlayer, xPos1, yPos, scale, horizon - mouseY, mouseX, partialTick); + renderPlayerModel(remotePlayer, xPos2, yPos, scale, horizon - mouseY, mouseX, partialTick); + + disableClipping(); + + if (chooser.getStatus() != null && !uploader.canUpload()) { + drawRect(40, height / 2 - 12, width / 2 - 40, height / 2 + 12, 0xB0000000); + drawCenteredString(fontRenderer, format(chooser.getStatus()), (int) xPos1, height / 2 - 4, 0xffffff); + } + + if (uploader.downloadInProgress() || uploader.isThrottled() || uploader.isOffline()) { + + int lineHeight = uploader.isThrottled() ? 18 : 12; + + drawRect((int) (xPos2 - width / 4 + 40), height / 2 - lineHeight, width - 40, height / 2 + lineHeight, 0xB0000000); + + if (uploader.isThrottled()) { + drawCenteredString(fontRenderer, format(SkinUploader.ERR_MOJANG), (int) xPos2, height / 2 - 10, 0xff5555); + drawCenteredString(fontRenderer, format(SkinUploader.ERR_WAIT, uploader.getRetries()), (int) xPos2, height / 2 + 2, 0xff5555); + } else if (uploader.isOffline()) { + drawCenteredString(fontRenderer, format(SkinUploader.ERR_OFFLINE), (int) xPos2, height / 2 - 4, 0xff5555); + } else { + drawCenteredString(fontRenderer, format(SkinUploader.STATUS_FETCH), (int) xPos2, height / 2 - 4, 0xffffff); + } + } + + boolean uploadInProgress = uploader.uploadInProgress(); + boolean showError = uploader.hasStatus(); + + if (uploadInProgress || showError || msgFadeOpacity > 0) { + if (!uploadInProgress && !showError) { + msgFadeOpacity -= deltaTime / 10; + } else if (msgFadeOpacity < 1) { + msgFadeOpacity += deltaTime / 10; + } + + msgFadeOpacity = MathHelper.clamp(msgFadeOpacity, 0, 1); + } + + if (msgFadeOpacity > 0) { + int opacity = (Math.min(180, (int) (msgFadeOpacity * 180)) & 255) << 24; + + drawRect(0, 0, width, height, opacity); + + String errorMsg = format(uploader.getStatusMessage()); + + if (uploadInProgress) { + drawCenteredString(fontRenderer, errorMsg, width / 2, height / 2, 0xffffff); + } else if (showError) { + int blockHeight = (height - fontRenderer.getWordWrappedHeight(errorMsg, width - 10)) / 2; + + drawCenteredString(fontRenderer, format("hdskins.failed"), width / 2, blockHeight - fontRenderer.FONT_HEIGHT * 2, 0xffff55); + fontRenderer.drawSplitString(errorMsg, 5, blockHeight, width - 10, 0xff5555); + } + } + + depthMask(true); + enableDepthTest(); + } + + private void renderPlayerModel(EntityPlayerModel thePlayer, float xPosition, float yPosition, float scale, float mouseY, float mouseX, float partialTick) { + mc.getTextureManager().bindTexture(thePlayer.getLocal(Type.SKIN).getTexture()); + + enableColorMaterial(); + pushMatrix(); + translatef(xPosition, yPosition, 300); + + scalef(scale, scale, scale); + rotatef(-15, 1, 0, 0); + + RenderHelper.enableStandardItemLighting(); + + float rot = ((updateCounter + partialTick) * 2.5F) % 360; + + rotatef(rot, 0, 1, 0); + + float lookFactor = (float)Math.sin((rot * (Math.PI / 180)) + 45); + float lookX = (float) Math.atan((xPosition - mouseX) / 20) * 30; + + thePlayer.rotationYawHead = lookX * lookFactor; + thePlayer.rotationPitch = (float) Math.atan(mouseY / 40) * -20; + + mc.getRenderManager().renderEntity(thePlayer, 0, 0, 0, 0, 1, false); + + popMatrix(); + RenderHelper.disableStandardItemLighting(); + disableColorMaterial(); + } + +/* + * / | + * 1/ |o Q = t + q + * /q | x = xPosition - mouseX + * *-----* sin(q) = o cos(q) = x tan(q) = o/x + * --|--x------------------------------------ + * | + * mouseX + */ + + private void enableClipping(int yBottom) { + GL11.glPopAttrib(); + + if (doubleBuffer == null) { + doubleBuffer = BufferUtils.createByteBuffer(32).asDoubleBuffer(); + } + + doubleBuffer.clear(); + doubleBuffer.put(0).put(1).put(0).put(-30).flip(); + + GL11.glClipPlane(GL11.GL_CLIP_PLANE0, doubleBuffer); + doubleBuffer.clear(); + doubleBuffer.put(0).put(-1).put(0).put(yBottom).flip(); + + GL11.glClipPlane(GL11.GL_CLIP_PLANE1, doubleBuffer); + GL11.glEnable(GL11.GL_CLIP_PLANE0); + GL11.glEnable(GL11.GL_CLIP_PLANE1); + } + + private void disableClipping() { + GL11.glDisable(GL11.GL_CLIP_PLANE1); + GL11.glDisable(GL11.GL_CLIP_PLANE0); + + disableDepthTest(); + enableBlend(); + depthMask(false); + } + + private void punchServer(String uploadMsg) { + uploader.uploadSkin(uploadMsg).handle(CallableFutures.callback(this::updateButtons)); + + updateButtons(); + } + + private void updateButtons() { + btnClear.enabled = uploader.canClear(); + btnUpload.enabled = uploader.canUpload() && uploader.supportsFeature(Feature.UPLOAD_USER_SKIN); + btnDownload.enabled = uploader.canClear() && !chooser.pickingInProgress(); + btnBrowse.enabled = !chooser.pickingInProgress(); + + boolean types = !uploader.supportsFeature(Feature.MODEL_TYPES); + boolean variants = !uploader.supportsFeature(Feature.MODEL_VARIANTS); + + btnModeSkin.setLocked(types); + btnModeElytra.setLocked(types); + + btnModeSteve.setLocked(variants); + btnModeAlex.setLocked(variants); + + btnClear.setLocked(!uploader.supportsFeature(Feature.DELETE_USER_SKIN)); + btnUpload.setLocked(!uploader.supportsFeature(Feature.UPLOAD_USER_SKIN)); + btnDownload.setLocked(!uploader.supportsFeature(Feature.DOWNLOAD_USER_SKIN)); + } + + protected class FeatureButton extends Button { + private List disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList(format("hdskins.warning.disabled.description")); + + protected boolean locked; + + public FeatureButton(int x, int y, int width, int height, String label, IGuiAction callback) { + super(x, y, width, height, label, callback); + } + + @Override + protected List getTooltip() { + if (locked) { + return disabledTooltip; + } + return super.getTooltip(); + } + + @Override + public Button setTooltip(String tooltip) { + disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList( + format("hdskins.warning.disabled.title", + format(tooltip), + format("hdskins.warning.disabled.description"))); + return super.setTooltip(tooltip); + } + + public void setLocked(boolean lock) { + locked = lock; + enabled &= !lock; + } + } + + protected class FeatureSwitch extends IconicButton { + private List disabledTooltip = null; + + protected boolean locked; + + public FeatureSwitch(int x, int y, IGuiAction callback) { + super(x, y, callback); + } + + @Override + protected List getTooltip() { + if (locked) { + return disabledTooltip; + } + return super.getTooltip(); + } + + @Override + public Button setTooltip(String tooltip) { + disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList( + format("hdskins.warning.disabled.title", + format(tooltip), + format("hdskins.warning.disabled.description"))); + return super.setTooltip(tooltip); + } + + public void setLocked(boolean lock) { + locked = lock; + enabled &= !lock; + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/HDSkinsConfigPanel.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/HDSkinsConfigPanel.java new file mode 100644 index 00000000..6870a072 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/HDSkinsConfigPanel.java @@ -0,0 +1,33 @@ +package com.minelittlepony.hdskins.gui; + +import com.minelittlepony.common.client.gui.Checkbox; +import com.minelittlepony.common.client.gui.GuiHost; +import com.minelittlepony.common.client.gui.IGuiGuest; +import com.minelittlepony.hdskins.HDSkins; +import com.minelittlepony.hdskins.upload.GLWindow; + +public class HDSkinsConfigPanel implements IGuiGuest { + @Override + public void initGui(GuiHost host) { + final HDSkins mod = HDSkins.getInstance(); + + host.addButton(new Checkbox(40, 40, "hdskins.options.skindrops", mod.experimentalSkinDrop, checked -> { + mod.experimentalSkinDrop = checked; + + mod.saveConfig(); + + if (checked) { + GLWindow.create(); + } else { + GLWindow.dispose(); + } + + return checked; + })).setTooltip(host.formatMultiLine("hdskins.warning.experimental", 250)); + } + + @Override + public String getTitle() { + return "HD Skins Settings"; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/RenderPlayerModel.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/RenderPlayerModel.java new file mode 100644 index 00000000..a47dce09 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/RenderPlayerModel.java @@ -0,0 +1,219 @@ +package com.minelittlepony.hdskins.gui; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.model.ModelBiped.ArmPose; +import net.minecraft.client.renderer.entity.model.ModelElytra; +import net.minecraft.client.renderer.entity.model.ModelPlayer; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderLivingBase; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.entity.layers.LayerRenderer; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.item.EntityBoat; +import net.minecraft.entity.player.EnumPlayerModelParts; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.inventory.EntityEquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntityBed; +import net.minecraft.util.ResourceLocation; + +import org.lwjgl.opengl.GL11; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import java.util.Set; + +import static net.minecraft.client.renderer.GlStateManager.*; + +public class RenderPlayerModel extends RenderLivingBase { + + /** + * The basic Elytra texture. + */ + protected final ResourceLocation TEXTURE_ELYTRA = new ResourceLocation("textures/entity/elytra.png"); + + private static final ModelPlayer FAT = new ModelPlayer(0, false); + private static final ModelPlayer THIN = new ModelPlayer(0, true); + + public RenderPlayerModel(RenderManager renderer) { + super(renderer, FAT, 0); + this.addLayer(this.getElytraLayer()); + } + + protected LayerRenderer getElytraLayer() { + final ModelElytra modelElytra = new ModelElytra(); + return new LayerRenderer() { + @Override + public void render(EntityLivingBase entityBase, float limbSwing, float limbSwingAmount, float partialTicks, float ageInTicks, float netHeadYaw, float headPitch, float scale) { + EntityPlayerModel entity = (EntityPlayerModel) entityBase; + ItemStack itemstack = entity.getItemStackFromSlot(EntityEquipmentSlot.CHEST); + + if (itemstack.getItem() == Items.ELYTRA) { + GlStateManager.color4f(1, 1, 1, 1); + GlStateManager.enableBlend(); + GlStateManager.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + + bindTexture(entity.getLocal(Type.ELYTRA).getTexture()); + + GlStateManager.pushMatrix(); + GlStateManager.translatef(0, 0, 0.125F); + + modelElytra.setRotationAngles(limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch, scale, entity); + modelElytra.render(entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch, scale); + + GlStateManager.disableBlend(); + GlStateManager.popMatrix(); + } + } + + @Override + public boolean shouldCombineTextures() { + return false; + } + }; + } + + @Override + protected ResourceLocation getEntityTexture(M entity) { + return entity.getLocal(Type.SKIN).getTexture(); + } + + @Override + protected boolean canRenderName(M entity) { + return Minecraft.getInstance().player != null && super.canRenderName(entity); + } + + @Override + protected boolean setBrightness(M entity, float partialTicks, boolean combineTextures) { + return Minecraft.getInstance().world != null && super.setBrightness(entity, partialTicks, combineTextures); + } + + public ModelPlayer getEntityModel(M entity) { + return entity.usesThinSkin() ? THIN : FAT; + } + + @Override + public void doRender(M entity, double x, double y, double z, float entityYaw, float partialTicks) { + + if (entity.isPlayerSleeping()) { + BedHead.instance.render(entity); + } + if (entity.getRidingEntity() != null) { + MrBoaty.instance.render(); + } + + ModelPlayer player = getEntityModel(entity); + mainModel = player; + + Set parts = Minecraft.getInstance().gameSettings.getModelParts(); + player.bipedHeadwear.isHidden = !parts.contains(EnumPlayerModelParts.HAT); + player.bipedBodyWear.isHidden = !parts.contains(EnumPlayerModelParts.JACKET); + player.bipedLeftLegwear.isHidden = !parts.contains(EnumPlayerModelParts.LEFT_PANTS_LEG); + player.bipedRightLegwear.isHidden = !parts.contains(EnumPlayerModelParts.RIGHT_PANTS_LEG); + player.bipedLeftArmwear.isHidden = !parts.contains(EnumPlayerModelParts.LEFT_SLEEVE); + player.bipedRightArmwear.isHidden = !parts.contains(EnumPlayerModelParts.RIGHT_SLEEVE); + player.isSneak = entity.isSneaking(); + + player.leftArmPose = ArmPose.EMPTY; + player.rightArmPose = ArmPose.EMPTY; + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + double offset = entity.getYOffset() + entity.posY; + + + + if (entity.isPlayerSleeping()) { + y--; + z += 0.5F; + } else if (player.isSneak) { + y -= 0.125D; + } + + pushMatrix(); + enableBlend(); + color4f(1, 1, 1, 0.3F); + translated(0, offset, 0); + + if (entity.isPlayerSleeping()) { + rotatef(-90, 1, 0, 0); + } + + super.doRender(entity, x, y, z, entityYaw, partialTicks); + + color4f(1, 1, 1, 1); + disableBlend(); + popMatrix(); + GL11.glPopAttrib(); + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + pushMatrix(); + scalef(1, -1, 1); + translated(0.001, offset, 0.001); + + if (entity.isPlayerSleeping()) { + rotatef(-90, 1, 0, 0); + } + + super.doRender(entity, x, y, z, entityYaw, partialTicks); + popMatrix(); + GL11.glPopAttrib(); + } + + static class BedHead extends TileEntityBed { + public static BedHead instance = new BedHead(Blocks.RED_BED.getDefaultState()); + + public IBlockState state; + + public BedHead(IBlockState state) { + this.state = state; + } + + @Override + public IBlockState getBlockState() { + return state; + } + + public void render(Entity entity) { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + pushMatrix(); + + scalef(-1, -1, -1); + + TileEntityRendererDispatcher dispatcher = TileEntityRendererDispatcher.instance; + + dispatcher.prepare(entity.getEntityWorld(), Minecraft.getInstance().getTextureManager(), Minecraft.getInstance().getRenderManager().getFontRenderer(), entity, null, 0); + dispatcher.getRenderer(this).render(BedHead.instance, -0.5F, 0, 0, 0, -1); + + popMatrix(); + GL11.glPopAttrib(); + } + } + + static class MrBoaty extends EntityBoat { + public static MrBoaty instance = new MrBoaty(); + + public MrBoaty() { + super(null); + } + + public void render() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + pushMatrix(); + + scalef(-1, -1, -1); + + Render render = Minecraft.getInstance().getRenderManager().getEntityRenderObject(this); + + render.doRender(this, 0, 0, 0, 0, 0); + + popMatrix(); + GL11.glPopAttrib(); + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/gui/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/gui/package-info.java new file mode 100644 index 00000000..be234747 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/gui/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.gui; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinGuiMainMenu.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinGuiMainMenu.java new file mode 100644 index 00000000..6b196d0d --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinGuiMainMenu.java @@ -0,0 +1,24 @@ +package com.minelittlepony.hdskins.mixin; + +import com.minelittlepony.common.client.gui.IconicButton; +import com.minelittlepony.hdskins.HDSkinManager; + +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiMainMenu.class) +public class MixinGuiMainMenu extends GuiScreen { + + @Inject(method = "initGui()V", at = @At("RETURN")) + private void onInit(CallbackInfo ci) { + addButton(new IconicButton(width - 50, height - 50, sender -> { + mc.displayGuiScreen(HDSkinManager.INSTANCE.createSkinsGui()); + }).setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0x3c5dcb)); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinImageBufferDownload.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinImageBufferDownload.java new file mode 100644 index 00000000..a3bb6590 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinImageBufferDownload.java @@ -0,0 +1,30 @@ +package com.minelittlepony.hdskins.mixin; + +import net.minecraft.client.renderer.IImageBuffer; +import net.minecraft.client.renderer.ImageBufferDownload; +import net.minecraft.client.renderer.texture.NativeImage; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.resources.texture.SimpleDrawer; + +@Mixin(ImageBufferDownload.class) +public abstract class MixinImageBufferDownload implements IImageBuffer { + + @Inject( + method = "parseUserSkin(Lnet/minecraft/client/renderer/texture/NativeImage;)Lnet/minecraft/client/renderer/texture/NativeImage;", + at = @At("RETURN"), + cancellable = true) + private void update(NativeImage image, CallbackInfoReturnable ci) { + // convert skins from mojang server + NativeImage image2 = ci.getReturnValue(); + boolean isLegacy = image.getHeight() == 32; + if (isLegacy) { + HDSkinManager.INSTANCE.convertSkin((SimpleDrawer)(() -> image2)); + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinMinecraft.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinMinecraft.java new file mode 100644 index 00000000..98060bfa --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinMinecraft.java @@ -0,0 +1,26 @@ +/*package com.minelittlepony.hdskins.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.minelittlepony.hdskins.upload.GLWindow; + +import net.minecraft.client.Minecraft; +import net.minecraft.crash.CrashReport; + +/** + * I removed it. :T + * + * / +@Mixin(value = Minecraft.class, priority = 9000) +public abstract class MixinMinecraft { + + //public void displayCrashReport(CrashReport crashReportIn) + @Inject(method = "displayCrashReport(Lnet/minecraft/crash/CrashReport;)V", at = @At("HEAD")) + private void onGameCrash(CrashReport report, CallbackInfo info) { + GLWindow.dispose(); + } +} +*/ \ No newline at end of file diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinNetworkPlayerInfo.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinNetworkPlayerInfo.java new file mode 100644 index 00000000..376ec613 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinNetworkPlayerInfo.java @@ -0,0 +1,117 @@ +package com.minelittlepony.hdskins.mixin; + +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.ducks.INetworkPlayerInfo; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import net.minecraft.client.network.NetworkPlayerInfo; +import net.minecraft.client.resources.SkinManager; +import net.minecraft.util.ResourceLocation; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +@Mixin(NetworkPlayerInfo.class) +public abstract class MixinNetworkPlayerInfo implements INetworkPlayerInfo { + + private Map customTextures = new HashMap<>(); + private Map customProfiles = new HashMap<>(); + + private Map vanillaProfiles = new HashMap<>(); + + @Shadow + @Final + private GameProfile gameProfile; + + @Shadow + Map playerTextures; + + @SuppressWarnings("InvalidMemberReference") // mc-dev bug? + @Redirect(method = {"getLocationSkin", "getLocationCape", "getLocationElytra"}, + at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;", remap = false)) + // synthetic + private Object getSkin(Map playerTextures, Object key) { + return getSkin(playerTextures, (Type) key); + } + + // with generics + private ResourceLocation getSkin(Map playerTextures, Type type) { + if (this.customTextures.containsKey(type)) { + return this.customTextures.get(type); + } + + return playerTextures.get(type); + } + + @Nullable + @Redirect(method = "getSkinType", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/NetworkPlayerInfo;skinType:Ljava/lang/String;")) + private String getTextureModel(NetworkPlayerInfo self) { + String model = getModelFrom(customProfiles); + if (model != null) { + return model; + } + return getModelFrom(vanillaProfiles); + } + + @Nullable + private static String getModelFrom(Map texture) { + if (texture.containsKey(Type.SKIN)) { + String model = texture.get(Type.SKIN).getMetadata("model"); + + return model != null ? model : "default"; + } + return null; + } + + @Inject(method = "loadPlayerTextures", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/resources/SkinManager;loadProfileTextures(" + + "Lcom/mojang/authlib/GameProfile;" + + "Lnet/minecraft/client/resources/SkinManager$SkinAvailableCallback;" + + "Z)V", + shift = At.Shift.BEFORE)) + private void onLoadTexture(CallbackInfo ci) { + HDSkinManager.INSTANCE.fetchAndLoadSkins(gameProfile, (type, location, profileTexture) -> { + customTextures.put(type, location); + customProfiles.put(type, profileTexture); + }); + } + + @Redirect(method = "loadPlayerTextures", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/resources/SkinManager;loadProfileTextures(" + + "Lcom/mojang/authlib/GameProfile;" + + "Lnet/minecraft/client/resources/SkinManager$SkinAvailableCallback;" + + "Z)V")) + private void redirectLoadPlayerTextures(SkinManager skinManager, GameProfile profile, SkinManager.SkinAvailableCallback callback, boolean requireSecure) { + skinManager.loadProfileTextures(profile, (typeIn, location, profileTexture) -> { + HDSkinManager.INSTANCE.parseSkin(profile, typeIn, location, profileTexture).thenAccept(v -> { + playerTextures.put(typeIn, location); + vanillaProfiles.put(typeIn, profileTexture); + }); + }, requireSecure); + } + + @Override + public void reloadTextures() { + synchronized (this) { + for (Map.Entry entry : customProfiles.entrySet()) { + HDSkinManager.INSTANCE.parseSkin(gameProfile, entry.getKey(), customTextures.get(entry.getKey()), entry.getValue()); + } + for (Map.Entry entry : vanillaProfiles.entrySet()) { + HDSkinManager.INSTANCE.parseSkin(gameProfile, entry.getKey(), playerTextures.get(entry.getKey()), entry.getValue()); + } + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinSkullRenderer.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinSkullRenderer.java new file mode 100644 index 00000000..05063d90 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinSkullRenderer.java @@ -0,0 +1,37 @@ +package com.minelittlepony.hdskins.mixin; + +import com.minelittlepony.hdskins.HDSkinManager; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import net.minecraft.client.renderer.tileentity.TileEntitySkullRenderer; +import net.minecraft.client.renderer.tileentity.TileEntityRenderer; +import net.minecraft.tileentity.TileEntitySkull; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.ResourceLocation; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import javax.annotation.Nullable; + +@Mixin(TileEntitySkullRenderer.class) +public abstract class MixinSkullRenderer extends TileEntityRenderer { + + @Redirect(method = "renderSkull", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/tileentity/TileEntitySkullRenderer;bindTexture(Lnet/minecraft/util/ResourceLocation;)V", + ordinal = 4)) + private void onBindTexture(TileEntitySkullRenderer tesr, ResourceLocation rl, float x, float y, float z, EnumFacing facing, float rotation, int meta, @Nullable GameProfile profile, int p_180543_8_, float ticks) { + if (profile != null) { + ResourceLocation skin = HDSkinManager.INSTANCE.getTextures(profile).get(Type.SKIN); + + if (skin != null) { + rl = skin; + } + } + + bindTexture(rl); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinThreadDownloadImageData.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinThreadDownloadImageData.java new file mode 100644 index 00000000..13a6661b --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/MixinThreadDownloadImageData.java @@ -0,0 +1,17 @@ +package com.minelittlepony.hdskins.mixin; + +import net.minecraft.client.renderer.texture.ThreadDownloadImageData; +import net.minecraft.client.renderer.texture.NativeImage; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import com.minelittlepony.hdskins.resources.texture.IBufferedTexture; + +@Mixin(ThreadDownloadImageData.class) +public interface MixinThreadDownloadImageData extends IBufferedTexture { + + @Accessor("bufferedImage") + @Override + NativeImage getBufferedImage(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/mixin/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/mixin/package-info.java new file mode 100644 index 00000000..6039acff --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/mixin/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.mixin; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/package-info.java new file mode 100644 index 00000000..2793f2ee --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/AsyncCacheLoader.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/AsyncCacheLoader.java new file mode 100644 index 00000000..5fc2520a --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/AsyncCacheLoader.java @@ -0,0 +1,42 @@ +package com.minelittlepony.hdskins.resources; + +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/minelittlepony/hdskins/resources/ImageLoader.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/ImageLoader.java new file mode 100644 index 00000000..1f1789f3 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/ImageLoader.java @@ -0,0 +1,69 @@ +package com.minelittlepony.hdskins.resources; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.NativeImage; +import net.minecraft.util.ResourceLocation; + +import com.minelittlepony.hdskins.resources.texture.DynamicTextureImage; +import com.minelittlepony.hdskins.resources.texture.ImageBufferDownloadHD; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +public class ImageLoader implements Supplier { + + private static Minecraft mc = Minecraft.getInstance(); + + private final ResourceLocation original; + + public ImageLoader(ResourceLocation loc) { + this.original = loc; + } + + @Override + @Nullable + public ResourceLocation get() { + NativeImage image = getImage(original); + final NativeImage updated = new ImageBufferDownloadHD().parseUserSkin(image); + if (updated == null) { + return null; + } + if (updated == image) { + // don't load a new image + return this.original; + } + return addTaskAndGet(() -> loadSkin(updated)); + } + + private static V addTaskAndGet(Callable callable) { + try { + return mc.addScheduledTask(callable).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Nullable + private static NativeImage getImage(ResourceLocation res) { + + try (InputStream in = mc.getResourceManager().getResource(res).getInputStream()) { + return NativeImage.read(in); + } catch (IOException e) { + return null; + } + } + + @Nullable + private ResourceLocation loadSkin(NativeImage image) { + + ResourceLocation conv = new ResourceLocation(original.getNamespace() + "-converted", original.getPath()); + boolean success = mc.getTextureManager().loadTexture(conv, new DynamicTextureImage(image)); + return success ? conv : null; + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/LocalTexture.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/LocalTexture.java new file mode 100644 index 00000000..3b56f55a --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/LocalTexture.java @@ -0,0 +1,133 @@ +package com.minelittlepony.hdskins.resources; + +import com.minelittlepony.hdskins.resources.texture.DynamicTextureImage; +import com.minelittlepony.hdskins.resources.texture.ImageBufferDownloadHD; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.NativeImage; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.resources.SkinManager.SkinAvailableCallback; +import net.minecraft.util.ResourceLocation; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class LocalTexture { + + private final TextureManager textureManager = Minecraft.getInstance().getTextureManager(); + + private DynamicTexture local; + private PreviewTexture remote; + + private ResourceLocation remoteResource; + private ResourceLocation localResource; + + private final IBlankSkinSupplier blank; + + private final Type type; + + private boolean remoteLoaded = false; + + public LocalTexture(GameProfile profile, Type type, IBlankSkinSupplier blank) { + this.blank = blank; + this.type = type; + + String file = String.format("%s/preview_%s.png", type.name().toLowerCase(), profile.getName()); + + remoteResource = new ResourceLocation(file); + textureManager.deleteTexture(remoteResource); + + + reset(); + } + + public ResourceLocation getTexture() { + if (hasRemote()) { + return remoteResource; + } + + return localResource; + } + + public void reset() { + localResource = blank.getBlankSkin(type); + } + + public boolean hasRemote() { + return remote != null; + } + + public boolean hasLocal() { + return local != null; + } + + public boolean hasRemoteTexture() { + return uploadComplete() && remoteLoaded; + } + + public boolean usingLocal() { + return !hasRemote() && hasLocal(); + } + + public boolean uploadComplete() { + return hasRemote() && remote.isTextureUploaded(); + } + + public PreviewTexture getRemote() { + return remote; + } + + public void setRemote(PreviewTextureManager ptm, SkinAvailableCallback callback) { + clearRemote(); + + remote = ptm.getPreviewTexture(remoteResource, type, blank.getBlankSkin(type), (type, location, profileTexture) -> { + if (callback != null) { + callback.onSkinTextureAvailable(type, location, profileTexture); + } + remoteLoaded = true; + }); + } + + public void setLocal(File file) { + if (!file.exists()) { + return; + } + + clearLocal(); + + try (FileInputStream input = new FileInputStream(file)) { + NativeImage image = NativeImage.read(input); + NativeImage bufferedImage = new ImageBufferDownloadHD().parseUserSkin(image); + + local = new DynamicTextureImage(bufferedImage); + localResource = textureManager.getDynamicTextureLocation("localSkinPreview", local); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void clearRemote() { + remoteLoaded = false; + if (hasRemote()) { + remote = null; + textureManager.deleteTexture(remoteResource); + } + } + + public void clearLocal() { + if (hasLocal()) { + local = null; + textureManager.deleteTexture(localResource); + localResource = blank.getBlankSkin(type); + } + } + + public interface IBlankSkinSupplier { + + ResourceLocation getBlankSkin(Type type); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTexture.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTexture.java new file mode 100644 index 00000000..2560314c --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTexture.java @@ -0,0 +1,47 @@ +package com.minelittlepony.hdskins.resources; + +import net.minecraft.client.renderer.IImageBuffer; +import net.minecraft.client.renderer.texture.ThreadDownloadImageData; +import net.minecraft.util.ResourceLocation; + +import com.minelittlepony.hdskins.VanillaModels; + +import javax.annotation.Nullable; + +public class PreviewTexture extends ThreadDownloadImageData { + + private boolean uploaded; + + private String model; + + private String fileUrl; + + public PreviewTexture(@Nullable String model, String url, ResourceLocation fallbackTexture, @Nullable IImageBuffer imageBuffer) { + super(null, url, fallbackTexture, imageBuffer); + + this.model = VanillaModels.nonNull(model); + this.fileUrl = url; + } + + public boolean isTextureUploaded() { + return uploaded && getGlTextureId() > -1; + } + + public String getUrl() { + return fileUrl; + } + + @Override + public void deleteGlTexture() { + super.deleteGlTexture(); + uploaded = true; + } + + public boolean hasModel() { + return model != null; + } + + public boolean usesThinArms() { + return VanillaModels.isSlim(model); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTextureManager.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTextureManager.java new file mode 100644 index 00000000..30f42b55 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/PreviewTextureManager.java @@ -0,0 +1,47 @@ +package com.minelittlepony.hdskins.resources; + +import com.google.common.collect.Maps; +import com.minelittlepony.hdskins.resources.texture.ISkinAvailableCallback; +import com.minelittlepony.hdskins.resources.texture.ImageBufferDownloadHD; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; + +import net.minecraft.client.resources.SkinManager; +import net.minecraft.util.ResourceLocation; + +import java.util.Map; + +import javax.annotation.Nullable; + +/** + * Manager for fetching preview textures. This ensures that multiple calls + * to the skin server aren't done when fetching preview textures. + */ +public class PreviewTextureManager { + + private final Map textures; + + public PreviewTextureManager(MinecraftTexturesPayload payload) { + this.textures = payload.getTextures(); + } + + @Nullable + public PreviewTexture getPreviewTexture(ResourceLocation location, MinecraftProfileTexture.Type type, ResourceLocation def, @Nullable SkinManager.SkinAvailableCallback callback) { + if (!textures.containsKey(type)) { + return null; + } + + MinecraftProfileTexture texture = textures.get(type); + ISkinAvailableCallback buff = new ImageBufferDownloadHD(type, () -> { + if (callback != null) { + callback.onSkinTextureAvailable(type, location, new MinecraftProfileTexture(texture.getUrl(), Maps.newHashMap())); + } + }); + + PreviewTexture skinTexture = new PreviewTexture(texture.getMetadata("model"), texture.getUrl(), def, buff); + + TextureLoader.loadTexture(location, skinTexture); + + return skinTexture; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinData.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinData.java new file mode 100644 index 00000000..d32100b0 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinData.java @@ -0,0 +1,24 @@ +package com.minelittlepony.hdskins.resources; + +import net.minecraft.util.ResourceLocation; + +import java.util.List; +import java.util.UUID; + +@SuppressWarnings("unused") +class SkinData { + + List skins; +} + +@SuppressWarnings("unused") +class Skin { + + String name; + UUID uuid; + String skin; + + public ResourceLocation getTexture() { + return new ResourceLocation("hdskins", String.format("textures/skins/%s.png", skin)); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinResourceManager.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinResourceManager.java new file mode 100644 index 00000000..96cf7408 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/SkinResourceManager.java @@ -0,0 +1,131 @@ +package com.minelittlepony.hdskins.resources; + +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; +import net.minecraft.resources.IResource; +import net.minecraft.resources.IResourceManager; +import net.minecraft.resources.IResourceManagerReloadListener; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +public class SkinResourceManager implements IResourceManagerReloadListener { + + private static final Logger logger = LogManager.getLogger(); + + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + private Map uuidSkins = Maps.newHashMap(); + private Map namedSkins = Maps.newHashMap(); + private Map> inProgress = Maps.newHashMap(); + private Map converted = Maps.newHashMap(); + + @Override + public void onResourceManagerReload(IResourceManager resourceManager) { + uuidSkins.clear(); + namedSkins.clear(); + executor.shutdownNow(); + executor = Executors.newSingleThreadExecutor(); + inProgress.clear(); + converted.clear(); + for (String domain : resourceManager.getResourceNamespaces()) { + try { + for (IResource res : resourceManager.getAllResources(new ResourceLocation(domain, "textures/skins/skins.json"))) { + try { + SkinData data = getSkinData(res.getInputStream()); + for (Skin s : data.skins) { + if (s.uuid != null) { + uuidSkins.put(s.uuid, s); + } + if (s.name != null) { + namedSkins.put(s.name, s); + } + } + } catch (JsonParseException je) { + logger.warn("Invalid skins.json in " + res.getPackName(), je); + } + } + } catch (IOException e) { + // ignore + } + } + + } + + private SkinData getSkinData(InputStream stream) { + try { + return new Gson().fromJson(new InputStreamReader(stream), SkinData.class); + } finally { + IOUtils.closeQuietly(stream); + } + } + + @Nullable + public ResourceLocation getPlayerTexture(GameProfile profile, Type type) { + if (type != Type.SKIN) + // not supported + return null; + + Skin skin = getSkin(profile); + if (skin != null) { + final ResourceLocation res = skin.getTexture(); + return getConvertedResource(res); + } + return null; + } + + /** + * Convert older resources to a newer format. + * + * @param res The skin resource to convert + * @return The converted resource + */ + @Nullable + public ResourceLocation getConvertedResource(@Nullable ResourceLocation res) { + loadSkinResource(res); + return converted.get(res); + } + + private void loadSkinResource(@Nullable final ResourceLocation res) { + if (res != null) { + // read and convert in a new thread + this.inProgress.computeIfAbsent(res, r -> CompletableFuture.supplyAsync(new ImageLoader(r), executor) + .whenComplete((loc, t) -> { + if (loc != null) + converted.put(res, loc); + else { + LogManager.getLogger().warn("Errored while processing {}. Using original.", res, t); + converted.put(res, res); + } + })); + + + } + + } + + @Nullable + private Skin getSkin(GameProfile profile) { + Skin skin = this.uuidSkins.get(profile.getId()); + if (skin == null) { + skin = this.namedSkins.get(profile.getName()); + } + return skin; + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/TextureLoader.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/TextureLoader.java new file mode 100644 index 00000000..71392b52 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/TextureLoader.java @@ -0,0 +1,14 @@ +package com.minelittlepony.hdskins.resources; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.ITextureObject; +import net.minecraft.util.ResourceLocation; + +public class TextureLoader { + + private static Minecraft mc = Minecraft.getInstance(); + + public static void loadTexture(final ResourceLocation textureLocation, final ITextureObject textureObj) { + mc.addScheduledTask((Runnable) () -> mc.getTextureManager().loadTexture(textureLocation, textureObj)); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/package-info.java new file mode 100644 index 00000000..3bd7a16b --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.resources; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/DynamicTextureImage.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/DynamicTextureImage.java new file mode 100644 index 00000000..6ae91027 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/DynamicTextureImage.java @@ -0,0 +1,26 @@ +package com.minelittlepony.hdskins.resources.texture; + +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.NativeImage; + +public class DynamicTextureImage extends DynamicTexture implements IBufferedTexture { + + private NativeImage image; + + public DynamicTextureImage(NativeImage bufferedImage) { + super(bufferedImage); + this.image = bufferedImage; + } + + @Override + public NativeImage getBufferedImage() { + return image; + } + + @Override + public void deleteGlTexture() { + super.deleteGlTexture(); + this.image = null; + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/IBufferedTexture.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/IBufferedTexture.java new file mode 100644 index 00000000..42e590c2 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/IBufferedTexture.java @@ -0,0 +1,11 @@ +package com.minelittlepony.hdskins.resources.texture; + +import net.minecraft.client.renderer.texture.NativeImage; + +import javax.annotation.Nullable; + +public interface IBufferedTexture { + + @Nullable + NativeImage getBufferedImage(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ISkinAvailableCallback.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ISkinAvailableCallback.java new file mode 100644 index 00000000..566434a8 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ISkinAvailableCallback.java @@ -0,0 +1,12 @@ +package com.minelittlepony.hdskins.resources.texture; + +import net.minecraft.client.renderer.IImageBuffer; +import net.minecraft.client.renderer.texture.NativeImage; + +@FunctionalInterface +public interface ISkinAvailableCallback extends IImageBuffer { + @Override + default NativeImage parseUserSkin(NativeImage image) { + return image; + } +} \ No newline at end of file diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ImageBufferDownloadHD.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ImageBufferDownloadHD.java new file mode 100644 index 00000000..e7435f93 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/ImageBufferDownloadHD.java @@ -0,0 +1,89 @@ +package com.minelittlepony.hdskins.resources.texture; + +import net.minecraft.client.renderer.texture.NativeImage; + +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.ISkinModifier; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import javax.annotation.Nullable; +import java.awt.Graphics; + +public class ImageBufferDownloadHD implements SimpleDrawer, ISkinAvailableCallback, ISkinModifier.IDrawer { + + private int scale; + private Graphics graphics; + private NativeImage image; + + private ISkinAvailableCallback callback = null; + + private Type skinType = Type.SKIN; + + public ImageBufferDownloadHD() { + + } + + public ImageBufferDownloadHD(Type type, ISkinAvailableCallback callback) { + this.callback = callback; + this.skinType = type; + } + + @Override + @Nullable + @SuppressWarnings({"SuspiciousNameCombination", "NullableProblems"}) + public NativeImage parseUserSkin(@Nullable NativeImage downloadedImage) { + // TODO: Do we want to convert other skin types? + if (downloadedImage == null || skinType != Type.SKIN) { + return downloadedImage; + } + + int imageWidth = downloadedImage.getWidth(); + int imageHeight = downloadedImage.getHeight(); + if (imageHeight == imageWidth) { + return downloadedImage; + } + scale = imageWidth / 64; + image = new NativeImage(imageWidth, imageWidth, true); + image.copyImageData(downloadedImage); + + // copy layers + // leg + draw(scale, 24, 48, 20, 52, 4, 16, 8, 20); // top + draw(scale, 28, 48, 24, 52, 8, 16, 12, 20); // bottom + draw(scale, 20, 52, 16, 64, 8, 20, 12, 32); // inside + draw(scale, 24, 52, 20, 64, 4, 20, 8, 32); // front + draw(scale, 28, 52, 24, 64, 0, 20, 4, 32); // outside + draw(scale, 32, 52, 28, 64, 12, 20, 16, 32); // back + // arm + draw(scale, 40, 48, 36, 52, 44, 16, 48, 20); // top + draw(scale, 44, 48, 40, 52, 48, 16, 52, 20); // bottom + draw(scale, 36, 52, 32, 64, 48, 20, 52, 32); + draw(scale, 40, 52, 36, 64, 44, 20, 48, 32); + draw(scale, 44, 52, 40, 64, 40, 20, 44, 32); + draw(scale, 48, 52, 44, 64, 52, 20, 56, 32); + + // mod things + HDSkinManager.INSTANCE.convertSkin(this); + + graphics.dispose(); + + if (callback != null) { + return callback.parseUserSkin(image); + } + + return image; + } + + @Override + public void skinAvailable() { + if (callback != null) { + callback.skinAvailable(); + } + } + + @Override + public NativeImage getImage() { + return image; + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/SimpleDrawer.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/SimpleDrawer.java new file mode 100644 index 00000000..e69dc309 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/SimpleDrawer.java @@ -0,0 +1,41 @@ +package com.minelittlepony.hdskins.resources.texture; + +import com.minelittlepony.hdskins.ISkinModifier; + +public interface SimpleDrawer extends ISkinModifier.IDrawer { + + @Override + default void draw(int scale, + /*destination: */ int dx1, int dy1, int dx2, int dy2, + /*source: */ int sx1, int sy1, int sx2, int sy2) { + + int srcX = sx1 * scale; + int srcY = sy1 * scale; + + int dstX = dx1 * scale; + int dstY = dy1 * scale; + + int width = (sx2 - sx1) * scale; + int height = (sy2 - sy1) * scale; + + getImage().copyAreaRGBA(srcX, srcY, dstX, dstY, width, height, false, false); + } + + /*public void copyAreaRGBA(int srcX, int srcY, int dstX, int dstY, int width, int height, boolean flipX, boolean flipY) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + + int shiftX = flipX ? width - 1 - x : x; + int shiftY = flipY ? height - 1 - y : y; + + int value = image.getPixelRGBA(srcX + x, srcY + y); + + image.setPixelRGBA( + srcX + dstX + shiftX, + srcY + dstY + shiftY, + value + ); + } + } + }*/ +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/package-info.java new file mode 100644 index 00000000..12789fb4 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/resources/texture/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.resources.texture; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/BethlehemSkinServer.java b/src/hdskins/java/com/minelittlepony/hdskins/server/BethlehemSkinServer.java new file mode 100644 index 00000000..eedfbd22 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/BethlehemSkinServer.java @@ -0,0 +1,101 @@ +package com.minelittlepony.hdskins.server; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.gson.annotations.Expose; +import com.minelittlepony.hdskins.gui.Feature; +import com.minelittlepony.hdskins.util.IndentedToStringStyle; +import com.minelittlepony.hdskins.util.MoreHttpResponses; +import com.minelittlepony.hdskins.util.NetClient; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +@ServerType("bethlehem") +public class BethlehemSkinServer implements SkinServer { + + private static final String SERVER_ID = "7853dfddc358333843ad55a2c7485c4aa0380a51"; + + @Expose + private final String address; + + private BethlehemSkinServer(String address) { + this.address = address; + } + + @Override + public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException { + try (MoreHttpResponses response = new NetClient("GET", getPath(profile)).send()) { + if (!response.ok()) { + throw new IOException(response.getResponse().getStatusLine().getReasonPhrase()); + } + + return response.json(MinecraftTexturesPayload.class); + } + } + + @Override + public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + SkinServer.verifyServerConnection(upload.getSession(), SERVER_ID); + + NetClient client = new NetClient("POST", address); + + client.putHeaders(createHeaders(upload)); + + if (upload.getImage() != null) { + client.putFile(upload.getType().toString().toLowerCase(Locale.US), "image/png", upload.getImage()); + } + + try (MoreHttpResponses response = client.send()) { + if (!response.ok()) { + throw new IOException(response.text()); + } + return new SkinUploadResponse(response.text()); + } + } + + protected Map createHeaders(SkinUpload upload) { + Builder builder = ImmutableMap.builder() + .put("accessToken", upload.getSession().getToken()) + .put("user", upload.getSession().getUsername()) + .put("uuid", UUIDTypeAdapter.fromUUID(upload.getSession().getProfile().getId())) + .put("type", upload.getType().toString().toLowerCase(Locale.US)); + + if (upload.getImage() == null) { + builder.put("clear", "1"); + } else { + builder.put("model", upload.getMetadata().getOrDefault("model", "default")); + } + + return builder.build(); + } + + private String getPath(GameProfile profile) { + return String.format("%s/profile/%s", address, UUIDTypeAdapter.fromUUID(profile.getId())); + } + + @Override + public String toString() { + return new IndentedToStringStyle.Builder(this) + .append("address", address) + .build(); + } + + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case MODEL_VARIANTS: + case MODEL_TYPES: + case LINK_PROFILE: + return true; + default: return false; + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/LegacySkinServer.java b/src/hdskins/java/com/minelittlepony/hdskins/server/LegacySkinServer.java new file mode 100644 index 00000000..670550d6 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/LegacySkinServer.java @@ -0,0 +1,188 @@ +package com.minelittlepony.hdskins.server; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.gson.annotations.Expose; +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.gui.Feature; +import com.minelittlepony.hdskins.util.CallableFutures; +import com.minelittlepony.hdskins.util.IndentedToStringStyle; +import com.minelittlepony.hdskins.util.MoreHttpResponses; +import com.minelittlepony.hdskins.util.NetClient; +import com.minelittlepony.hdskins.util.TexturesPayloadBuilder; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import net.minecraft.client.Minecraft; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpHead; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.Date; +import java.util.EnumMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; + +@ServerType("legacy") +public class LegacySkinServer implements SkinServer { + + private static final String SERVER_ID = "7853dfddc358333843ad55a2c7485c4aa0380a51"; + + private static final Logger logger = LogManager.getLogger(); + + @Expose + private final String address; + + @Expose + private final String gateway; + + public LegacySkinServer(String address, @Nullable String gateway) { + this.address = address; + this.gateway = gateway; + } + + @Override + public CompletableFuture getPreviewTextures(GameProfile profile) { + return CallableFutures.asyncFailableFuture(() -> { + SkinServer.verifyServerConnection(Minecraft.getInstance().getSession(), SERVER_ID); + + if (Strings.isNullOrEmpty(gateway)) { + throw gatewayUnsupported(); + } + + Map map = new EnumMap<>(MinecraftProfileTexture.Type.class); + for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { + map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null)); + } + + return TexturesPayloadBuilder.createTexturesPayload(profile, map); + }, HDSkinManager.skinDownloadExecutor); + } + + @Override + public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { + + String url = getPath(address, type, profile); + try { + builder.put(type, loadProfileTexture(profile, url)); + } catch (IOException e) { + logger.trace("Couldn't find texture for {} at {}. Does it exist?", profile.getName(), url, e); + } + } + + Map map = builder.build(); + if (map.isEmpty()) { + throw new IOException(String.format("No textures found for %s at %s", profile, this.address)); + } + return TexturesPayloadBuilder.createTexturesPayload(profile, map); + } + + private MinecraftProfileTexture loadProfileTexture(GameProfile profile, String url) throws IOException { + try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, new HttpHead(url))) { + if (!resp.ok()) { + throw new IOException("Bad response code: " + resp.getResponseCode() + ". URL: " + url); + } + logger.debug("Found skin for {} at {}", profile.getName(), url); + + Header eTagHeader = resp.getResponse().getFirstHeader(HttpHeaders.ETAG); + final String eTag = eTagHeader == null ? "" : StringUtils.strip(eTagHeader.getValue(), "\""); + + // Add the ETag onto the end of the texture hash. Should properly cache the textures. + return new MinecraftProfileTexture(url, null) { + @Override + public String getHash() { + return super.getHash() + eTag; + } + }; + } + } + + @Override + public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + if (Strings.isNullOrEmpty(gateway)) { + throw gatewayUnsupported(); + } + + SkinServer.verifyServerConnection(upload.getSession(), SERVER_ID); + + NetClient client = new NetClient("POST", gateway); + + client.putFormData(createHeaders(upload), "image/png"); + + if (upload.getImage() != null) { + client.putFile(upload.getType().toString().toLowerCase(Locale.US), "image/png", upload.getImage()); + } + + String response = client.send().text(); + + if (response.startsWith("ERROR: ")) { + response = response.substring(7); + } + + if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK")) { + throw new IOException(response); + } + + return new SkinUploadResponse(response); + } + + private UnsupportedOperationException gatewayUnsupported() { + return new UnsupportedOperationException("Server does not have a gateway."); + } + + private Map createHeaders(SkinUpload upload) { + Builder builder = ImmutableMap.builder() + .put("user", upload.getSession().getUsername()) + .put("uuid", UUIDTypeAdapter.fromUUID(upload.getSession().getProfile().getId())) + .put("type", upload.getType().toString().toLowerCase(Locale.US)); + + if (upload.getImage() == null) { + builder.put("clear", "1"); + } + + return builder.build(); + } + + private static 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?%s", address, path, uuid, Long.toString(new Date().getTime() / 1000)); + } + + @Override + public boolean verifyGateway() { + return !Strings.isNullOrEmpty(gateway); + } + + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case DELETE_USER_SKIN: + return true; + default: return false; + } + } + + @Override + public String toString() { + return new IndentedToStringStyle.Builder(this) + .append("address", address) + .append("gateway", gateway) + .build(); + } + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/ServerType.java b/src/hdskins/java/com/minelittlepony/hdskins/server/ServerType.java new file mode 100644 index 00000000..946fc26f --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/ServerType.java @@ -0,0 +1,21 @@ +package com.minelittlepony.hdskins.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The remote server API level that this skin server implements. + * + * Current values are: + * - legacy + * - valhalla + * - bethlehem + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServerType { + + String value(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServer.java b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServer.java new file mode 100644 index 00000000..0235b160 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServer.java @@ -0,0 +1,99 @@ +package com.minelittlepony.hdskins.server; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.gui.Feature; +import com.minelittlepony.hdskins.util.CallableFutures; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.Session; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface SkinServer { + + Gson gson = new GsonBuilder() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .create(); + + List defaultServers = Lists.newArrayList(new LegacySkinServer( + "http://skins.voxelmodpack.com", + "http://skinmanager.voxelmodpack.com") + ); + + /** + * Returns true for any features that this skin server supports. + */ + boolean supportsFeature(Feature feature); + + /** + * Synchronously loads texture information for the provided profile. + * + * @return The parsed server response as a textures payload. + * + * @throws IOException If any authenticaiton or network error occurs. + */ + MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException; + + /** + * Synchronously uploads a skin to this server. + * + * @param upload The payload to send. + * + * @return A server response object. + * + * @throws IOException + * @throws AuthenticationException + */ + SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException; + + /** + * Asynchronously uploads a skin to the server. + * + * Returns an incomplete future for chaining other actions to be performed after this method completes. + * Actions are dispatched to the default skinUploadExecutor + * + * @param upload The payload to send. + */ + default CompletableFuture uploadSkin(SkinUpload upload) { + return CallableFutures.asyncFailableFuture(() -> performSkinUpload(upload), HDSkinManager.skinUploadExecutor); + } + + /** + * Asynchronously loads texture information for the provided profile. + * + * Returns an incomplete future for chaining other actions to be performed after this method completes. + * Actions are dispatched to the default skinDownloadExecutor + */ + default CompletableFuture getPreviewTextures(GameProfile profile) { + return CallableFutures.asyncFailableFuture(() -> loadProfileData(profile), HDSkinManager.skinDownloadExecutor); + } + + /** + * Called to validate this skin server's state. + * Any servers with an invalid gateway format will not be loaded and generate an exception. + */ + default boolean verifyGateway() { + return true; + } + + /** + * Joins with the Mojang API to verify the current user's session. + + * @throws AuthenticationException if authentication failed or the session is invalid. + */ + static void verifyServerConnection(Session session, String serverId) throws AuthenticationException { + MinecraftSessionService service = Minecraft.getInstance().getSessionService(); + service.joinServer(session.getProfile(), session.getToken(), serverId); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServerSerializer.java b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServerSerializer.java new file mode 100644 index 00000000..1d5624a5 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinServerSerializer.java @@ -0,0 +1,37 @@ +package com.minelittlepony.hdskins.server; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.minelittlepony.hdskins.HDSkinManager; + +import java.lang.reflect.Type; + +public class SkinServerSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(SkinServer src, Type typeOfSrc, JsonSerializationContext context) { + ServerType serverType = src.getClass().getAnnotation(ServerType.class); + + if (serverType == null) { + throw new JsonIOException("Skin server class did not have a type: " + typeOfSrc); + } + + JsonObject obj = context.serialize(src).getAsJsonObject(); + obj.addProperty("type", serverType.value()); + + return obj; + } + + @Override + public SkinServer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String type = json.getAsJsonObject().get("type").getAsString(); + + return context.deserialize(json, HDSkinManager.INSTANCE.getSkinServerClass(type)); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUpload.java b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUpload.java new file mode 100644 index 00000000..e4ab1f86 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUpload.java @@ -0,0 +1,49 @@ +package com.minelittlepony.hdskins.server; + +import net.minecraft.util.Session; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import java.net.URI; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +public class SkinUpload { + + private final Session session; + private final URI image; + private final Map metadata; + private final Type type; + + public SkinUpload(Session session, Type type, @Nullable URI image, Map metadata) { + this.session = session; + this.image = image; + this.metadata = metadata; + this.type = type; + } + + public Session getSession() { + return session; + } + + @Nullable + public URI getImage() { + return image; + } + + public Map getMetadata() { + return metadata; + } + + public MinecraftProfileTexture.Type getType() { + return type; + } + + public String getSchemaAction() { + return image == null ? "none" : image.getScheme(); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUploadResponse.java b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUploadResponse.java new file mode 100644 index 00000000..43ea208e --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/SkinUploadResponse.java @@ -0,0 +1,23 @@ +package com.minelittlepony.hdskins.server; + +import com.google.common.base.MoreObjects; + +public class SkinUploadResponse { + + private final String message; + + public SkinUploadResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("message", message) + .toString(); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/ValhallaSkinServer.java b/src/hdskins/java/com/minelittlepony/hdskins/server/ValhallaSkinServer.java new file mode 100644 index 00000000..e8aff2fa --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/ValhallaSkinServer.java @@ -0,0 +1,216 @@ +package com.minelittlepony.hdskins.server; + +import com.google.common.base.Preconditions; +import com.google.gson.annotations.Expose; +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.gui.Feature; +import com.minelittlepony.hdskins.util.IndentedToStringStyle; +import com.minelittlepony.hdskins.util.MoreHttpResponses; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.Session; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.MultipartEntityBuilder; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Locale; +import java.util.UUID; + +@ServerType("valhalla") +public class ValhallaSkinServer implements SkinServer { + + @Expose + private final String address; + + private transient String accessToken; + + public ValhallaSkinServer(String address) { + this.address = address; + } + + @Override + public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException { + try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, new HttpGet(getTexturesURI(profile)))) { + + if (response.ok()) { + return response.unwrapAsJson(MinecraftTexturesPayload.class); + } + + throw new IOException("Server sent non-ok response code: " + response.getResponseCode()); + } + } + + @Override + public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + try { + return uploadPlayerSkin(upload); + } catch (IOException e) { + if (e.getMessage().equals("Authorization failed")) { + accessToken = null; + return uploadPlayerSkin(upload); + } + + throw e; + } + } + + private SkinUploadResponse uploadPlayerSkin(SkinUpload upload) throws IOException, AuthenticationException { + authorize(upload.getSession()); + + switch (upload.getSchemaAction()) { + case "none": + return resetSkin(upload); + case "file": + return uploadFile(upload); + case "http": + case "https": + return uploadUrl(upload); + default: + throw new IOException("Unsupported URI scheme: " + upload.getSchemaAction()); + } + } + + private SkinUploadResponse resetSkin(SkinUpload upload) throws IOException { + return upload(RequestBuilder.delete() + .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) + .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) + .build()); + } + + private SkinUploadResponse uploadFile(SkinUpload upload) throws IOException { + final File file = new File(upload.getImage()); + + MultipartEntityBuilder b = MultipartEntityBuilder.create() + .addBinaryBody("file", file, ContentType.create("image/png"), file.getName()); + + upload.getMetadata().forEach(b::addTextBody); + + return upload(RequestBuilder.put() + .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) + .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) + .setEntity(b.build()) + .build()); + } + + private SkinUploadResponse uploadUrl(SkinUpload upload) throws IOException { + return upload(RequestBuilder.post() + .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) + .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) + .addParameter("file", upload.getImage().toString()) + .addParameters(MoreHttpResponses.mapAsParameters(upload.getMetadata())) + .build()); + } + + private SkinUploadResponse upload(HttpUriRequest request) throws IOException { + try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, request)) { + return response.unwrapAsJson(SkinUploadResponse.class); + } + } + + private void authorize(Session session) throws IOException, AuthenticationException { + if (this.accessToken != null) { + return; + } + GameProfile profile = session.getProfile(); + String token = session.getToken(); + AuthHandshake handshake = authHandshake(profile.getName()); + + if (handshake.offline) { + return; + } + + // join the session server + Minecraft.getInstance().getSessionService().joinServer(profile, token, handshake.serverId); + + AuthResponse response = authResponse(profile.getName(), handshake.verifyToken); + if (!response.userId.equals(profile.getId())) { + throw new IOException("UUID mismatch!"); // probably won't ever throw + } + this.accessToken = response.accessToken; + } + + private AuthHandshake authHandshake(String name) throws IOException { + try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post() + .setUri(getHandshakeURI()) + .addParameter("name", name) + .build())) { + return resp.unwrapAsJson(AuthHandshake.class); + } + } + + private AuthResponse authResponse(String name, long verifyToken) throws IOException { + try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post() + .setUri(getResponseURI()) + .addParameter("name", name) + .addParameter("verifyToken", String.valueOf(verifyToken)) + .build())) { + return resp.unwrapAsJson(AuthResponse.class); + } + } + + private URI buildUserTextureUri(GameProfile profile, MinecraftProfileTexture.Type textureType) { + String user = UUIDTypeAdapter.fromUUID(profile.getId()); + String skinType = textureType.name().toLowerCase(Locale.US); + return URI.create(String.format("%s/user/%s/%s", this.address, user, skinType)); + } + + private URI getTexturesURI(GameProfile profile) { + Preconditions.checkNotNull(profile.getId(), "profile id required for skins"); + return URI.create(String.format("%s/user/%s", this.address, UUIDTypeAdapter.fromUUID(profile.getId()))); + } + + private URI getHandshakeURI() { + return URI.create(String.format("%s/auth/handshake", this.address)); + } + + private URI getResponseURI() { + return URI.create(String.format("%s/auth/response", this.address)); + } + + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case DELETE_USER_SKIN: + case MODEL_VARIANTS: + case MODEL_TYPES: + return true; + default: return false; + } + } + + @Override + public String toString() { + return new IndentedToStringStyle.Builder(this) + .append("address", address) + .toString(); + } + + @SuppressWarnings("WeakerAccess") + private static class AuthHandshake { + + private boolean offline; + private String serverId; + private long verifyToken; + } + + @SuppressWarnings("WeakerAccess") + private static class AuthResponse { + + private String accessToken; + private UUID userId; + + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/server/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/server/package-info.java new file mode 100644 index 00000000..de870a42 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/server/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.server; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropListener.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropListener.java new file mode 100644 index 00000000..5cc57cc6 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropListener.java @@ -0,0 +1,48 @@ +package com.minelittlepony.hdskins.upload; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.io.File; +import java.io.IOException; +import java.util.List; + +@FunctionalInterface +public interface FileDropListener extends DropTargetListener { + + @Override + default void dragEnter(DropTargetDragEvent dtde) { + } + + @Override + default void dragOver(DropTargetDragEvent dtde) { + } + + @Override + default void dropActionChanged(DropTargetDragEvent dtde) { + } + + @Override + default void dragExit(DropTargetEvent dte) { + } + + @SuppressWarnings("unchecked") + @Override + default void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(DnDConstants.ACTION_LINK); + try { + onDrop((List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)); + dtde.getDropTargetContext().dropComplete(true); + } catch (UnsupportedFlavorException | IOException e) { + e.printStackTrace(); + } + + } + + void onDrop(List files); + +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropper.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropper.java new file mode 100644 index 00000000..ca9b8000 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/FileDropper.java @@ -0,0 +1,78 @@ +package com.minelittlepony.hdskins.upload; + +import net.minecraft.client.MainWindow; +import net.minecraft.client.Minecraft; + +import java.awt.Color; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetListener; +import java.util.TooManyListenersException; + +import javax.swing.BorderFactory; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.SwingConstants; + +public class FileDropper extends JFrame { + private static final long serialVersionUID = -2945117328826695659L; + + private static FileDropper instance = null; + + public static FileDropper getAWTContext() { + if (instance == null) { + instance = new FileDropper(); + } + + return instance; + } + + private final DropTarget dt; + + public FileDropper() { + super("Skin Drop"); + + setType(Type.UTILITY); + setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + setResizable(false); + setTitle("Skin Drop"); + setSize(256, 256); + setAlwaysOnTop(true); + getRootPane().setWindowDecorationStyle(JRootPane.NONE); + getContentPane().setLayout(null); + + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY)); + panel.setBounds(10, 11, 230, 205); + getContentPane().add(panel); + + JLabel txtInst = new JLabel("Drop skin files here"); + txtInst.setHorizontalAlignment(SwingConstants.CENTER); + txtInst.setVerticalAlignment(SwingConstants.CENTER); + panel.add(txtInst); + + dt = new DropTarget(); + + setDropTarget(dt); + + if (InternalDialog.hiddenFrame == null) { + InternalDialog.hiddenFrame = this; + } + } + + public void show(DropTargetListener dtl) throws TooManyListenersException { + dt.addDropTargetListener(dtl); + setVisible(true); + requestFocusInWindow(); + + MainWindow window = Minecraft.getInstance().mainWindow; + + setLocation(window.getWindowX(), window.getWindowY()); + } + + public void hide(DropTargetListener dtl) { + dt.removeDropTargetListener(dtl); + setVisible(false); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/GLWindow.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/GLWindow.java new file mode 100644 index 00000000..72462e08 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/GLWindow.java @@ -0,0 +1,291 @@ +package com.minelittlepony.hdskins.upload; + +import com.google.common.collect.Lists; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.Canvas; +import java.awt.Frame; +import java.awt.Image; +import java.awt.Window; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.List; +import java.util.TooManyListenersException; +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import javax.swing.*; + +/** + * Experimental window to control file drop. It kind of sucks. + * + * @deprecated TODO: Merge GLFW branch + */ +@Deprecated +public class GLWindow extends DropTarget { + + // Serial version because someone decided to extend DropTarget + private static final long serialVersionUID = -8891327070920541481L; + + @Nullable + private static GLWindow instance = null; + + private static final Logger logger = LogManager.getLogger(); + + /** + * Gets or creates the current GLWindow context. + */ + public static synchronized GLWindow current() { + if (instance == null) { + instance = new GLWindow(); + } + return instance; + } + + public static void create() { + try { + current().open(); + } catch (LWJGLException e) { + throw new RuntimeException(e); + } + } + + /** + * Destroys the current GLWindow context and restores default behaviour. + */ + public static synchronized void dispose() { + if (instance != null) { + instance.close(); + } + } + + private static int getScaledPixelUnit(int i) { + return Math.max((int)Math.round(i * Display.getPixelScaleFactor()), 0); + } + + private final Minecraft mc = Minecraft.getInstance(); + + private JFrame frame; + private Canvas canvas; + + private volatile DropTargetListener dropListener = null; + + private int windowState = 0; + + private boolean isFullscreen; + + private boolean ready = false; + private boolean closeRequested = false; + + private GLWindow() { + + } + + public JFrame getFrame() { + return frame; + } + + private int frameFactorX; + private int frameFactorY; + + private synchronized void open() throws LWJGLException { + // Dimensions from LWJGL may have a non 1:1 scale on high DPI monitors. + int x = getScaledPixelUnit(Display.getX()); + int y = getScaledPixelUnit(Display.getY()); + + int w = getScaledPixelUnit(Display.getWidth()); + int h = getScaledPixelUnit(Display.getHeight()); + + isFullscreen = mc.isFullScreen(); + + canvas = new Canvas(); + + frame = new JFrame(Display.getTitle()); + frame.add(canvas); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent windowEvent) { + if (!closeRequested) { + for (Window w : Frame.getWindows()) { + w.dispose(); + } + + mc.shutdown(); + } + closeRequested = false; + } + + @Override + public void windowStateChanged(WindowEvent event) { + windowState = event.getNewState(); + onResize(); + } + + @Override + public void windowOpened(WindowEvent e) { + // Once the window has opened compare the content and window dimensions to get + // the OS's frame size then reassign adjusted dimensions to match LWJGL's window. + frameFactorX = frame.getWidth() - frame.getContentPane().getWidth(); + frameFactorY = frame.getHeight() - frame.getContentPane().getHeight(); + + frame.setSize(w + frameFactorX, h + frameFactorY); + } + }); + frame.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent componentEvent) { + onResize(); + } + }); + + // TODO: (Unconfirmed) reports say the icon appears small on some OSs. + // I've yet to reproduce this. + setIcons(); + + // Order here is important. Size is set _before_ displaying but + // after events to ensure the window and canvas both get correct dimensions. + frame.setResizable(Display.isResizable()); + frame.setLocation(x, y); + frame.setSize(w, h); + frame.setVisible(true); + + Display.setParent(canvas); + Display.setFullscreen(isFullscreen); + + ready = true; + } + + private synchronized void close() { + if (frame == null) { + String msg = "GLClose was called in an illegal state! You cannot close the GLWindow before it has been opened."; + logger.fatal(new IllegalStateException(msg)); + return; + } + + closeRequested = true; + + try { + Display.setParent(null); + } catch (LWJGLException e) { + e.printStackTrace(); + } + + try { + if (isFullscreen) { + Display.setFullscreen(true); + } else { + if ((windowState & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) { + Display.setLocation(0, 0); + Display.setDisplayMode(Display.getDesktopDisplayMode()); + } else { + Display.setDisplayMode(new DisplayMode(frame.getContentPane().getWidth(), frame.getContentPane().getHeight())); + Display.setLocation(Math.max(0, frame.getX() + frameFactorX/3), Math.max(0, frame.getY() + frameFactorY/7)); + } + + // https://bugs.mojang.com/browse/MC-68754 + Display.setResizable(false); + Display.setResizable(true); + } + } catch (LWJGLException e) { + e.printStackTrace(); + } + + frame.setVisible(false); + frame.dispose(); + + for (Window w : Frame.getWindows()) { + w.dispose(); + } + + instance = null; + } + + private void setIcons() { + // VanillaTweakInjector.loadIconsOnFrames(); + try { + // + // The icons are stored in Display#cached_icons. However they're not the _original_ values. + // LWJGL copies the initial byte streams and then reverses them. The result is a stream that's not + // only already consumed, but somehow invalid when you try to parse it through ImageIO.read. + // + DefaultResourcePack pack = (DefaultResourcePack) mc.getResourcePackRepository().rprDefaultResourcePack; + + List images = Lists.newArrayList( + ImageIO.read(pack.getInputStreamAssets(new ResourceLocation("icons/icon_16x16.png"))), + ImageIO.read(pack.getInputStreamAssets(new ResourceLocation("icons/icon_32x32.png"))) + ); + + Frame[] frames = Frame.getFrames(); + + if (frames != null) { + for (Frame frame : frames) { + try { + frame.setIconImages(images); + } catch (Throwable t) {} + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void onResize() { + canvas.setBounds(0, 0, frame.getContentPane().getWidth(), frame.getContentPane().getHeight()); + } + + public void refresh(boolean fullscreen) { + if (ready && fullscreen != isFullscreen) { + // Repaint the canvas, not the window. + // The former strips the window of its state. The latter fixes a viewport scaling bug. + canvas.setBounds(0, 0, 0, 0); + onResize(); + isFullscreen = fullscreen; + } + } + + public synchronized void clearDropTargetListener() { + SwingUtilities.invokeLater(this::syncClearDropTargetListener); + } + + private void syncClearDropTargetListener() { + if (dropListener != null) { + if (!ready) { + FileDropper.getAWTContext().hide(dropListener); + } else { + frame.setDropTarget(null); + removeDropTargetListener(dropListener); + } + + dropListener = null; + } + } + + public synchronized void setDropTargetListener(FileDropListener dtl) { + SwingUtilities.invokeLater(() -> syncSetDropTargetListener(dtl)); + } + + private void syncSetDropTargetListener(FileDropListener dtl) { + try { + syncClearDropTargetListener(); + + dropListener = dtl; + + if (!ready) { + FileDropper.getAWTContext().show(dtl); + return; + } + + frame.setDropTarget(this); + addDropTargetListener(dtl); + } catch (TooManyListenersException ignored) { } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileCallback.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileCallback.java new file mode 100644 index 00000000..28c3de0d --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileCallback.java @@ -0,0 +1,8 @@ +package com.minelittlepony.hdskins.upload; + +import java.io.File; + +@FunctionalInterface +public interface IFileCallback { + void onDialogClosed(File file, int dialogResults); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileDialog.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileDialog.java new file mode 100644 index 00000000..5d26ea64 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/IFileDialog.java @@ -0,0 +1,5 @@ +package com.minelittlepony.hdskins.upload; + +public interface IFileDialog extends Runnable { + void start(); +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/InternalDialog.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/InternalDialog.java new file mode 100644 index 00000000..8d364f22 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/InternalDialog.java @@ -0,0 +1,38 @@ +package com.minelittlepony.hdskins.upload; + +import javax.swing.JFrame; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class InternalDialog { + + private static final Logger LOGGER = LogManager.getLogger(); + + static JFrame hiddenFrame; + + public static JFrame getAWTContext() { + JFrame context = GLWindow.current().getFrame(); + + if (context != null) { + return context; + } + + if (hiddenFrame == null) { + hiddenFrame = new JFrame("InternalDialogue"); + hiddenFrame.setVisible(false); + hiddenFrame.requestFocusInWindow(); + hiddenFrame.requestFocus(); + + try { + if (hiddenFrame.isAlwaysOnTopSupported()) { + hiddenFrame.setAlwaysOnTop(true); + } + } catch (SecurityException e) { + LOGGER.fatal("Could not set window on top state. This is probably Forge's fault.", e); + } + } + + return hiddenFrame; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFile.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFile.java new file mode 100644 index 00000000..52493da5 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFile.java @@ -0,0 +1,76 @@ +package com.minelittlepony.hdskins.upload; + +import net.minecraft.client.Minecraft; + +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +import org.apache.commons.lang3.StringUtils; + +import com.minelittlepony.hdskins.HDSkins; + +/** + * Base class for "open file" dialog threads + * + * @author Adam Mummery-Smith + */ +public abstract class ThreadOpenFile extends Thread implements IFileDialog { + + protected String dialogTitle; + + /** + * Delegate to call back when the dialog box is closed + */ + protected final IFileCallback parentScreen; + + protected ThreadOpenFile(Minecraft minecraft, String dialogTitle, IFileCallback callback) throws IllegalStateException { + if (minecraft.mainWindow.isFullscreen()) { + throw new IllegalStateException("Cannot open an awt window whilst minecraft is in full screen mode!"); + } + + this.parentScreen = callback; + this.dialogTitle = dialogTitle; + } + + @Override + public void run() { + JFileChooser fileDialog = new JFileChooser(); + fileDialog.setDialogTitle(dialogTitle); + + String last = HDSkins.getInstance().lastChosenFile; + if (!StringUtils.isBlank(last)) { + fileDialog.setSelectedFile(new File(last)); + } + fileDialog.setFileFilter(getFileFilter()); + + int dialogResult = showDialog(fileDialog); + + File f = fileDialog.getSelectedFile(); + + if (f != null) { + HDSkins.getInstance().lastChosenFile = f.getAbsolutePath(); + HDSkins.getInstance().saveConfig(); + + if (!f.exists() && f.getName().indexOf('.') == -1) { + f = appendMissingExtension(f); + } + } + + parentScreen.onDialogClosed(f, dialogResult); + } + + protected int showDialog(JFileChooser chooser) { + return chooser.showOpenDialog(InternalDialog.getAWTContext()); + } + + /** + * Subclasses should override this to return a file filter + */ + protected abstract FileFilter getFileFilter(); + + protected File appendMissingExtension(File file) { + return file; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFilePNG.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFilePNG.java new file mode 100644 index 00000000..b3b548bc --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadOpenFilePNG.java @@ -0,0 +1,33 @@ +package com.minelittlepony.hdskins.upload; + +import net.minecraft.client.Minecraft; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +/** + * Opens an awt "Open File" dialog with a PNG file filter + * + * @author Adam Mummery-Smith + */ +public class ThreadOpenFilePNG extends ThreadOpenFile { + + public ThreadOpenFilePNG(Minecraft minecraft, String dialogTitle, IFileCallback callback) throws IllegalStateException { + super(minecraft, dialogTitle, callback); + } + + @Override + protected FileFilter getFileFilter() { + return new FileFilter() { + @Override + public String getDescription() { + return "PNG Files (*.png)"; + } + + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".png"); + } + }; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFile.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFile.java new file mode 100644 index 00000000..97009799 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFile.java @@ -0,0 +1,47 @@ +package com.minelittlepony.hdskins.upload; + +import net.minecraft.client.Minecraft; + +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; + +import java.io.File; + +/** + * Opens an awt "Save File" dialog + */ +public abstract class ThreadSaveFile extends ThreadOpenFile { + + protected String filename; + + protected ThreadSaveFile(Minecraft minecraft, String dialogTitle, String initialFilename, IFileCallback callback) throws IllegalStateException { + super(minecraft, dialogTitle, callback); + this.filename = initialFilename; + } + + @Override + protected int showDialog(JFileChooser chooser) { + + + do { + chooser.setSelectedFile(new File(filename)); + + int result = chooser.showSaveDialog(InternalDialog.getAWTContext()); + + File f = chooser.getSelectedFile(); + if (result == 0 && f != null && f.exists()) { + + if (JOptionPane.showConfirmDialog(chooser, + "A file named \"" + f.getName() + "\" already exists. Do you want to replace it?", "Confirm", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) { + continue; + } + + f.delete(); + } + + + return result; + } while (true); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFilePNG.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFilePNG.java new file mode 100644 index 00000000..e43a40c0 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/ThreadSaveFilePNG.java @@ -0,0 +1,34 @@ +package com.minelittlepony.hdskins.upload; + +import net.minecraft.client.Minecraft; + +import javax.swing.filechooser.FileFilter; + +import java.io.File; + +public class ThreadSaveFilePNG extends ThreadSaveFile { + + public ThreadSaveFilePNG(Minecraft minecraft, String dialogTitle, String filename, IFileCallback callback) { + super(minecraft, dialogTitle, filename, callback); + } + + @Override + protected FileFilter getFileFilter() { + return new FileFilter() { + @Override + public String getDescription() { + return "PNG Files (*.png)"; + } + + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".png"); + } + }; + } + + @Override + protected File appendMissingExtension(File file) { + return new File(file.getParentFile(), file.getName() + ".png"); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/upload/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/upload/package-info.java new file mode 100644 index 00000000..71836634 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/upload/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.upload; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/CallableFutures.java b/src/hdskins/java/com/minelittlepony/hdskins/util/CallableFutures.java new file mode 100644 index 00000000..98a00c06 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/CallableFutures.java @@ -0,0 +1,56 @@ +package com.minelittlepony.hdskins.util; + +import com.google.common.util.concurrent.Runnables; +import net.minecraft.client.Minecraft; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; + +public class CallableFutures { + + private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + public static CompletableFuture asyncFailableFuture(Callable call, Executor exec) { + CompletableFuture ret = new CompletableFuture<>(); + exec.execute(() -> { + try { + ret.complete(call.call()); + } catch (Throwable e) { + ret.completeExceptionally(e); + } + }); + return ret; + } + + public static CompletableFuture failedFuture(Exception e) { + CompletableFuture ret = new CompletableFuture<>(); + ret.completeExceptionally(e); + return ret; + } + + public static BiFunction callback(Runnable c) { + return (o, t) -> { + if (t != null) { + t.printStackTrace(); + } else { + c.run(); + } + return null; + }; + } + + public static CompletableFuture scheduleTask(Runnable task) { + // schedule a task for next tick. + return CompletableFuture.runAsync(Runnables.doNothing(), delayed(50, TimeUnit.MILLISECONDS)) + .handleAsync(callback(task), Minecraft.getInstance()::addScheduledTask); + } + + private static Executor delayed(long time, TimeUnit unit) { + return (task) -> executor.schedule(task, time, unit); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/Edge.java b/src/hdskins/java/com/minelittlepony/hdskins/util/Edge.java new file mode 100644 index 00000000..385ded25 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/Edge.java @@ -0,0 +1,32 @@ +package com.minelittlepony.hdskins.util; + +public abstract class Edge { + + private boolean previousState; + + private Callback callback; + + public Edge(Callback callback) { + this.callback = callback; + } + + public void update() { + boolean state = nextState(); + + if (state != previousState) { + previousState = state; + callback.call(state); + } + } + + public boolean getState() { + return previousState; + } + + protected abstract boolean nextState(); + + @FunctionalInterface + public interface Callback { + void call(boolean state); + } +} \ No newline at end of file diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/IndentedToStringStyle.java b/src/hdskins/java/com/minelittlepony/hdskins/util/IndentedToStringStyle.java new file mode 100644 index 00000000..c8ab61cb --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/IndentedToStringStyle.java @@ -0,0 +1,35 @@ +package com.minelittlepony.hdskins.util; + +import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import static net.minecraft.util.text.TextFormatting.*; + +public class IndentedToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 2031593562293731492L; + + private static final ToStringStyle INSTANCE = new IndentedToStringStyle(); + + private IndentedToStringStyle() { + this.setFieldNameValueSeparator(": " + RESET + ITALIC); + this.setContentStart(null); + this.setFieldSeparator(SystemUtils.LINE_SEPARATOR + " " + RESET + YELLOW); + this.setFieldSeparatorAtStart(true); + this.setContentEnd(null); + this.setUseIdentityHashCode(false); + this.setUseShortClassName(true); + } + + public static class Builder extends ToStringBuilder { + public Builder(Object o) { + super(o, IndentedToStringStyle.INSTANCE); + } + + @Override + public String build() { + return YELLOW + super.build(); + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/MoreHttpResponses.java b/src/hdskins/java/com/minelittlepony/hdskins/util/MoreHttpResponses.java new file mode 100644 index 00000000..f5e0c299 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/MoreHttpResponses.java @@ -0,0 +1,111 @@ +package com.minelittlepony.hdskins.util; + +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import com.google.gson.JsonObject; +import com.minelittlepony.hdskins.server.SkinServer; + +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Utility class for getting different response types from a http response. + */ +@FunctionalInterface +public interface MoreHttpResponses extends AutoCloseable { + + CloseableHttpResponse getResponse(); + + default boolean ok() { + return getResponseCode() == HttpStatus.SC_OK; + } + + default int getResponseCode() { + return getResponse().getStatusLine().getStatusCode(); + } + + default String getContentType() { + return getResponse().getEntity().getContentType().getValue(); + } + + default InputStream getInputStream() throws IOException { + return getResponse().getEntity().getContent(); + } + + default BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); + } + + default byte[] bytes() throws IOException { + try (InputStream input = getInputStream()) { + return ByteStreams.toByteArray(input); + } + } + + default String text() throws IOException { + try (BufferedReader reader = getReader()) { + return CharStreams.toString(reader); + } + } + + default Stream lines() throws IOException { + try (BufferedReader reader = getReader()) { + return reader.lines(); + } + } + + default T json(Class type) throws IOException { + try (BufferedReader reader = getReader()) { + return SkinServer.gson.fromJson(reader, type); + } + } + + default T json(Type type) throws IOException { + try (BufferedReader reader = getReader()) { + return SkinServer.gson.fromJson(reader, type); + } + } + + default T unwrapAsJson(Type type) throws IOException { + if (!"application/json".equals(getContentType())) { + throw new IOException("Server returned a non-json response!"); + } + + if (ok()) { + return json(type); + } + + throw new IOException(json(JsonObject.class).get("message").getAsString()); + } + + @Override + default void close() throws IOException { + this.getResponse().close(); + } + + static MoreHttpResponses execute(CloseableHttpClient client, HttpUriRequest request) throws IOException { + CloseableHttpResponse response = client.execute(request); + return () -> response; + } + + static NameValuePair[] mapAsParameters(Map parameters) { + return parameters.entrySet().stream() + .map(entry -> + new BasicNameValuePair(entry.getKey(), entry.getValue()) + ) + .toArray(NameValuePair[]::new); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/NetClient.java b/src/hdskins/java/com/minelittlepony/hdskins/util/NetClient.java new file mode 100644 index 00000000..d191c1ad --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/NetClient.java @@ -0,0 +1,81 @@ +package com.minelittlepony.hdskins.util; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.MultipartEntityBuilder; + +import com.minelittlepony.hdskins.HDSkinManager; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Ew. Why so many builders? >.< + */ +public class NetClient { + + private final RequestBuilder rqBuilder; + private MultipartEntityBuilder entityBuilder; + + private Map headers; + + public NetClient(String method, String uri) { + rqBuilder = RequestBuilder.create(method).setUri(uri); + } + + public NetClient putFile(String key, String contentType, URI file) { + if (entityBuilder == null) { + entityBuilder = MultipartEntityBuilder.create(); + } + + File f = new File(file); + HttpEntity entity = entityBuilder.addBinaryBody(key, f, ContentType.create(contentType), f.getName()).build(); + + rqBuilder.setEntity(entity); + + return this; + } + + public NetClient putFormData(Map data, String contentTypes) { + if (entityBuilder == null) { + entityBuilder = MultipartEntityBuilder.create(); + } + + for (Map.Entry i : data.entrySet()) { + entityBuilder.addTextBody(i.getKey(), i.getValue().toString()); + } + return this; + } + + public NetClient putHeaders(Map headers) { + this.headers = headers; + + return this; + } + + public MoreHttpResponses send() throws IOException { + if (entityBuilder != null) { + rqBuilder.setEntity(entityBuilder.build()); + } + + HttpUriRequest request = rqBuilder.build(); + + if (headers != null) { + for (Map.Entry parameter : headers.entrySet()) { + request.addHeader(parameter.getKey(), parameter.getValue().toString()); + } + } + + return MoreHttpResponses.execute(HDSkinManager.httpClient, request); + } + + public CompletableFuture async(Executor exec) { + return CallableFutures.asyncFailableFuture(this::send, exec); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/PlayerUtil.java b/src/hdskins/java/com/minelittlepony/hdskins/util/PlayerUtil.java new file mode 100644 index 00000000..5c9e3ccd --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/PlayerUtil.java @@ -0,0 +1,24 @@ +package com.minelittlepony.hdskins.util; + +import net.minecraft.client.entity.AbstractClientPlayer; +import net.minecraft.client.network.NetworkPlayerInfo; +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.lang.reflect.Field; + +public class PlayerUtil { + + private static final Field playerInfo = FieldUtils.getAllFields(AbstractClientPlayer.class)[0]; + + public static NetworkPlayerInfo getInfo(AbstractClientPlayer player) { + try { + if (!playerInfo.isAccessible()) { + playerInfo.setAccessible(true); + } + return (NetworkPlayerInfo) FieldUtils.readField(playerInfo, player); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/ProfileTextureUtil.java b/src/hdskins/java/com/minelittlepony/hdskins/util/ProfileTextureUtil.java new file mode 100644 index 00000000..c2dd89dd --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/ProfileTextureUtil.java @@ -0,0 +1,39 @@ +package com.minelittlepony.hdskins.util; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; + +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.NativeImage; + +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.lang.reflect.Field; +import java.util.Map; + +public class ProfileTextureUtil { + + private static Field metadata = FieldUtils.getDeclaredField(MinecraftProfileTexture.class, "metadata", true); + + @SuppressWarnings("unchecked") + public static Map getMetadata(MinecraftProfileTexture texture) { + try { + return (Map) FieldUtils.readField(metadata, texture); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to read metadata field", e); + } + } + + public static void setMetadata(MinecraftProfileTexture texture, Map meta) { + try { + FieldUtils.writeField(metadata, texture, meta); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to write metadata field", e); + } + } + + public static NativeImage getDynamicBufferedImage(int width, int height, DynamicTexture texture) { + NativeImage image = new NativeImage(16, 16, true); + image.copyImageData(texture.getTextureData()); + return image; + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/TexturesPayloadBuilder.java b/src/hdskins/java/com/minelittlepony/hdskins/util/TexturesPayloadBuilder.java new file mode 100644 index 00000000..59c7a399 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/TexturesPayloadBuilder.java @@ -0,0 +1,50 @@ +package com.minelittlepony.hdskins.util; + +import com.google.gson.Gson; +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.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; + +import java.util.HashMap; +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. + */ +@SuppressWarnings("unused") +public class TexturesPayloadBuilder { + + private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + + public static MinecraftTexturesPayload createTexturesPayload(GameProfile profile, Map textures) { + // This worked fine as is before I started using sub-classes. + MinecraftTexturesPayload payload = gson.fromJson(gson.toJson(new TexturesPayloadBuilder(profile)), MinecraftTexturesPayload.class); + payload.getTextures().putAll(textures); + return payload; + } + + private long timestamp; + + private UUID profileId; + private String profileName; + + private boolean isPublic; + + private Map textures; + + private TexturesPayloadBuilder(GameProfile profile) { + profileId = profile.getId(); + profileName = profile.getName(); + timestamp = System.currentTimeMillis(); + + isPublic = true; + + this.textures = new HashMap<>(); + } +} diff --git a/src/hdskins/java/com/minelittlepony/hdskins/util/package-info.java b/src/hdskins/java/com/minelittlepony/hdskins/util/package-info.java new file mode 100644 index 00000000..d663a079 --- /dev/null +++ b/src/hdskins/java/com/minelittlepony/hdskins/util/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.util; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/hdskins/resources/assets/hdskins/lang/de_de.json b/src/hdskins/resources/assets/hdskins/lang/de_de.json new file mode 100644 index 00000000..a3dc5cff --- /dev/null +++ b/src/hdskins/resources/assets/hdskins/lang/de_de.json @@ -0,0 +1,41 @@ +{ + "hdskins.choose": "Suche die datei aus", + "hdskins.manager": "Skin Verwalter", + "hdskins.error.unreadable": "Datei nicht lesbar", + "hdskins.error.ext": "Keine PNG Datei", + "hdskins.error.open": "Fehler beim öffnen der skin Datei", + "hdskins.error.invalid": "Das ist doch kein skin du scherzkeks", + "hdskins.error.select": "Bitte erst den skin auswählen", + "hdskins.error.mojang": "Mojang API Fehler", + "hdskins.error.mojang.wait": "Bitte warte %d minuten", + "hdskins.error.noserver": "Es ist kein gültiges Skin Server verfügbar", + "hdskins.error.offline": "- Server Offline -", + "hdskins.open.title": "Suche dir ein skin aus", + "hdskins.save.title": "Wo soll dieses gegen gespeichert werden", + "hdskins.fetch": "Rufe Skin ab...", + "hdskins.failed": "Hochladen des Skins fehlgeschlagen", + "hdskins.request": "Sende Anfrage zu Server bitte warten...", + "hdskins.upload": "Skin wird hochgeladen bitte warten...", + "hdskins.local": "Lokaler Skin", + "hdskins.server": "Server Skin", + "hdskins.mode.steve": "Steve Model", + "hdskins.mode.alex": "Alex Model", + "hdskins.mode.skin": "Spieler Texture", + "hdskins.mode.cape": "Cape Texture", + "hdskins.mode.elytra": "Elytra Texture", + "hdskins.mode.stand": "Stehen", + "hdskins.mode.sleep": "Schlafen", + "hdskins.mode.ride": "Reiten", + "hdskins.options.chevy": ">>", + "hdskins.options.chevy.title": "Skin Hochladen", + "hdskins.options.download": "<<", + "hdskins.options.download.title": "Skin Speichern", + "hdskins.options.close": "Zurück", + "hdskins.options.clear": "Entfernen", + "hdskins.options.browse": "Suchen", + "hdskins.options.skindrops": "Experimental Skin Drop", + "hdskins.options.cache": "Bereinige Temporären Skin speicher (cache)", + "hdskins.warning.experimental": "§6WARNUNG: diese Funktion ist §4experimental§6, Das heißt falls es passt nicht richtig funktioniert, oder kaputt geht bist du selber verantwortlich", + "hdskins.warning.disabled.title": "§c%s %s", + "hdskins.warning.disabled.description": "§4(DEAKTIVIERT)\n§7diese Funktion wird von deinem aktuellen Skin Server nicht unterstützt.\n§7Bitte suche dir einen anderen Server um Fortzufahren" +} \ No newline at end of file diff --git a/src/hdskins/resources/assets/hdskins/lang/en_us.json b/src/hdskins/resources/assets/hdskins/lang/en_us.json new file mode 100644 index 00000000..84fc1ef8 --- /dev/null +++ b/src/hdskins/resources/assets/hdskins/lang/en_us.json @@ -0,0 +1,41 @@ +{ + "hdskins.choose": "Choose a file", + "hdskins.manager": "Skin Manager", + "hdskins.error.unreadable": "File not readable", + "hdskins.error.ext": "File not PNG", + "hdskins.error.open": "Error opening skin file", + "hdskins.error.invalid": "Not a valid skin file", + "hdskins.error.select": "Please select a skin first", + "hdskins.error.mojang": "Mojang API Error", + "hdskins.error.mojang.wait": "Please wait %d minutes", + "hdskins.error.noserver": "There was no valid skin server available", + "hdskins.error.offline": "- Server Offline -", + "hdskins.open.title": "Choose skin", + "hdskins.save.title": "Choose save location", + "hdskins.fetch": "Fetching skin...", + "hdskins.failed": "Uploading skin failed", + "hdskins.request": "Sending request to server please wait...", + "hdskins.upload": "Uploading skin please wait...", + "hdskins.local": "Local Skin", + "hdskins.server": "Server Skin", + "hdskins.mode.steve": "Steve Model", + "hdskins.mode.alex": "Alex Model", + "hdskins.mode.skin": "Player Texture", + "hdskins.mode.cape": "Cape Texture", + "hdskins.mode.elytra": "Elytra Texture", + "hdskins.mode.stand": "Standing", + "hdskins.mode.sleep": "Sleeping", + "hdskins.mode.ride": "Riding", + "hdskins.options.chevy": ">>", + "hdskins.options.chevy.title": "Upload Skin", + "hdskins.options.download": "<<", + "hdskins.options.download.title": "Save Skin", + "hdskins.options.close": "Close", + "hdskins.options.clear": "Clear", + "hdskins.options.browse": "Browse", + "hdskins.options.skindrops": "Experimental Skin Drop", + "hdskins.options.cache": "Clear Skin Cache", + "hdskins.warning.experimental": "§6WARNING: This feature is §4experimental§6, meaning things may break or derp or even slurp. Enabling this means you accept responsibility for what may happen to your chickens.", + "hdskins.warning.disabled.title": "§c%s %s", + "hdskins.warning.disabled.description": "§4(DISABLED)\n§7This feature is not supported by your current skin server.\n§7Please choose a different one to proceed." +} \ No newline at end of file diff --git a/src/hdskins/resources/assets/hdskins/lang/fr_fr.json b/src/hdskins/resources/assets/hdskins/lang/fr_fr.json new file mode 100644 index 00000000..042ce9ab --- /dev/null +++ b/src/hdskins/resources/assets/hdskins/lang/fr_fr.json @@ -0,0 +1,38 @@ +{ + "hdskins.choose": "Choisissez un fichier", + "hdskins.manager": "Skin Manager HD", + "hdskins.error.unreadable": "Fichier non lisible", + "hdskins.error.ext": "Fichier non PNG", + "hdskins.error.open": "Erreur d'ouverture du fichier", + "hdskins.error.invalid": "Pas un fichier valide", + "hdskins.error.select": "S.V.P. s�lectionner un skin en premier", + "hdskins.error.mojang": "Erreur d'API Mojang", + "hdskins.error.mojang.wait": "Please wait %d minutes", + "hdskins.open.title": "Choisissez un skin", + "hdskins.fetch": "Chargement du skin...", + "hdskins.failed": "Ajout du skin �chou�", + "hdskins.request": "Envoi de la requ�te au serveur S.V.P. patientez ...", + "hdskins.upload": "Ajout du skin, S.V.P. patientez ...", + "hdskins.local": "Local Skin", + "hdskins.server": "Server Skin", + "hdskins.mode.steve": "Steve Model", + "hdskins.mode.alex": "Alex Model", + "hdskins.mode.skin": "Player Texture", + "hdskins.mode.cape": "Cape Texture", + "hdskins.mode.elytra": "Elytra Texture", + "hdskins.mode.stand": "Standing", + "hdskins.mode.sleep": "Sleeping", + "hdskins.mode.ride": "Riding", + "hdskins.options.chevy": ">>", + "hdskins.options.chevy.title": "Upload Skin", + "hdskins.options.download": "<<", + "hdskins.options.download.title": "Save Skin", + "hdskins.options.close": "Close", + "hdskins.options.clear": "Clear", + "hdskins.options.browse": "Browse", + "hdskins.options.skindrops": "Experimental Skin Drop", + "hdskins.options.cache": "Clear Skin Cache", + "hdskins.warning.experimental": "§6WARNING: This feature is §4experimental§6, meaning things may break or derp or even slurp. Enabling this means you accept responsibility for what may happen to your chickens.", + "hdskins.warning.disabled.title": "§c%s %s", + "hdskins.warning.disabled.description": "§4(DISABLED)\n§7This feature is not supported by your current skin server.\n§7Please choose a different one to proceed." +} \ No newline at end of file diff --git a/src/hdskins/resources/assets/hdskins/lang/ru_ru.json b/src/hdskins/resources/assets/hdskins/lang/ru_ru.json new file mode 100644 index 00000000..9244f219 --- /dev/null +++ b/src/hdskins/resources/assets/hdskins/lang/ru_ru.json @@ -0,0 +1,41 @@ +{ + "hdskins.choose": "Выберите файл", + "hdskins.manager": "Помощник по работе со скинами", + "hdskins.error.unreadable": "Не удалось прочитать файл", + "hdskins.error.ext": "Файл должен быть в формате .png", + "hdskins.error.open": "Ошибка при открытии файла скина", + "hdskins.error.invalid": "Неверный файл скина", + "hdskins.error.select": "Пожалуйста, сначала выберите скин", + "hdskins.error.mojang": "Ошибка Mojang API", + "hdskins.error.mojang.wait": "Пожалуйста, подождите %d минут(ы)...", + "hdskins.error.noserver": "Не найдено ни одного скин-сервера", + "hdskins.error.offline": "- Сервер не в сети -", + "hdskins.open.title": "Выберите ваш скин", + "hdskins.save.title": "Выберите имя файла для сохранения...", + "hdskins.fetch": "Скачивание скина...", + "hdskins.failed": "Загрузка скина не удалась", + "hdskins.request": "Отправка запроса на сервер, пожалуйста, подождите.", + "hdskins.upload": "Загрузка скина, пожалуйста, подождите.", + "hdskins.local": "Локальный скин", + "hdskins.server": "Скин на сервере", + "hdskins.mode.steve": "Стандартная модель", + "hdskins.mode.alex": "Тонконогая модель", + "hdskins.mode.skin": "Скин игрока", + "hdskins.mode.cape": "Текстура плаща", + "hdskins.mode.elytra": "Текстура надкрылий", + "hdskins.mode.stand": "Обычная поза", + "hdskins.mode.sleep": "Поза сна", + "hdskins.mode.ride": "Сидячая поза", + "hdskins.options.chevy": ">>", + "hdskins.options.chevy.title": "Загрузить скин", + "hdskins.options.download": "<<", + "hdskins.options.download.title": "Скачать скин", + "hdskins.options.close": "Закрыть", + "hdskins.options.clear": "Удалить скин", + "hdskins.options.browse": "Выбрать", + "hdskins.options.skindrops": "Экспериментальное перетаскивание скина", + "hdskins.options.cache": "Очистить кеш скинов", + "hdskins.warning.experimental": "§6ВНИМАНИЕ: Эта функция §4экспериментальная§6, что означает, что вещи могут начать ломаться, извиваться или даже захлёбываться. При её включении авторы сего текста не несут ответственности за возможные последствия.", + "hdskins.warning.disabled.title": "§c%s %s", + "hdskins.warning.disabled.description": "§4(ОТКЛЮЧЕНО)\n§7Данная опция не поддерживается текущим скин-сервером.\n§7Пожалуйста, выберите другой скин-сервер." +} \ No newline at end of file diff --git a/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_0.png b/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_0.png new file mode 100644 index 0000000000000000000000000000000000000000..64d152ed2a4d308744c44bffa267af13e813d4e4 GIT binary patch literal 11342 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 EA6*&(y8r+H literal 0 HcmV?d00001 diff --git a/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_1.png b/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8f8b83a4f64f4c44e8ab3238b2423cb13bbd5d64 GIT binary patch literal 12143 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 kavZpj#FP{8=6(Bk@VRQq0i*KaH58jsO4v literal 0 HcmV?d00001 diff --git a/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_2.png b/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d27fab3f79bab0198b0874d9f96992594443b1e1 GIT binary patch literal 11219 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 KBr3#ALjMOx?d2N) literal 0 HcmV?d00001 diff --git a/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_4.png b/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_4.png new file mode 100644 index 0000000000000000000000000000000000000000..b7692f231970d685b0ddf0d9d6991999f28bab3a GIT binary patch literal 10534 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 W1tFut%>?c^g4C3>6l>+JqW%x>rH85j literal 0 HcmV?d00001 diff --git a/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_5.png b/src/hdskins/resources/assets/hdskins/textures/cubemaps/cubemap0_5.png new file mode 100644 index 0000000000000000000000000000000000000000..f05f3a12bdf1ed33401963b66134639c0baf7be5 GIT binary patch literal 9282 zcmW-m2Q*yI7snStq9%k8t3>n`#OgI_qDF5k$m+d!B7TSnqC^cMT3A+Jy+rRNy49BG ztlrE1U;cZ}>^tY3dGqd@xpVL5ekV#xLz$H5DG>++B7Lo*pbcDQ|F!$Lz&|VTdK7SZ z;HqNe0Rj736e%=K|A>%2#LnQr!a5vM^bgw5%?7%UOLcgd|JR8?ZxK{L1GieU*8% zetiL4`$a5BtG)#K6gVs0}vqy5- z8{RYZUw8xg`O!S~rcI1+Lt=?s)PLZ+Y=1d3zg1m&F6vqm3O-d`{e9`{%X-MZ`a7IY zuKONDuN@+~L>sZ#`?3)kLG#{Kuxa~;vKC|@LAk^1^-BLL`Ww&pzV<$tZ~g%3`;RE` z)pXP4&n+Dd1kRl0TvG~|5*&=rC^So&;p~lIh&Zht%N4PQ zp7ybf^?T_gd}+=(!QZUJ3B%0Z!UjIr)X{9~-f) ztxm-)=|!UVk_RgIO$euVvSM^gIaD-ya81&1xXHITf^P%p={YEn2Mj8+gFJ&9(E7Pr zIFPD_;;5H!<-*^#s#!Lgvp%n2g*H^{b=LmsrA#n18JypMZ`orML;o5jIaHTE_@%0+ zSE3P-5@v=|Wb^bC>g8#zQH)q5b7afvKkhW;yO|U;Whrnumx`{ei4Z_+X)FG7r;_Of zLT(ALTS@drGOj<{qXtI`cywBoaRl7zA%22`cGhb{t~^yLJ-`m`9D_Ex`Z+zN^*7xY zb7LkS1e=mCUgjA5L=l>^a=WwY6a`pIoYeV>XLSv`GYs08fn(((Jz`=@nHzM+^5zWd z9@guGati$hYoqk?iA`Jxs3E7}EP~W8L+bC_REV8MWOdakp}^CgMa8kj6d}T!Dc7Bk z4ExGqW{d`=c*G0Ak3Uz`KBv}XMU$ZFV;m?XgDIMO9+=k`7-s7A-PKX6Sjl8!bHY4c^QcO$Wg=)kjLNhdO#dN7RTj07#eRV>$uIH6EhM;p(Wc&2}cpCy7C zZBIMQ4Ds|wM5I4WT@0}Dm9o?%Y^Dt5tN1Ihu$XI{W-TT1$i$7~h4l+e5R;#O&lFvO zu}C*WuO#+zz+zv)ykAmplQL2$Yln&UvADaMHTBLwJvm^QCRp1LIX=@(hd)B?oLue) z%rIiiO?M`B;)@$J5%3SO-Au9ot1M18&x&a&CKCx>4plF{RW0an*v5dbfDc~bzU{Bl z58ZVIz$EEDsy`NT%Hpe>0F2rbuGMnYq|>RIG3EU?MbTa(EMO|(Z|MR>cG`N>2&LO` zi~au!qa~rciIFe|LS^rAr7@p~f%;lwHJmIy_X&aU4Sw6I80sLK>Qui0qp$zHSw3+j ztH}-Xja%G)5exZDB69S=_ z>SZS|FCn7Z_`m2*d|$zl<;I0|G6HAM3ist0>k0<^N*l`9vGQ*Xdwst{hKlG)|Id6YGYgm6GT*m8Fs`H1hPL^e~XuKN`;jdq#Y zWXry!sg8X{STH>JUMj7)WvI@2Twe}wQA;ntMJw4mg6-OpM7d-Dw{{y`Zls4%h9Mhg z>^5W8qk!Ek+v+ju9qGcH$_v>1>z zIDX+=TM-jF!ta~%zD@Mub0Bz1O?Lv$mr~sSOVCm?@;1@4W^L=IY>MH^C)t-yzE$|r z9dtd(O6)n0{qaVa>6?oN^wf(7+Plg_fbPnUmu%o~Hgf?B-NFQj+8^}AL65{=fj@3ezE<>p7HF*Y5`@yZa}Mp7dzX$9uJN!s7^DR!kyH% zw@TNxkJQX3Zz5XTgOPlS;yFzIb6%Cd=M*QWe#)Ha_b3xN`^A#HPwSnqybu+DnOb zs_G@1+D?LXPx;r(0I!su+myOKn$El6&EvM;%^E2iC1M$oq z!I0nH3t7mE#0eMK5o8MVUB2#+rX(3C+3=UU2MY=_kcCK!XCmq9z4GIQgyekSHhXUj5i^k6R3mD)?+JfX9!6UongC z!QfVT2M|Tn<~l5wCAJ zO-D|neu_5;^+1#zjEOdFd6!sn%~KV&-Kiztu?_e=^#AVQ4Im1SDhi`EIy%a|(!o0(x^da1BfF6HGb z$;?5DfEp45Vwdgc(4hvUo~iULtt^rk*PS?qv*=$RpS?38+T7$_VqVo!V|EfdcW$o_ zG=GT*O`}vv*-6)>KULkcMtFaVztr*2uF<kNLIQs!8SMtS}v z2aqn+55DFpP*r%>3wb@=W>&7J$Y6S3E$8hBQ_XjA@ftRW!Tx_5FVZyDqnb_sgDdh! z3oT&+sO>n*pBZ>nh|`+I)Qy{+gGzSGJ1h9i47qlV{!XmgRAfb%@HG|5xU2WSlvoFH z1AyUb5h=Jjwjv&r1N2e6X{(=e-?zbBS=|e9b@C2^ex)Z~>ZpbXMW3cH+vMz*a@UU`I_ zr5>%m0>*Qc`@8|z5gj_L9@^)F!xO8ku9BMH*O*NQ7ev3`^BxhHL(Yu?*=_M5kMf4Z zrt_`11of~JJ*neUniCCxdo|Z|@+ST&nUA}Prn67EW6F}py9S9CT5P=OOD%f!pv4@P zR-IW7)PPiPwDK@qaL;;iQbH$AmDx(#LqB@Uo_5f3sFq{8jR#-oZ!wp?)wBG>1lcD# zd5-i#h?T0kW;Rt! z*`^4|LQdwV7_6||hg9$~C068`>K{!!xN zFx$Ws1)tF?1eZ^O3J{n!23!SvJw_U`@yCf9CpcU;D<3>V%j({L1|eR8wmWg2i8Es^s=zGcP=J%{IRo z9euLh`=J%VhDa=GF;+{7wq>A0$jv;pg#X~v^QzCPoGcGv9#sTbLM?u4w}vi*qq{e?;$Ab3f&nlS!*f+D!#n zNmDmw_bt#zC|j(mmjpWX5;aU}GRcYPIMX_ z;cA-&IMdvITp-PLJDzDdB)@A|o5rgUq3G(~6z#lf zHW6LE!;R->LW*x~y%Gbe_+Zq_N&mzuJn$!0uxQHULwc=nE+6OK3LpCzHon}Om!jYB|U zj|$!4wXIdwdzC_LIxNtbJT*8?W~#rQKxG59pGnr(wUSo zuPG=QW=#(EQ9?wHi z$wWKadXM1GF+5${Nr(3uI=$n1^^q}-%}wzfsk!1$kYjFNlKBk0irB~z_X>IwfJ&y| zSo24;T0opFrW!pYT=|Wem5A|+vikJyjQ?|~ipQ77Nntvm$Eg&W3_6BgsA(JgdSlR5o7^{T!rd5ikU)d=`0^LI^x zX<1@Zm(U%Kx-4d^#)<_yjAuG6uuLLfWMArXtNu*W@SqE;)n*HvEykAyEHY;uw{!MM zmw*b%BP;1)@G!^tQl8oVhnr-vjus-dTQ;O)S1?&^0S3SEAap|8%CPb{*DvKnA9u#`iv?9 z+uZS)H-(&`7%jtOTW6mP6i_yXuU#$V;^tHIx(tsTv*hoJr3Uy_S>Ce<;Mekm)iiA)Ux3EAEQ>xlERPN8kD2lQ zTx*XjOl-@Mq^!|Xpyc*l1r@Ks(EmsdzY@}^ZK3SoZ!JQm)Te9mPqVPiN*y;}mVg?# zq`udp;tsiqe1{XfxRD)*L1Qoa0^BZaYe3X}Ly)-8HIjo&LR7~7-HHtZb{Buj`ycgo}c#!L?ZUHA*B{A)S{QcbV)D& zbN&ZTZE^pB+FwN?sjS_W>oV_#^^ciO2oQ?(Y}VZ60tn7rxXi_-YIZ^!oy=vU&#Qej zGmiBm{^$=dA{HHU2E43ObBEgnpQRw*ovFE@TKXD06+n|(*a3%5mI&3A*C>5XLd4<= zQ(kqj07|WSO)8rX#PhPvA}Eo6t>M|U6}kz3pLrP}jSv&J*RV%^zMy_ow=4A3CaMw8 zH;f+QyDyiAE*yK#f3;87O{}kS3AyOfR9)_o@t1IAyf7 z!8FZOS}OV8858ZMbxy1$Y{9Y&;Z}9V_uNDe9lKvXFi|9_FL(`@D78ryf2EPz0*@UY zdfhd~zw~p(RVccqYaYtmu{KX;x>!r23_z<^D^A!kj?TqqMmDzk-re<5v&8A5A&bmT z5E!@WetFEaNS3tU{^Y%hw`-IMalACE#qjqzs5tqpa4%x(c8G_5$O6Nb9z_I2A!MT!B)cED#tj~MUGiay;m0Nfbz=oUsTq^g_%TAe-M(kc9hp*%*<2}sn zXuzh#x%@eLB80}@#j19xC1hV3G~W_}MLU!=limLZ5#2xqTL3O}xncGjFk{GOZ_t*waPw7VKY_yQ`NWmtj6F*k*Q_i^atKpp8Rv z?0R2Z$jMpA^+0XQaiL4_h9<-9Wp?u^JN6Ro(|SAww7(&f> z)bBQ)$(AqjR$}MbFILyuiJ^;=CP;0Oq`8=!6_lhW^B|!$mtLLgA zund>@j@t>(I{)q7{Ex$GA6-03D( z+OH&LiGQLeJ0fY^2o}L-;}DagB1UW0D%=03@Y$LrHJi>PL>x&_s8ZAiiE6!wREc^n zR(#C5gkwf_;zy$Jj6WKr6L3rzaySLNrrzU;@805^!}%1Z8R@-WQ`6`ZN=R;YeLxo% zzGxCk-1+R`CFpCyvrud8Jd&m{$%+-NL;5+0ma=N5g&u=xfdU(Um+sNwUxD$(9bu#v z|9CD}m0Smk$tb}C1hp<)nOiu4c@37`E~em zMJ~40G$8MO@>sfxS!%xf7APWnQ~Woh|1HO*_fpMV_mH24-pU9z>+foxWVjXTz#85% z&fZE!F?23L>W)H`R}C(zqw0gV>Y z10)R~sSu}d4!P=vs^U|p0Xn7~H+argPAsC1=zG@wiMC*%+}eiZwOQ|O6gHKOY_k!q9s|an z$P#uyJ;y9X-0i{q*&pn)jZY^WID_Za3(6uf8B5og_aI+VkTYHzFUSz@0}5150ICHo z5AWYJPP3vbq;Ytt%7s9H1rrVV3FMI?ZZKkHVTl+gX4cAevdMmmO^wEi@aHHe7<_!@ znTb*Hx97%207oW}D+>c%BSjjAGU@UZUc$J?ot$fdm{B>Ygw(-FiFiJG8#e!BD&*?w z>?(0^y-|X0U-EiuHKx$TskAjyCh*8W#`k7&DQGpL6}xEyG+@yV&>{@jMT33p-M-Y# zl>y^z3id?RbAx+t{fYQy%KhCS*(dP+U(9YBxH`@gJ`yo|CqK)1Vm4HSfJz0BXvP<9 z#3Om!ct)+FTD6q{xj>y@nj$3mO$$Jf<1-lHT02SQn##w?dtPS%%ROD%f7HX{C%Ky) zM@R)NvA5J)e~~nH>Jq&6PO2_shY`DC(s~k?)pU;OAUqGP&2GAz$~c!k=Z*_LIz)Xw z`Ok`he1={Aj=trbT2;ons7Lbn1LlrRm(%NMz!tu*K1v^*k`TAW5PQY`9<7$hm2PUF z^dw!kW-{5~YYVT!9ZTBNDnoK`Hza-iUNGn7y#l5~oBjjS#GPW_($6UkrXE1?boR>N z3!WFX$y<&q?`c4*D~4OH#bK8R930w8O&qprkOU>A|9(Lj1ID|*tLK5&V-qIC-)q{8 zj`tu-X2h8_hnr-JH*fZ~%QeAK^RqBlGG8|5rHPni=WQiry9Ex*qOYY8sl-noEd=#8 z0L)Q{PNM{lFt!^A>^ZZ4(M_(sF_yI}`UD5gw|jCVqrXf_2DYSjk4NpFNW%9vN#6sK z+m6i5W>|J3-R*pt^vz-1;%R2&>-%{waL?aSc;?KD?O)054G25#irAngL^s+;dWM1_ z@-UGE@<7F-t?2smK-yO3%idP@%abPsWBNG;EZNP*|G}*Uqhs3Ef(kd%oIFgY*U|;+ zZq90@NjVvVZ#rtB$8r>i=H1%~JZ46%e2KmF6o={Sql5m}tl5wV&mwVZzRfdf!y%QM zG_fKRqFq~C^LU+*hnnC1;Z(byrDk)E%eiEg1N;{saFWVZ$*#_i0{K9;`t_fwhU(CK z#ubP<0ZzktZ(zj{h-ITZ;E?R&rlmS9H129GuH`aZb1k$rsHcjd;6dQZgD=jt|32`# z($$&sM?UihK7F%NI;v|$XhXT~h2p}mmel$YgE4!wa_HamJm6&fwbUdm5uHEE&GdpL zTMSVr&M>1UNk2mAP=7zGtUv}%r$^UacU9@EYktHS{02AsdcK1(2Pr0Tr5&p}qtc=b1D{<|YFygOx^gEmRk_*GmlAWC#q5(xSzG?aYkzVix z)(=<4zo(XQn3;(zR1~xV`V`3Y07tPUMx@oWRhou)Lxbz`k6>MjS8n?J0WTL(MAeMi z`Q$)7rGJx60%A(2XEH;_5Mz3wuwc75hSDj^R%5TK{@_kKOxaOOJK~5UIoC z*Lrvwl8AU!>$2ouPrcAklgym&AoV4zX4HhZxn>kfyiilrDopLNx=9%0Z^kGhpAscc z>nj(b{9;=vW6PowQ0J1z>}grhGZ0r3mqiunBDTXPMSm$bSBU#k&PfCxDBDf$99vbV z&I0n&&acvEMSlS$41jDwO)B5*gIt$j^cY=RS>V~2%yrE6d z+yQ$s)*}7M`q2HQzQ zK~#9!?ORQ2)LayPa`QQfq%)zVRw+WeP*nT@eyr=J3#nT-E?fz&{1f^gir~hAxarEh zx)6jS2o*u0P(?>FjWv_Ww-WZC^XJb~ z0EnUps;Yur0|2EIw{G34eE!v|R}zkqD5aJ2q|PEy6rs^*Kv9&;eMyq|yk%MZw`Ey4 zDWue4YBU-EfG`Xp%W@&lyLaz0G?M1#<|vDok|aTrB9)A7$h3)Naq-hGz^Kj?R9nQzBM3!Z!stQ5~;y51rJc2OOkK>q!yQQTitgNg+ z*L4_%fk%%X;memVkR%D`&Yi=D4blJIqy?~QvN!9h@x zB&2ByP1A7m=1sI(E%?5VR;z`%xjC4oiPx`R0|0K{zKw6+zTx}#@6a?2mSv&S>EO+q zH|Y2K+#N&^EXpI|IL7w&_OXJXG(SI2<2Z(*D7bRv3a(wdhA<4_x-NX*$L#DZ2q9>< z+nEQO%_f2%fbaXTEDMg~K-YDA{P+={K79gPGfmS>9>K<@X^w{i?%lgb0l;w_xUP$t znHhv(h#&~i>2$d7I*!AEjDcDZ1c;&ttyT+;<8U8mJMOwJ0KoVCfsdu@*RR8}EIfby z{CE|RzwzYBlgtyVtE-v$&CSiq7k=>I0sZ~^cj4Uh^fcbTe_#1R8yg#y>)*Y5m$J=p zU6)%c+rYPP-%2B|2ke)kC@TCJ1u^0C*N6HsG_K^jZf5s~QOKdhqr;5v!wOo80n5wF z6aYNW!_?Fi78VvV3}3%~<#PZs^RDY+YilbrzHs3JWLbu7+nAl5#m}EV0RVsg`~d*$ z?CcCg6H#F-Q)FSYDEf#=TYmoR*|VAPix)4XJuSfVyrKPZYikR>@AEPHR$v0NYj&SS z1x7P7GnMJF$ZJ^^48!2cVYtG0hN(qz7hpTQUlLzX+(9OIk|d?V=lS#ZlO%yy`Scs0hO_(ChUw{@v^KU|ANr-R?mB zu}Bd`_nXaTJp~L4fO+JtgTh5Tb&DiS5oLlgOQ<9GzA<1Jh3q9{nevL(sIr8z`?VCn z;p$ScBLKB+Men=Yk7rb%fMyBek z>pH}~fcS;#x-MS6d^!4p$T(3|UtpMOF*ykWi?#oJMJhi&bqc7WQ7B{VU0{8EeE<{0 zeqh~Wd#BO@_QHfyservO;eTfV2gQJq=K+^4UCK1%@87?lnO|L9&HT;H&CSewzW@K@ z$B)deudlC<^qt9R^XU_>fV>&!eg4!dV3bMD$T47fc{y`d;6K{e`GRJ%nKA8+jg1UH zYnLZUf+&gxO56GV|JvG`bmSH=%y|O#Op#yvzR&vsV)vJ|)!2Mh*Z9a3Fiab5KhH3s z#bdjS4&$G(#ts9-PVuNtmL2-HKmjh)2DXFt|JT;m#=`3T;jsO4v07*qo IM6N<$g1vu_`v3p{ literal 0 HcmV?d00001 diff --git a/src/hdskins/resources/hdskins.mixin.json b/src/hdskins/resources/hdskins.mixin.json new file mode 100644 index 00000000..6ea1f2f3 --- /dev/null +++ b/src/hdskins/resources/hdskins.mixin.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.7", + "package": "com.minelittlepony.hdskins.mixin", + "refmap": "hdskins.mixin.refmap.json", + "mixins": [ + "MixinMinecraft", + "MixinGuiMainMenu", + "MixinImageBufferDownload", + "MixinNetworkPlayerInfo", + "MixinSkullRenderer", + "MixinThreadDownloadImageData" + ] +} diff --git a/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/LiteModHDSkins.java b/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/LiteModHDSkins.java new file mode 100644 index 00000000..3196cc18 --- /dev/null +++ b/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/LiteModHDSkins.java @@ -0,0 +1,105 @@ +package com.minelittlepony.hdskins.litemod; + +import com.google.gson.GsonBuilder; +import com.minelittlepony.common.client.gui.GuiLiteHost; +import com.minelittlepony.hdskins.HDSkinManager; +import com.minelittlepony.hdskins.HDSkins; +import com.minelittlepony.hdskins.gui.HDSkinsConfigPanel; +import com.minelittlepony.hdskins.server.SkinServer; +import com.minelittlepony.hdskins.server.SkinServerSerializer; +import com.mumfrey.liteloader.Configurable; +import com.mumfrey.liteloader.InitCompleteListener; +import com.mumfrey.liteloader.ViewportListener; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.modconfig.AdvancedExposable; +import com.mumfrey.liteloader.modconfig.ConfigPanel; +import com.mumfrey.liteloader.modconfig.ConfigStrategy; +import com.mumfrey.liteloader.modconfig.ExposableOptions; +import com.mumfrey.liteloader.util.ModUtilities; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; + +import java.io.File; +import java.util.function.Function; + +@ExposableOptions(strategy = ConfigStrategy.Unversioned, filename = "hdskins") +public class LiteModHDSkins extends HDSkins implements InitCompleteListener, ViewportListener, Configurable, AdvancedExposable { + + @Override + public String getName() { + return HDSkins.MOD_NAME; + } + + @Override + public String getVersion() { + return HDSkins.VERSION; + } + + @Override + public void saveConfig() { + LiteLoader.getInstance().writeConfig(this); + } + + @Override + public void init(File configPath) { + + // register config + LiteLoader.getInstance().registerExposable(this, null); + super.init(); + } + + @Override + public void upgradeSettings(String version, File configPath, File oldConfigPath) { + HDSkinManager.INSTANCE.clearSkinCache(); + } + + @Override + public void setupGsonSerialiser(GsonBuilder gsonBuilder) { + gsonBuilder.registerTypeAdapter(SkinServer.class, new SkinServerSerializer()); + } + + @Override + public File getConfigFile(File configFile, File configFileLocation, String defaultFileName) { + return null; + } + + @Override + public Class getConfigPanelClass() { + return Panel.class; + } + + @Override + public void onInitCompleted(Minecraft minecraft, LiteLoader loader) { + initComplete(); + } + + @Override + public void onViewportResized(ScaledResolution resolution, int displayWidth, int displayHeight) { + + } + + @Override + public void onFullScreenToggled(boolean fullScreen) { + super.onToggledFullScreen(fullScreen); + } + + @Override + protected void addRenderer(Class type, Function> renderer) { + ModUtilities.addRenderer(type, renderer.apply(Minecraft.getMinecraft().getRenderManager())); + } + + @Override + public File getAssetsDirectory() { + return LiteLoader.getAssetsDirectory(); + } + + public static class Panel extends GuiLiteHost { + public Panel() { + super(new HDSkinsConfigPanel()); + } + } +} diff --git a/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/package-info.java b/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/package-info.java new file mode 100644 index 00000000..167f1f93 --- /dev/null +++ b/src/hdskinslitemod/java/com/minelittlepony/hdskins/litemod/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package com.minelittlepony.hdskins.litemod; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault;