Merge pull request #74 from MineLittlePony/valhalla

Valhalla Implementation
This commit is contained in:
Matthew Messinger 2018-07-14 10:54:30 -04:00 committed by GitHub
commit 8e663c81d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 564 additions and 177 deletions

View file

@ -1,3 +1,11 @@
language: java
install: true
jdk:
- oraclejdk8
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/

View file

@ -13,11 +13,13 @@ buildscript {
dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT'
classpath 'org.spongepowered:mixingradle:0.6-SNAPSHOT'
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
}
}
apply plugin: 'net.minecraftforge.gradle.liteloader'
apply plugin: 'org.spongepowered.mixin'
apply plugin: 'com.github.johnrengelman.shadow'
group = 'com.minelittlepony'
version = '1.12.2.2-SNAPSHOT'
@ -44,6 +46,13 @@ sourceSets {
}
}
dependencies {
// use the same version as httpclient
compile('org.apache.httpcomponents:httpmime:4.3.2') {
transitive = false
}
}
litemod {
doFirst {
json {
@ -56,7 +65,6 @@ litemod {
description.hdskinsmod = '''\
Separate skin server for Mine Little Pony that also supports HD skins.
Access via button on the main menu.'''
mixinConfigs += [
'minelp.mixin.json',
'hdskins.mixin.json'
@ -68,6 +76,24 @@ litemod {
jar {
from sourceSets.hdskins.output
from litemod
classifier 'base'
extension 'jar'
}
shadowJar {
extension 'litemod'
classifier "mc$minecraft.version"
baseName "mod-${project.name.toLowerCase()}"
from sourceSets.hdskins.output
from litemod
dependencies {
exclude dependency('deobf.com.mumfrey:liteloader:')
exclude dependency('deobf.org.ow2.asm:')
exclude 'META-INF/**'
}
relocate 'org.apache.http.entity.mime', 'com.voxelmodpack.repack.org.apache.http.entity.mime'
exclude 'dummyThing'
}
sourceJar {
// add hdskins sources
@ -86,10 +112,16 @@ task skinZip(type: Zip) {
version 'v1'
}
artifacts {
archives shadowJar
}
reobf {
srgJar {
mappingType = 'SEARGE'
}
shadowJar {}
}
mixin {
defaultObfuscationEnv notch

View file

@ -1,8 +1,11 @@
package com.voxelmodpack.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.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@ -21,7 +24,10 @@ import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.voxelmodpack.hdskins.gui.GuiSkins;
import com.voxelmodpack.hdskins.resource.SkinResourceManager;
import com.voxelmodpack.hdskins.skins.AsyncCacheLoader;
import com.voxelmodpack.hdskins.skins.LegacySkinServer;
import com.voxelmodpack.hdskins.skins.ServerType;
import com.voxelmodpack.hdskins.skins.SkinServer;
import com.voxelmodpack.hdskins.skins.ValhallaSkinServer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.IImageBuffer;
import net.minecraft.client.renderer.texture.ITextureObject;
@ -31,13 +37,15 @@ import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.client.resources.SkinManager.SkinAvailableCallback;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
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.Date;
import java.util.List;
@ -50,7 +58,6 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class HDSkinManager implements IResourceManagerReloadListener {
@ -68,6 +75,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
private List<ISkinCacheClearListener> clearListeners = Lists.newArrayList();
private BiMap<String, Class<? extends SkinServer>> skinServerTypes = HashBiMap.create(2);
private List<SkinServer> skinServers = Lists.newArrayList();
private Map<UUID, Map<Type, ResourceLocation>> skinCache = Maps.newHashMap();
@ -85,6 +93,13 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
private Class<? extends GuiSkins> skinsClass = null;
private HDSkinManager() {
// register default skin server types
addSkinServerType(LegacySkinServer.class);
addSkinServerType(ValhallaSkinServer.class);
}
public void setPrefferedSkinsGuiClass(Class<? extends GuiSkins> clazz) {
skinsClass = clazz;
}
@ -102,35 +117,42 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
}
public Optional<ResourceLocation> getSkinLocation(GameProfile profile1, final Type type, boolean loadIfAbsent) {
if (!enabled)
if (!enabled) {
return Optional.empty();
}
ResourceLocation skin = this.resources.getPlayerTexture(profile1, type);
if (skin != null)
if (skin != null) {
return Optional.of(skin);
}
// try to recreate a broken gameprofile
// happens when server sends a random profile with skin and displayname
Property textures = Iterables.getFirst(profile1.getProperties().get("textures"), null);
if (textures != null) {
MinecraftTexturesPayload texturePayload = GSON.fromJson(new String(Base64.decodeBase64(textures.getValue())), MinecraftTexturesPayload.class);
String json = new String(Base64.getDecoder().decode(textures.getValue()), StandardCharsets.UTF_8);
MinecraftTexturesPayload texturePayload = GSON.fromJson(json, MinecraftTexturesPayload.class);
if (texturePayload != null) {
// name is optional
String name = texturePayload.getProfileName();
UUID uuid = texturePayload.getProfileId();
// uuid is required
if (uuid != null)
if (uuid != null) {
profile1 = new GameProfile(uuid, name);
}
// probably uses this texture for a reason. Don't mess with it.
if (!texturePayload.getTextures().isEmpty() && texturePayload.getProfileId() == null)
if (!texturePayload.getTextures().isEmpty() && texturePayload.getProfileId() == null) {
return Optional.empty();
}
}
}
final GameProfile profile = profile1;
// cannot get texture without id!
if (profile.getId() == null) return Optional.empty();
if (profile.getId() == null) {
return Optional.empty();
}
if (!this.skinCache.containsKey(profile.getId())) {
this.skinCache.put(profile.getId(), Maps.newHashMap());
@ -194,8 +216,9 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
for (SkinServer server : skinServers) {
Optional<MinecraftTexturesPayload> profileData = server.loadProfileData(profile);
profileData.map(MinecraftTexturesPayload::getTextures).ifPresent(it -> it.forEach(textures::putIfAbsent));
if (textures.size() == Type.values().length)
if (textures.size() == Type.values().length) {
break;
}
}
return textures;
@ -211,10 +234,25 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
return textures;
}
public void addSkinServer(SkinServer skinServer) {
this.skinServers.add(0, skinServer);
public void addSkinServerType(Class<? extends SkinServer> 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<? extends SkinServer> getSkinServerClass(String type) {
return this.skinServerTypes.get(type);
}
public void addSkinServer(SkinServer skinServer) {
this.skinServers.add(skinServer);
}
@Deprecated
public SkinServer getGatewayServer() {
return this.skinServers.get(0);
}
@ -223,32 +261,8 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
this.enabled = enabled;
}
@Nullable
public static PreviewTexture getPreviewTexture(ResourceLocation skinResource, GameProfile profile, Type type, ResourceLocation def, @Nullable final SkinAvailableCallback callback) {
TextureManager textureManager = Minecraft.getMinecraft().getTextureManager();
MinecraftProfileTexture url = INSTANCE.getGatewayServer().getPreviewTexture(type, profile).orElse(null);
if (url == null)
return null;
IImageBuffer buffer = new ImageBufferDownloadHD();
PreviewTexture skinTexture = new PreviewTexture(url.getMetadata("model"), url.getUrl(), def, type == Type.SKIN ? new IImageBuffer() {
@Override
@Nullable
public BufferedImage parseUserSkin(BufferedImage image) {
return buffer.parseUserSkin(image);
}
@Override
public void skinAvailable() {
if (callback != null) {
callback.skinAvailable(type, skinResource, new MinecraftProfileTexture(url.getUrl(), Maps.newHashMap()));
}
}
} : null);
textureManager.loadTexture(skinResource, skinTexture);
return skinTexture;
public static PreviewTextureManager getPreviewTextureManager(GameProfile profile) {
return new PreviewTextureManager(INSTANCE.getGatewayServer().getPreviewTextures(profile));
}
public void addClearListener(ISkinCacheClearListener listener) {

View file

@ -12,10 +12,10 @@ public class PreviewTexture extends ThreadDownloadImageData {
private String model;
public PreviewTexture(String model, String url, ResourceLocation fallbackTexture, @Nullable IImageBuffer imageBuffer) {
public PreviewTexture(@Nullable String model, String url, ResourceLocation fallbackTexture, @Nullable IImageBuffer imageBuffer) {
super(null, url, fallbackTexture, imageBuffer);
this.model = model;
this.model = model == null ? "default" : model;
}
public boolean isTextureUploaded() {

View file

@ -0,0 +1,54 @@
package com.voxelmodpack.hdskins;
import com.google.common.collect.Maps;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.IImageBuffer;
import net.minecraft.client.resources.SkinManager;
import net.minecraft.util.ResourceLocation;
import java.awt.image.BufferedImage;
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<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures;
PreviewTextureManager(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures) {
this.textures = textures;
}
@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);
IImageBuffer buffer = new ImageBufferDownloadHD();
PreviewTexture skinTexture = new PreviewTexture(texture.getMetadata("model"), texture.getUrl(), def,
type == MinecraftProfileTexture.Type.SKIN ? new IImageBuffer() {
@Override
@Nullable
public BufferedImage parseUserSkin(BufferedImage image) {
return buffer.parseUserSkin(image);
}
@Override
public void skinAvailable() {
if (callback != null) {
callback.skinAvailable(type, location, new MinecraftProfileTexture(texture.getUrl(), Maps.newHashMap()));
}
}
} : null);
Minecraft.getMinecraft().getTextureManager().loadTexture(location, skinTexture);
return skinTexture;
}
}

View file

@ -8,6 +8,7 @@ import com.voxelmodpack.hdskins.DynamicTextureImage;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.ImageBufferDownloadHD;
import com.voxelmodpack.hdskins.PreviewTexture;
import com.voxelmodpack.hdskins.PreviewTextureManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
@ -76,8 +77,10 @@ public class EntityPlayerModel extends EntityLivingBase {
this.textureManager.deleteTexture(this.remoteElytraResource);
}
this.remoteSkinTexture = HDSkinManager.getPreviewTexture(this.remoteSkinResource, this.profile, Type.SKIN, getBlankSkin(), listener);
this.remoteElytraTexture = HDSkinManager.getPreviewTexture(this.remoteElytraResource, this.profile, Type.ELYTRA, getBlankElytra(), null);
PreviewTextureManager ptm = HDSkinManager.getPreviewTextureManager(this.profile);
this.remoteSkinTexture = ptm.getPreviewTexture(this.remoteSkinResource, Type.SKIN, getBlankSkin(), listener);
this.remoteElytraTexture = ptm.getPreviewTexture(this.remoteElytraResource, Type.ELYTRA, getBlankElytra(), null);
}

View file

@ -4,15 +4,15 @@ import static com.mojang.authlib.minecraft.MinecraftProfileTexture.Type.ELYTRA;
import static com.mojang.authlib.minecraft.MinecraftProfileTexture.Type.SKIN;
import static net.minecraft.client.renderer.GlStateManager.*;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.skins.SkinServer;
import com.voxelmodpack.hdskins.skins.SkinUploadResponse;
import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiButton;
@ -37,13 +37,17 @@ import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.DoubleBuffer;
import java.nio.file.Path;
import java.util.Map;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.JFileChooser;
import javax.swing.UIManager;
public class GuiSkins extends GuiScreen {
public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResponse> {
private static final int MAX_SKIN_DIMENSION = 1024;
private int updateCounter = 0;
@ -55,6 +59,8 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
private GuiButton btnModeSkinnySkin;
private GuiButton btnModeElytra;
private GuiButton btnAbout;
protected EntityPlayerModel localPlayer;
protected EntityPlayerModel remotePlayer;
@ -192,8 +198,10 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
this.buttonList.add(this.btnModeSkin = new GuiItemStackButton(4, 2, 2, skin));
skin = new ItemStack(Items.LEATHER_LEGGINGS);
Items.LEATHER_LEGGINGS.setColor(skin, 0xfff500);
this.buttonList.add(this.btnModeSkinnySkin = new GuiItemStackButton(6, 2, 21, skin));
this.buttonList.add(this.btnModeElytra = new GuiItemStackButton(5, 2, 52, new ItemStack(Items.ELYTRA)));
this.buttonList.add(this.btnModeSkinnySkin = new GuiItemStackButton(6, 2, 21, skin));
this.buttonList.add(this.btnAbout = new GuiButton(-1, this.width - 25, this.height - 25, 20, 20, "?"));
this.btnUpload.enabled = false;
this.btnBrowse.enabled = !this.mc.isFullScreen();
@ -201,6 +209,7 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
this.btnModeSkin.enabled = this.thinArmType;
this.btnModeSkinnySkin.enabled = !this.thinArmType;
this.btnModeElytra.enabled = this.textureType == SKIN;
}
private void enableDnd() {
@ -439,6 +448,10 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
}
this.drawHoveringText(I18n.format(text), mouseX, y);
}
if (this.btnAbout.isMouseOver()) {
SkinServer gateway = HDSkinManager.INSTANCE.getGatewayServer();
this.drawHoveringText(Splitter.on("\r\n").splitToList(gateway.toString()), mouseX, mouseY);
}
if (this.fetchingSkin) {
String opacity1;
@ -490,7 +503,8 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
enableDepth();
}
private void renderPlayerModel(EntityPlayerModel thePlayer, float xPosition, float yPosition, float scale, float mouseY, float mouseX, float partialTick) {
private void renderPlayerModel(EntityPlayerModel thePlayer, float xPosition, float yPosition, float scale, float mouseY, float mouseX,
float partialTick) {
enableColorMaterial();
pushMatrix();
translate(xPosition, yPosition, 300.0F);
@ -551,38 +565,44 @@ public class GuiSkins extends GuiScreen implements FutureCallback<SkinUploadResp
private void clearUploadedSkin(Session session) {
this.uploadingSkin = true;
this.skinUploadMessage = I18n.format("hdskins.request");
Futures.addCallback(HDSkinManager.INSTANCE.getGatewayServer().uploadSkin(session, null, this.textureType, this.thinArmType), this);
HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, null, this.textureType, getMetadata())
.thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure);
}
private void uploadSkin(Session session, @Nullable File skinFile) {
this.uploadingSkin = true;
this.skinUploadMessage = I18n.format("hdskins.upload");
Path path = skinFile == null ? null : skinFile.toPath();
Futures.addCallback(HDSkinManager.INSTANCE.getGatewayServer().uploadSkin(session, path, this.textureType, this.thinArmType), this);
URI path = skinFile == null ? null : skinFile.toURI();
HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, path, this.textureType, getMetadata())
.thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure);
}
private Map<String, String> getMetadata() {
return ImmutableMap.of("model", this.thinArmType ? "slim" : "default");
}
private void setUploadError(@Nullable String error) {
this.uploadError = error != null && error.startsWith("ERROR: ") ? error.substring(7) : error;
this.uploadError = error;
this.btnUpload.enabled = true;
}
@Override
public void onSuccess(@Nullable SkinUploadResponse result) {
if (result != null)
onUploadComplete(result);
}
@Override
public void onFailure(Throwable t) {
private Void onFailure(Throwable t) {
LogManager.getLogger().warn("Upload failed", t);
this.setUploadError(t.toString());
this.uploadingSkin = false;
return null;
}
private void onUploadComplete(SkinUploadResponse response) {
LiteLoaderLogger.info("Upload completed with: %s", response);
this.uploadingSkin = false;
if (!"OK".equalsIgnoreCase(response.getMessage())) {
if (!response.isSuccess()) {
this.setUploadError(response.getMessage());
} else {
this.pendingRemoteSkinRefresh = true;

View file

@ -1,7 +1,9 @@
package com.voxelmodpack.hdskins.mod;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
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;
@ -9,26 +11,25 @@ import com.mumfrey.liteloader.util.ModUtilities;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.gui.EntityPlayerModel;
import com.voxelmodpack.hdskins.gui.GLWindow;
import com.voxelmodpack.hdskins.gui.GuiSkins;
import com.voxelmodpack.hdskins.gui.HDSkinsConfigPanel;
import com.voxelmodpack.hdskins.gui.RenderPlayerModel;
import com.voxelmodpack.hdskins.skins.SkinServer;
import com.voxelmodpack.hdskins.skins.SkinServerSerializer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.resources.IReloadableResourceManager;
import java.io.File;
import java.lang.reflect.Method;
import java.util.List;
@ExposableOptions(strategy = ConfigStrategy.Unversioned, filename = "hdskins")
public class LiteModHDSkinsMod implements HDSkinsMod {
public class LiteModHDSkinsMod implements HDSkinsMod, AdvancedExposable {
@Expose
public List<String> skin_servers = SkinServer.defaultServers;
public List<SkinServer> skin_servers = SkinServer.defaultServers;
@Expose
public boolean experimentalSkinDrop = true;
public boolean experimentalSkinDrop = false;
@Override
public String getName() {
@ -46,17 +47,6 @@ public class LiteModHDSkinsMod implements HDSkinsMod {
// register config
LiteLoader.getInstance().registerExposable(this, null);
// try it initialize voxelmenu button
try {
Class<?> ex = Class.forName("com.thevoxelbox.voxelmenu.GuiMainMenuVoxelBox");
Method mRegisterCustomScreen = ex.getDeclaredMethod("registerCustomScreen", Class.class, String.class);
mRegisterCustomScreen.invoke(null, GuiSkins.class, "HD Skins Manager");
} catch (ClassNotFoundException var4) {
// voxelmenu's not here, man
} catch (Exception var5) {
var5.printStackTrace();
}
IReloadableResourceManager irrm = (IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager();
irrm.registerReloadListener(HDSkinManager.INSTANCE);
@ -70,6 +60,16 @@ public class LiteModHDSkinsMod implements HDSkinsMod {
HDSkinManager.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<? extends ConfigPanel> getConfigPanelClass() {
return HDSkinsConfigPanel.class;
@ -80,14 +80,7 @@ public class LiteModHDSkinsMod implements HDSkinsMod {
ModUtilities.addRenderer(EntityPlayerModel.class, new RenderPlayerModel<>(minecraft.getRenderManager()));
// register skin servers.
for (String s : skin_servers) {
try {
HDSkinManager.INSTANCE.addSkinServer(SkinServer.from(s));
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
skin_servers.forEach(HDSkinManager.INSTANCE::addSkinServer);
}
@Override

View file

@ -1,7 +0,0 @@
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package com.voxelmodpack.hdskins.mod;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View file

@ -0,0 +1,26 @@
package com.voxelmodpack.hdskins.skins;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
public class CallableFutures {
public static <T> CompletableFuture<T> asyncFailableFuture(Callable<T> call, Executor exec) {
CompletableFuture<T> ret = new CompletableFuture<>();
exec.execute(() -> {
try {
ret.complete(call.call());
} catch (Throwable e) {
ret.completeExceptionally(e);
}
});
return ret;
}
public static <T> CompletableFuture<T> failedFuture(Exception e) {
CompletableFuture<T> ret = new CompletableFuture<>();
ret.completeExceptionally(e);
return ret;
}
}

View file

@ -0,0 +1,18 @@
package com.voxelmodpack.hdskins.skins;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.builder.ToStringStyle;
public class IndentedToStringStyle extends ToStringStyle {
public static final ToStringStyle INSTANCE = new IndentedToStringStyle();
private IndentedToStringStyle() {
this.setContentStart(null);
this.setFieldSeparator(SystemUtils.LINE_SEPARATOR + " ");
this.setFieldSeparatorAtStart(true);
this.setContentEnd(null);
this.setUseIdentityHashCode(false);
this.setUseShortClassName(true);
}
}

View file

@ -1,10 +1,8 @@
package com.voxelmodpack.hdskins.skins;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.annotations.Expose;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
@ -15,43 +13,50 @@ import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.upload.ThreadMultipartPostUpload;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Session;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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) {
this(address, null);
}
public LegacySkinServer(String address, @Nullable String gateway) {
this.address = address;
this.gateway = gateway;
}
@Override
public Optional<MinecraftProfileTexture> getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile) {
if (Strings.isNullOrEmpty(this.gateway))
return Optional.empty();
return Optional.of(new MinecraftProfileTexture(getPath(this.gateway, type, profile), null));
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getPreviewTextures(GameProfile profile) {
if (Strings.isNullOrEmpty(this.gateway)) {
return Collections.emptyMap();
}
Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = new EnumMap<>(MinecraftProfileTexture.Type.class);
for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) {
map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null));
}
return map;
}
@Override
@ -88,19 +93,25 @@ public class LegacySkinServer implements SkinServer {
}
@Override
public ListenableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type, boolean thinSkinType) {
public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image,
MinecraftProfileTexture.Type type, Map<String, String> metadata) {
if (Strings.isNullOrEmpty(this.gateway))
return Futures.immediateFailedFuture(new NullPointerException("gateway url is blank"));
if (Strings.isNullOrEmpty(this.gateway)) {
return CallableFutures.failedFuture(new NullPointerException("gateway url is blank"));
}
return HDSkinManager.skinUploadExecutor.submit(() -> {
return CallableFutures.asyncFailableFuture(() -> {
verifyServerConnection(session, SERVER_ID);
Map<String, ?> data = image == null ? getClearData(session, type) : getUploadData(session, type, (thinSkinType ? "slim" : "default"), image);
String model = metadata.getOrDefault("model", "default");
Map<String, ?> data = image == null ? getClearData(session, type) : getUploadData(session, type, model, image);
ThreadMultipartPostUpload upload = new ThreadMultipartPostUpload(this.gateway, data);
String response = upload.uploadMultipart();
if (response.startsWith("ERROR: ")) {
response = response.substring(7);
}
return new SkinUploadResponse(response.equalsIgnoreCase("OK"), response);
});
}, HDSkinManager.skinUploadExecutor);
}
private static Map<String, ?> getData(Session session, MinecraftProfileTexture.Type type, String model, String param, Object val) {
@ -116,7 +127,7 @@ public class LegacySkinServer implements SkinServer {
return getData(session, type, "default", "clear", "1");
}
private static Map<String, ?> getUploadData(Session session, MinecraftProfileTexture.Type type, String model, Path skinFile) {
private static Map<String, ?> getUploadData(Session session, MinecraftProfileTexture.Type type, String model, URI skinFile) {
return getData(session, type, model, type.toString().toLowerCase(Locale.US), skinFile);
}
@ -131,24 +142,11 @@ public class LegacySkinServer implements SkinServer {
service.joinServer(session.getProfile(), session.getToken(), serverId);
}
/**
* Should be in the format {@code legacy:http://address;http://gateway}. Gateway is optional.
*/
static LegacySkinServer from(String parsed) {
Matcher matcher = Pattern.compile("^legacy:(.+?)(?:;(.*))?$").matcher(parsed);
if (matcher.find()) {
String addr = matcher.group(1);
String gate = matcher.group(2);
return new LegacySkinServer(addr, gate);
}
throw new IllegalArgumentException("server format string was not correct");
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("address", address)
.add("gateway", gateway)
.toString();
return new ToStringBuilder(this, IndentedToStringStyle.INSTANCE)
.append("address", this.address)
.append("gateway", this.gateway)
.build();
}
}

View file

@ -0,0 +1,13 @@
package com.voxelmodpack.hdskins.skins;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerType {
String value();
}

View file

@ -1,39 +1,33 @@
package com.voxelmodpack.hdskins.skins;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import net.minecraft.util.Session;
import java.nio.file.Path;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
public interface SkinServer {
List<String> defaultServers = Lists.newArrayList("legacy:http://skins.voxelmodpack.com;http://skinmanager.voxelmodpack.com");
List<SkinServer> defaultServers = Lists.newArrayList(new LegacySkinServer(
"http://skins.voxelmodpack.com",
"http://skinmanager.voxelmodpack.com"));
Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile);
Optional<MinecraftProfileTexture> getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile);
default Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getPreviewTextures(GameProfile profile) {
return loadProfileData(profile).map(MinecraftTexturesPayload::getTextures).orElse(Collections.emptyMap());
}
ListenableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type, boolean thinArmType);
CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image,
MinecraftProfileTexture.Type type, Map<String, String> metadata);
static SkinServer from(String server) {
int i = server.indexOf(':');
if (i >= 0) {
String type = server.substring(0, i);
switch (type) {
case "legacy":
return LegacySkinServer.from(server);
case "valhalla": {
return ValhallaSkinServer.from(server);
}
}
}
throw new IllegalArgumentException();
}
}

View file

@ -0,0 +1,34 @@
package com.voxelmodpack.hdskins.skins;
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.voxelmodpack.hdskins.HDSkinManager;
import java.lang.reflect.Type;
public class SkinServerSerializer implements JsonSerializer<SkinServer>, JsonDeserializer<SkinServer> {
@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();
Class<? extends SkinServer> clas = HDSkinManager.INSTANCE.getSkinServerClass(type);
return context.deserialize(json, clas);
}
}

View file

@ -2,8 +2,6 @@ package com.voxelmodpack.hdskins.skins;
import com.google.common.base.MoreObjects;
import javax.annotation.Nullable;
public class SkinUploadResponse {
private final boolean success;
@ -18,7 +16,6 @@ public class SkinUploadResponse {
return success;
}
@Nullable
public String getMessage() {
return message;
}

View file

@ -1,45 +1,231 @@
package com.voxelmodpack.hdskins.skins;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.annotations.Expose;
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 com.voxelmodpack.hdskins.HDSkinManager;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Session;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
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 org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import java.nio.file.Path;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
@ServerType("valhalla")
public class ValhallaSkinServer implements SkinServer {
@SuppressWarnings("unused")
private final String baseURL;
@Expose
private final String address;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
.create();
public ValhallaSkinServer(String baseURL) {
this.baseURL = baseURL;
private transient String accessToken;
public ValhallaSkinServer(String address) {
this.address = address;
}
@Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) {
try (CloseableHttpClient client = HttpClients.createSystem();
CloseableHttpResponse response = client.execute(new HttpGet(getTexturesURI(profile)))) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return Optional.of(readJson(response.getEntity().getContent(), MinecraftTexturesPayload.class));
}
} catch (IOException e) {
e.printStackTrace();
}
return Optional.empty();
}
@Override
public Optional<MinecraftProfileTexture> getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile) {
return null;
public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image,
MinecraftProfileTexture.Type type, Map<String, String> metadata) {
return CallableFutures.asyncFailableFuture(() -> {
try (CloseableHttpClient client = HttpClients.createSystem()) {
authorize(client, session);
GameProfile profile = session.getProfile();
if (image == null) {
return resetSkin(client, profile, type);
}
switch (image.getScheme()) {
case "file":
return uploadFile(client, new File(image), profile, type, metadata);
case "http":
case "https":
return uploadUrl(client, image, profile, type, metadata);
default:
throw new IOException("Unsupported URI scheme: " + image.getScheme());
}
}
}, HDSkinManager.skinUploadExecutor);
}
private SkinUploadResponse resetSkin(CloseableHttpClient client, GameProfile profile, MinecraftProfileTexture.Type type) throws IOException {
return upload(client, RequestBuilder.delete()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.build());
}
private SkinUploadResponse uploadFile(CloseableHttpClient client, File file, GameProfile profile, MinecraftProfileTexture.Type type,
Map<String, String> metadata) throws IOException {
MultipartEntityBuilder b = MultipartEntityBuilder.create();
b.addBinaryBody("file", file, ContentType.create("image/png"), file.getName());
metadata.forEach(b::addTextBody);
return upload(client, RequestBuilder.put()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.setEntity(b.build())
.build());
}
private SkinUploadResponse uploadUrl(CloseableHttpClient client, URI uri, GameProfile profile, MinecraftProfileTexture.Type type,
Map<String, String> metadata) throws IOException {
return upload(client, RequestBuilder.post()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.addParameter("file", uri.toString())
.addParameters(metadata.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.toArray(NameValuePair[]::new))
.build());
}
private SkinUploadResponse upload(CloseableHttpClient client, HttpUriRequest request) throws IOException {
try (CloseableHttpResponse response = client.execute(request)) {
int code = response.getStatusLine().getStatusCode();
JsonObject json = readJson(response.getEntity().getContent(), JsonObject.class);
return new SkinUploadResponse(code == HttpStatus.SC_OK, json.get("message").getAsString());
}
}
private void authorize(CloseableHttpClient client, Session session) throws IOException, AuthenticationException {
if (accessToken != null) {
return;
}
GameProfile profile = session.getProfile();
String token = session.getToken();
AuthHandshake handshake = authHandshake(client, profile.getName());
if (handshake.offline) {
return;
}
// join the session server
Minecraft.getMinecraft().getSessionService().joinServer(profile, token, handshake.serverId);
AuthResponse response = authResponse(client, 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 <T> T readJson(InputStream in, Class<T> cl) throws IOException {
try (Reader r = new InputStreamReader(in)) {
return gson.fromJson(r, cl);
}
}
private AuthHandshake authHandshake(CloseableHttpClient client, String name) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
.setUri(getHandshakeURI())
.addParameter("name", name)
.build())) {
return readJson(resp.getEntity().getContent(), AuthHandshake.class);
}
}
private AuthResponse authResponse(CloseableHttpClient client, String name, long verifyToken) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
.setUri(getResponseURI())
.addParameter("name", name)
.addParameter("verifyToken", String.valueOf(verifyToken))
.build())) {
return readJson(resp.getEntity().getContent(), 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 ListenableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type, boolean thinArmType) {
return null;
public String toString() {
return new ToStringBuilder(this, IndentedToStringStyle.INSTANCE)
.append("address", this.address)
.toString();
}
static ValhallaSkinServer from(String server) {
Matcher matcher = Pattern.compile("^valhalla:(.*)$").matcher(server);
if (matcher.find())
return new ValhallaSkinServer(matcher.group(1));
throw new IllegalArgumentException();
@SuppressWarnings("WeakerAccess")
static class AuthHandshake {
boolean offline;
String serverId;
long verifyToken;
}
@SuppressWarnings("WeakerAccess")
static class AuthResponse {
String accessToken;
UUID userId;
}
}

View file

@ -18,7 +18,9 @@ import javax.annotation.Nullable;
* Uploader for Multipart form data
*
* @author Adam Mummery-Smith
* @deprecated Use httpmime multipart upload
*/
@Deprecated
public class ThreadMultipartPostUpload {
protected final Map<String, ?> sourceData;

View file

@ -7,6 +7,7 @@ import com.minelittlepony.pony.data.PonyDataSerialzier;
import com.minelittlepony.render.PonySkullRenderer;
import com.mumfrey.liteloader.core.LiteLoader;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.skins.LegacySkinServer;
import com.voxelmodpack.hdskins.skins.SkinServer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.entity.RenderManager;
@ -27,7 +28,8 @@ public class MineLittlePony {
public static final String MOD_NAME = "Mine Little Pony";
public static final String MOD_VERSION = "@VERSION@";
private static final String MINELP_LEGACY_SERVER = "legacy:http://minelpskins.voxelmodpack.com;http://minelpskinmanager.voxelmodpack.com";
private static final String MINELP_LEGACY_SERVER = "http://minelpskins.voxelmodpack.com";
private static final String MINELP_LEGACY_GATEWAY = "http://minelpskinmanager.voxelmodpack.com";
private static final KeyBinding SETTINGS_GUI = new KeyBinding("Settings", Keyboard.KEY_F9, "Mine Little Pony");
@ -57,7 +59,7 @@ public class MineLittlePony {
ms.registerMetadataSectionType(new PonyDataSerialzier(), IPonyData.class);
// This also makes it the default gateway server.
SkinServer.defaultServers.add(MINELP_LEGACY_SERVER);
SkinServer.defaultServers.add(new LegacySkinServer(MINELP_LEGACY_SERVER, MINELP_LEGACY_GATEWAY));
}
/**