Slightly rewrite texture loading so it is better adaptable.

Also exposes metadata more
This commit is contained in:
Matthew Messinger 2018-08-24 21:55:45 -04:00
parent 72324feaf3
commit 981cd002b3
8 changed files with 183 additions and 193 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) {
ResourceLocation skin = this.resources.getPlayerTexture(profile1, type);
if (skin != null) {
return Optional.of(skin);
}
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);
}
}
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);
return skins.getUnchecked(profile);
}
private void loadTexture(GameProfile profile, final Type type, final SkinAvailableCallback callback) {
if (profile.getId() == null) {
return;
}
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, () -> {
callback.skinAvailable(type, resource, texture);
});
// 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));
}
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);
//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(),
new ImageBufferDownloadHD(type, () -> {
if (callback != null) {
callback.skinAvailable(type, resource, texture);
}
})));
}
return textures;
return resource;
}
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);
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 textures;
return map;
}
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

@ -4,65 +4,92 @@ 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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Mixin(NetworkPlayerInfo.class)
public abstract class MixinPlayerInfo {
public abstract class MixinPlayerInfo implements INetworkPlayerInfo {
@Shadow
public abstract GameProfile getGameProfile();
@Inject(
method = "getLocationSkin",
cancellable = true,
at = @At("RETURN"))
private void getLocationSkin(CallbackInfoReturnable<ResourceLocation> ci) {
getTextureLocation(ci, Type.SKIN);
private Map<Type, ResourceLocation> customTextures = new HashMap<>();
private Map<Type, MinecraftProfileTexture> customProfiles = new HashMap<>();
@Shadow @Final private GameProfile gameProfile;
@Shadow public abstract String getSkinType();
@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);
@Inject(method = "getSkinType", at = @At("RETURN"), cancellable = true)
private void getTextureModel(CallbackInfoReturnable<String> cir) {
getProfileTexture(Type.SKIN).ifPresent(profile -> {
String model = profile.getMetadata("model");
cir.setReturnValue(model != null ? model : "default");
});
}
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();
this.customTextures.values().forEach(tm::deleteTexture);
this.customTextures.clear();
this.customProfiles.clear();
}
}

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,
@Nullable GameProfile profile, int p_180543_8_, float ticks) {
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
bindTexture(rl);
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,41 +1,42 @@
package com.minelittlepony.mixin;
import com.minelittlepony.MineLittlePony;
import com.minelittlepony.PonyManager;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.voxelmodpack.hdskins.INetworkPlayerInfo;
import net.minecraft.client.network.NetworkPlayerInfo;
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 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;
@Mixin(value = NetworkPlayerInfo.class, priority = 999)
public abstract class MixinNetworkPlayerInfo implements INetworkPlayerInfo {
import net.minecraft.client.network.NetworkPlayerInfo;
@Shadow private String skinType;
@Mixin(NetworkPlayerInfo.class)
public abstract class MixinNetworkPlayerInfo implements IPlayerInfo {
@Shadow
private String skinType;
@Shadow @Final private GameProfile gameProfile;
@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()));
info.setReturnValue(MineLittlePony.getInstance().getManager()
.getPony((NetworkPlayerInfo) (Object) this)
.getRace(false)
.getModel()
.getId(usesSlimArms()));
}
@Override
public boolean usesSlimArms() {
private 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 getProfileTexture(Type.SKIN)
.map(profile -> profile.getMetadata("model"))
.filter("slim"::equals)
.isPresent() || PonyManager.isSlimSkin(this.gameProfile.getId());
return PonyManager.isSlimSkin(unwrap().getGameProfile().getId());
}
return "slim".equals(skinType);

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();