This commit is contained in:
Sollace 2018-08-26 03:13:22 +02:00
commit 23f01e0f12
9 changed files with 192 additions and 215 deletions

View file

@ -19,18 +19,19 @@ import com.mumfrey.liteloader.core.LiteLoader;
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.BethlehemSkinServer;
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.texture.TextureManager;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.network.NetworkPlayerInfo;
import net.minecraft.client.renderer.texture.ITextureObject;
import net.minecraft.client.resources.DefaultPlayerSkin;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.client.resources.SkinManager.SkinAvailableCallback;
import net.minecraft.client.resources.SkinManager;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.FileUtils;
import org.apache.http.impl.client.CloseableHttpClient;
@ -46,15 +47,17 @@ 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.Optional;
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.Collectors;
import javax.annotation.Nullable;
public final class HDSkinManager implements IResourceManagerReloadListener {
@ -64,24 +67,16 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
public static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8);
public static final CloseableHttpClient httpClient = HttpClients.createSystem();
private static final ResourceLocation LOADING = new ResourceLocation("LOADING");
public static final HDSkinManager INSTANCE = new HDSkinManager();
private boolean enabled = true;
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();
private LoadingCache<GameProfile, Map<Type, MinecraftProfileTexture>> skins = CacheBuilder.newBuilder()
.initialCapacity(20)
.maximumSize(100)
.expireAfterWrite(4, TimeUnit.HOURS)
.build(AsyncCacheLoader.create(CacheLoader.from(this::loadProfileData), Collections.emptyMap(), skinDownloadExecutor));
private LoadingCache<GameProfile, CompletableFuture<Map<Type, MinecraftProfileTexture>>> skins = CacheBuilder.newBuilder()
.expireAfterAccess(15, TimeUnit.SECONDS)
.build(CacheLoader.from(this::loadProfileData));
private List<ISkinModifier> skinModifiers = Lists.newArrayList();
@ -107,19 +102,30 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
return skinsGuiFunc.apply(ImmutableList.copyOf(this.skinServers));
}
public Optional<ResourceLocation> getSkinLocation(GameProfile profile1, final Type type, boolean loadIfAbsent) {
if (!enabled) {
return Optional.empty();
private CompletableFuture<Map<Type, MinecraftProfileTexture>> loadProfileData(GameProfile profile) {
return CompletableFuture.supplyAsync(() -> {
Map<Type, MinecraftProfileTexture> 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);
}
ResourceLocation skin = this.resources.getPlayerTexture(profile1, type);
if (skin != null) {
return Optional.of(skin);
}
return textureMap;
}, skinDownloadExecutor);
}
public CompletableFuture<Map<Type, MinecraftProfileTexture>> loadProfileTextures(GameProfile profile) {
// 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);
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);
@ -129,86 +135,55 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
UUID uuid = texturePayload.getProfileId();
// uuid is required
if (uuid != null) {
profile1 = new GameProfile(uuid, name);
profile = new GameProfile(uuid, name);
}
// probably uses this texture for a reason. Don't mess with it.
if (!texturePayload.getTextures().isEmpty() && texturePayload.getProfileId() == null) {
return Optional.empty();
return CompletableFuture.completedFuture(Collections.emptyMap());
}
}
}
final GameProfile profile = profile1;
// cannot get texture without id!
if (profile.getId() == null) {
return Optional.empty();
}
if (!this.skinCache.containsKey(profile.getId())) {
this.skinCache.put(profile.getId(), Maps.newHashMap());
}
skin = this.skinCache.get(profile.getId()).get(type);
if (skin == null) {
if (loadIfAbsent && getProfileData(profile).containsKey(type)) {
skinCache.get(profile.getId()).put(type, LOADING);
loadTexture(profile, type, (t, loc, tex) -> skinCache.get(profile.getId()).put(t, loc));
}
return Optional.empty();
}
return skin == LOADING ? Optional.empty() : Optional.of(skin);
}
private void loadTexture(GameProfile profile, final Type type, final SkinAvailableCallback callback) {
if (profile.getId() == null) {
return;
return skins.getUnchecked(profile);
}
public ResourceLocation loadTexture(Type type, MinecraftProfileTexture texture, @Nullable SkinManager.SkinAvailableCallback callback) {
String skinDir = type.toString().toLowerCase() + "s/";
final MinecraftProfileTexture texture = getProfileData(profile).get(type);
final ResourceLocation resource = new ResourceLocation("hdskins", skinDir + texture.getHash());
ITextureObject texObj = Minecraft.getMinecraft().getTextureManager().getTexture(resource);
ISkinAvailableCallback buffs = new ImageBufferDownloadHD(type, () -> {
//noinspection ConstantConditions
if (texObj != null) {
if (callback != null) {
callback.skinAvailable(type, resource, texture);
});
}
} else {
// schedule texture loading on the main thread.
TextureLoader.loadTexture(resource, new ThreadDownloadImageETag(
new File(LiteLoader.getAssetsDirectory(), "hd/" + skinDir + texture.getHash().substring(0, 2) + "/" + texture.getHash()),
texture.getUrl(),
DefaultPlayerSkin.getDefaultSkinLegacy(),
buffs));
new ImageBufferDownloadHD(type, () -> {
if (callback != null) {
callback.skinAvailable(type, resource, texture);
}
})));
}
return resource;
}
private Map<Type, MinecraftProfileTexture> loadProfileData(GameProfile profile) {
Map<Type, MinecraftProfileTexture> textures = Maps.newEnumMap(Type.class);
for (SkinServer server : skinServers) {
try {
server.loadProfileData(profile).getTextures().forEach(textures::putIfAbsent);
if (textures.size() == Type.values().length) {
break;
}
} catch (IOException e) {
logger.trace(e);
public Map<Type, ResourceLocation> getTextures(GameProfile profile) {
Map<Type, ResourceLocation> map = new HashMap<>();
for (Map.Entry<Type, MinecraftProfileTexture> e : loadProfileTextures(profile).getNow(Collections.emptyMap()).entrySet()) {
map.put(e.getKey(), loadTexture(e.getKey(), e.getValue(), null));
}
return map;
}
return textures;
}
public Map<Type, MinecraftProfileTexture> getProfileData(GameProfile profile) {
boolean was = !skins.asMap().containsKey(profile);
Map<Type, MinecraftProfileTexture> textures = skins.getUnchecked(profile);
// This is the initial value. Refreshing will load it asynchronously.
if (was) {
skins.refresh(profile);
}
return textures;
}
public void addSkinServerType(Class<? extends SkinServer> type) {
private 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);
@ -222,14 +197,10 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
return this.skinServerTypes.get(type);
}
public void addSkinServer(SkinServer skinServer) {
void addSkinServer(SkinServer skinServer) {
this.skinServers.add(skinServer);
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void addClearListener(ISkinCacheClearListener listener) {
clearListeners.add(listener);
}
@ -237,18 +208,14 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
public void clearSkinCache() {
LiteLoaderLogger.info("Clearing local player skin cache");
try {
FileUtils.deleteDirectory(new File(LiteLoader.getAssetsDirectory(), "skins"));
FileUtils.deleteDirectory(new File(LiteLoader.getAssetsDirectory(), "hd"));
} catch (IOException e) {
e.printStackTrace();
FileUtils.deleteQuietly(new File(LiteLoader.getAssetsDirectory(), "hd"));
NetHandlerPlayClient connection = Minecraft.getMinecraft().getConnection();
if (connection != null) {
connection.getPlayerInfoMap().forEach(this::clearNetworkSkin);
}
TextureManager textures = Minecraft.getMinecraft().getTextureManager();
skinCache.values().stream()
.flatMap(m -> m.values().stream())
.forEach(textures::deleteTexture);
skinCache.clear();
skins.invalidateAll();
clearListeners = clearListeners.stream()
@ -256,6 +223,10 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
.collect(Collectors.toList());
}
private void clearNetworkSkin(NetworkPlayerInfo player) {
((INetworkPlayerInfo) player).deleteTextures();
}
private boolean onSkinCacheCleared(ISkinCacheClearListener callback) {
try {
return callback.onSkinCacheCleared();

View file

@ -0,0 +1,15 @@
package com.voxelmodpack.hdskins;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.util.ResourceLocation;
import java.util.Optional;
public interface INetworkPlayerInfo {
Optional<ResourceLocation> getResourceLocation(MinecraftProfileTexture.Type type);
Optional<MinecraftProfileTexture> getProfileTexture(MinecraftProfileTexture.Type type);
void deleteTextures();
}

View file

@ -26,7 +26,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
@ -193,7 +192,8 @@ public class ThreadDownloadImageETag extends SimpleTexture {
Files.createDirectories(cacheFile.getParent());
Files.copy(resp.getInputStream(), cacheFile);
BufferedImage bufferedimage = ImageIO.read(Files.newInputStream(cacheFile));
try (InputStream in = Files.newInputStream(cacheFile)) {
BufferedImage bufferedimage = ImageIO.read(in);
// maybe write the etag to disk
Header eTag = resp.getResponse().getFirstHeader(HttpHeaders.ETAG);
@ -206,6 +206,7 @@ public class ThreadDownloadImageETag extends SimpleTexture {
}
setBufferedImage(bufferedimage);
}
}
} catch (Exception exception) {
LOGGER.error("Couldn\'t download http texture", exception);
}

View file

@ -4,65 +4,100 @@ import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.INetworkPlayerInfo;
import net.minecraft.client.Minecraft;
import net.minecraft.client.network.NetworkPlayerInfo;
import net.minecraft.client.renderer.texture.TextureManager;
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.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
@Mixin(NetworkPlayerInfo.class)
public abstract class MixinPlayerInfo {
public abstract class MixinPlayerInfo implements INetworkPlayerInfo {
@Shadow
public abstract GameProfile getGameProfile();
private Map<Type, ResourceLocation> customTextures = new HashMap<>();
private Map<Type, MinecraftProfileTexture> customProfiles = new HashMap<>();
@Inject(
method = "getLocationSkin",
cancellable = true,
at = @At("RETURN"))
private void getLocationSkin(CallbackInfoReturnable<ResourceLocation> ci) {
getTextureLocation(ci, Type.SKIN);
@Shadow @Final private GameProfile gameProfile;
@Shadow Map<Type, ResourceLocation> playerTextures;
@Shadow private boolean playerTexturesLoaded;
@Shadow private String skinType;
@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;"))
// synthetic
private Object getSkin(Map<Type, ResourceLocation> playerTextures, Object key) {
return getSkin(playerTextures, (Type) key);
}
@Inject(
method = "getLocationCape",
cancellable = true,
at = @At("RETURN"))
private void getLocationCape(CallbackInfoReturnable<ResourceLocation> ci) {
getTextureLocation(ci, Type.CAPE);
// with generics
private ResourceLocation getSkin(Map<Type, ResourceLocation> playerTextures, Type type) {
return getResourceLocation(type).orElseGet(() -> playerTextures.get(type));
}
@Inject(
method = "getLocationElytra",
cancellable = true,
at = @At("RETURN"))
private void getLocationElytra(CallbackInfoReturnable<ResourceLocation> ci) {
getTextureLocation(ci, Type.ELYTRA);
@Redirect(method = "getSkinType()Ljava/lang/String;",
at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/NetworkPlayerInfo;skinType:Ljava/lang/String;"))
private String getTextureModel(NetworkPlayerInfo self) {
return getProfileTexture(Type.SKIN).map(profile -> {
String model = profile.getMetadata("model");
return model != null ? model : "default";
}).orElse(this.skinType);
}
private void getTextureLocation(CallbackInfoReturnable<ResourceLocation> ci, Type type) {
Optional<ResourceLocation> texture = HDSkinManager.INSTANCE.getSkinLocation(getGameProfile(), type, true);
texture.ifPresent(ci::setReturnValue);
@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.loadProfileTextures(this.gameProfile)
.thenAcceptAsync(m -> m.forEach((type, profile) -> {
HDSkinManager.INSTANCE.loadTexture(type, profile, (typeIn, location, profileTexture) -> {
customTextures.put(type, location);
customProfiles.put(type, profileTexture);
});
}), Minecraft.getMinecraft()::addScheduledTask);
}
@Inject(
method = "getSkinType",
cancellable = true,
at = @At("RETURN"))
private void getSkinType(CallbackInfoReturnable<String> ci) {
MinecraftProfileTexture skin = HDSkinManager.INSTANCE.getProfileData(getGameProfile()).get(Type.SKIN);
if (skin != null) {
String type = skin.getMetadata("model");
if (type == null)
type = "default";
String type1 = type;
Optional<ResourceLocation> texture = HDSkinManager.INSTANCE.getSkinLocation(getGameProfile(), Type.SKIN, false);
@Override
public Optional<ResourceLocation> getResourceLocation(Type type) {
return Optional.ofNullable(this.customTextures.get(type));
}
texture.ifPresent((res) -> ci.setReturnValue(type1));
}
@Override
public Optional<MinecraftProfileTexture> getProfileTexture(Type type) {
return Optional.ofNullable(this.customProfiles.get(type));
}
@Override
public void deleteTextures() {
TextureManager tm = Minecraft.getMinecraft().getTextureManager();
Stream.concat(this.customTextures.values().stream(), this.playerTextures.values().stream())
.forEach(tm::deleteTexture);
this.customTextures.clear();
this.customProfiles.clear();
this.playerTextures.clear();
this.skinType = null;
this.playerTexturesLoaded = false;
}
}

View file

@ -13,7 +13,6 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import javax.annotation.Nullable;
import java.util.Optional;
@Mixin(TileEntitySkullRenderer.class)
public abstract class MixinSkullRenderer extends TileEntitySpecialRenderer<TileEntitySkull> {
@ -24,16 +23,15 @@ public abstract class MixinSkullRenderer extends TileEntitySpecialRenderer<TileE
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,
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) {
Optional<ResourceLocation> skin = HDSkinManager.INSTANCE.getSkinLocation(profile, Type.SKIN, true);
if (skin.isPresent())
// rebind
bindTexture(skin.get());
else
bindTexture(rl);
} else
ResourceLocation skin = HDSkinManager.INSTANCE.getTextures(profile).get(Type.SKIN);
if (skin != null) {
rl = skin;
}
}
bindTexture(rl);
}
}

View file

@ -6,9 +6,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.minelittlepony.pony.data.Pony;
import com.minelittlepony.pony.data.PonyLevel;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.ISkinCacheClearListener;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.AbstractClientPlayer;
import net.minecraft.client.network.NetworkPlayerInfo;
@ -83,8 +81,6 @@ public class PonyManager implements IResourceManagerReloadListener, ISkinCacheCl
}
public Pony getPony(NetworkPlayerInfo playerInfo) {
// force load HDSkins if they're not available
HDSkinManager.INSTANCE.getProfileData(playerInfo.getGameProfile());
ResourceLocation skin = playerInfo.getLocationSkin();
UUID uuid = playerInfo.getGameProfile().getId();

View file

@ -1,17 +0,0 @@
package com.minelittlepony.ducks;
import net.minecraft.client.network.NetworkPlayerInfo;
public interface IPlayerInfo {
/**
* Returns true if the vanilla skin (the one returned by NetworkPlayerInfo.getSkinLocation) uses the ALEX model type.
*/
boolean usesSlimArms();
/**
* Quick cast back to the original type.
*/
default NetworkPlayerInfo unwrap() {
return (NetworkPlayerInfo)this;
}
}

View file

@ -1,43 +1,22 @@
package com.minelittlepony.mixin;
import com.minelittlepony.MineLittlePony;
import com.voxelmodpack.hdskins.INetworkPlayerInfo;
import net.minecraft.client.network.NetworkPlayerInfo;
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.callback.CallbackInfoReturnable;
import com.minelittlepony.MineLittlePony;
import com.minelittlepony.PonyManager;
import com.minelittlepony.ducks.IPlayerInfo;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.voxelmodpack.hdskins.HDSkinManager;
import net.minecraft.client.network.NetworkPlayerInfo;
@Mixin(NetworkPlayerInfo.class)
public abstract class MixinNetworkPlayerInfo implements IPlayerInfo {
@Shadow
private String skinType;
public abstract class MixinNetworkPlayerInfo implements INetworkPlayerInfo {
@Inject(method = "getSkinType()Ljava/lang/String;", at = @At("RETURN"), cancellable = true)
private void getSkinType(CallbackInfoReturnable<String> info) {
info.setReturnValue(MineLittlePony.getInstance().getManager().getPony(unwrap()).getRace(false).getModel().getId(usesSlimArms()));
}
@Override
public boolean usesSlimArms() {
if (skinType == null) {
MinecraftProfileTexture skin = HDSkinManager.INSTANCE.getProfileData(unwrap().getGameProfile()).get(Type.SKIN);
if (skin != null) {
return "slim".equals(skin.getMetadata("model"));
}
return PonyManager.isSlimSkin(unwrap().getGameProfile().getId());
}
return "slim".equals(skinType);
info.setReturnValue(MineLittlePony.getInstance().getManager()
.getPony((NetworkPlayerInfo) (Object) this)
.getRace(false)
.getModel()
.getId("slim".equals(info.getReturnValue())));
}
}

View file

@ -34,7 +34,6 @@ import net.minecraft.util.EnumHandSide;
import net.minecraft.util.ResourceLocation;
import java.util.Map;
import java.util.Optional;
public class RenderPonyPlayer extends RenderPlayer implements IRenderPony<AbstractClientPlayer> {
@ -58,9 +57,9 @@ public class RenderPonyPlayer extends RenderPlayer implements IRenderPony<Abstra
if (profile != null) {
deadMau5.setVisible("deadmau5".equals(profile.getName()));
Optional<ResourceLocation> skin = HDSkinManager.INSTANCE.getSkinLocation(profile, Type.SKIN, true);
if (skin.isPresent()) {
return skin.get();
ResourceLocation skin = HDSkinManager.INSTANCE.getTextures(profile).get(Type.SKIN);
if (skin != null) {
return skin;
}
Minecraft minecraft = Minecraft.getMinecraft();