172 lines
7.3 KiB
Java
172 lines
7.3 KiB
Java
package net.minecraft.client.resources;
|
|
|
|
import com.google.common.cache.CacheBuilder;
|
|
import com.google.common.cache.CacheLoader;
|
|
import com.google.common.cache.LoadingCache;
|
|
import com.google.common.hash.Hashing;
|
|
import com.mojang.authlib.GameProfile;
|
|
import com.mojang.authlib.SignatureState;
|
|
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
|
|
import com.mojang.authlib.minecraft.MinecraftProfileTextures;
|
|
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
|
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
|
|
import com.mojang.authlib.properties.Property;
|
|
import com.mojang.logging.LogUtils;
|
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
|
import java.nio.file.Path;
|
|
import java.time.Duration;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.function.Supplier;
|
|
import net.fabricmc.api.EnvType;
|
|
import net.fabricmc.api.Environment;
|
|
import net.minecraft.Optionull;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.client.renderer.texture.SkinTextureDownloader;
|
|
import net.minecraft.client.resources.PlayerSkin.Model;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
public class SkinManager {
|
|
static final Logger LOGGER = LogUtils.getLogger();
|
|
private final MinecraftSessionService sessionService;
|
|
private final LoadingCache<SkinManager.CacheKey, CompletableFuture<Optional<PlayerSkin>>> skinCache;
|
|
private final SkinManager.TextureCache skinTextures;
|
|
private final SkinManager.TextureCache capeTextures;
|
|
private final SkinManager.TextureCache elytraTextures;
|
|
|
|
public SkinManager(Path skinDirectory, MinecraftSessionService sessionService, Executor executor) {
|
|
this.sessionService = sessionService;
|
|
this.skinTextures = new SkinManager.TextureCache(skinDirectory, Type.SKIN);
|
|
this.capeTextures = new SkinManager.TextureCache(skinDirectory, Type.CAPE);
|
|
this.elytraTextures = new SkinManager.TextureCache(skinDirectory, Type.ELYTRA);
|
|
this.skinCache = CacheBuilder.newBuilder()
|
|
.expireAfterAccess(Duration.ofSeconds(15L))
|
|
.build(
|
|
new CacheLoader<SkinManager.CacheKey, CompletableFuture<Optional<PlayerSkin>>>() {
|
|
public CompletableFuture<Optional<PlayerSkin>> load(SkinManager.CacheKey cacheKey) {
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
Property property = cacheKey.packedTextures();
|
|
if (property == null) {
|
|
return MinecraftProfileTextures.EMPTY;
|
|
} else {
|
|
MinecraftProfileTextures minecraftProfileTextures = sessionService.unpackTextures(property);
|
|
if (minecraftProfileTextures.signatureState() == SignatureState.INVALID) {
|
|
SkinManager.LOGGER.warn("Profile contained invalid signature for textures property (profile id: {})", cacheKey.profileId());
|
|
}
|
|
|
|
return minecraftProfileTextures;
|
|
}
|
|
}, Util.backgroundExecutor().forName("unpackSkinTextures"))
|
|
.thenComposeAsync(minecraftProfileTextures -> SkinManager.this.registerTextures(cacheKey.profileId(), minecraftProfileTextures), executor)
|
|
.handle((playerSkin, throwable) -> {
|
|
if (throwable != null) {
|
|
SkinManager.LOGGER.warn("Failed to load texture for profile {}", cacheKey.profileId, throwable);
|
|
}
|
|
|
|
return Optional.ofNullable(playerSkin);
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
public Supplier<PlayerSkin> lookupInsecure(GameProfile profile) {
|
|
CompletableFuture<Optional<PlayerSkin>> completableFuture = this.getOrLoad(profile);
|
|
PlayerSkin playerSkin = DefaultPlayerSkin.get(profile);
|
|
return () -> (PlayerSkin)((Optional)completableFuture.getNow(Optional.empty())).orElse(playerSkin);
|
|
}
|
|
|
|
public PlayerSkin getInsecureSkin(GameProfile profile) {
|
|
PlayerSkin playerSkin = (PlayerSkin)((Optional)this.getOrLoad(profile).getNow(Optional.empty())).orElse(null);
|
|
return playerSkin != null ? playerSkin : DefaultPlayerSkin.get(profile);
|
|
}
|
|
|
|
public CompletableFuture<Optional<PlayerSkin>> getOrLoad(GameProfile profile) {
|
|
Property property = this.sessionService.getPackedTextures(profile);
|
|
return this.skinCache.getUnchecked(new SkinManager.CacheKey(profile.getId(), property));
|
|
}
|
|
|
|
CompletableFuture<PlayerSkin> registerTextures(UUID uuid, MinecraftProfileTextures textures) {
|
|
MinecraftProfileTexture minecraftProfileTexture = textures.skin();
|
|
CompletableFuture<ResourceLocation> completableFuture;
|
|
Model model;
|
|
if (minecraftProfileTexture != null) {
|
|
completableFuture = this.skinTextures.getOrLoad(minecraftProfileTexture);
|
|
model = Model.byName(minecraftProfileTexture.getMetadata("model"));
|
|
} else {
|
|
PlayerSkin playerSkin = DefaultPlayerSkin.get(uuid);
|
|
completableFuture = CompletableFuture.completedFuture(playerSkin.texture());
|
|
model = playerSkin.model();
|
|
}
|
|
|
|
String string = Optionull.map(minecraftProfileTexture, MinecraftProfileTexture::getUrl);
|
|
MinecraftProfileTexture minecraftProfileTexture2 = textures.cape();
|
|
CompletableFuture<ResourceLocation> completableFuture2 = minecraftProfileTexture2 != null
|
|
? this.capeTextures.getOrLoad(minecraftProfileTexture2)
|
|
: CompletableFuture.completedFuture(null);
|
|
MinecraftProfileTexture minecraftProfileTexture3 = textures.elytra();
|
|
CompletableFuture<ResourceLocation> completableFuture3 = minecraftProfileTexture3 != null
|
|
? this.elytraTextures.getOrLoad(minecraftProfileTexture3)
|
|
: CompletableFuture.completedFuture(null);
|
|
return CompletableFuture.allOf(completableFuture, completableFuture2, completableFuture3)
|
|
.thenApply(
|
|
void_ -> new PlayerSkin(
|
|
(ResourceLocation)completableFuture.join(),
|
|
string,
|
|
(ResourceLocation)completableFuture2.join(),
|
|
(ResourceLocation)completableFuture3.join(),
|
|
model,
|
|
textures.signatureState() == SignatureState.SIGNED
|
|
)
|
|
);
|
|
}
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
record CacheKey(UUID profileId, @Nullable Property packedTextures) {
|
|
}
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
static class TextureCache {
|
|
private final Path root;
|
|
private final Type type;
|
|
private final Map<String, CompletableFuture<ResourceLocation>> textures = new Object2ObjectOpenHashMap<>();
|
|
|
|
TextureCache(Path root, Type type) {
|
|
this.root = root;
|
|
this.type = type;
|
|
}
|
|
|
|
public CompletableFuture<ResourceLocation> getOrLoad(MinecraftProfileTexture texture) {
|
|
String string = texture.getHash();
|
|
CompletableFuture<ResourceLocation> completableFuture = (CompletableFuture<ResourceLocation>)this.textures.get(string);
|
|
if (completableFuture == null) {
|
|
completableFuture = this.registerTexture(texture);
|
|
this.textures.put(string, completableFuture);
|
|
}
|
|
|
|
return completableFuture;
|
|
}
|
|
|
|
private CompletableFuture<ResourceLocation> registerTexture(MinecraftProfileTexture texture) {
|
|
String string = Hashing.sha1().hashUnencodedChars(texture.getHash()).toString();
|
|
ResourceLocation resourceLocation = this.getTextureLocation(string);
|
|
Path path = this.root.resolve(string.length() > 2 ? string.substring(0, 2) : "xx").resolve(string);
|
|
return SkinTextureDownloader.downloadAndRegisterSkin(resourceLocation, path, texture.getUrl(), this.type == Type.SKIN);
|
|
}
|
|
|
|
private ResourceLocation getTextureLocation(String name) {
|
|
String string = switch (this.type) {
|
|
case SKIN -> "skins";
|
|
case CAPE -> "capes";
|
|
case ELYTRA -> "elytra";
|
|
};
|
|
return ResourceLocation.withDefaultNamespace(string + "/" + name);
|
|
}
|
|
}
|
|
}
|