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>> 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>>() { public CompletableFuture> 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 lookupInsecure(GameProfile profile) { CompletableFuture> 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> getOrLoad(GameProfile profile) { Property property = this.sessionService.getPackedTextures(profile); return this.skinCache.getUnchecked(new SkinManager.CacheKey(profile.getId(), property)); } CompletableFuture registerTextures(UUID uuid, MinecraftProfileTextures textures) { MinecraftProfileTexture minecraftProfileTexture = textures.skin(); CompletableFuture 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 completableFuture2 = minecraftProfileTexture2 != null ? this.capeTextures.getOrLoad(minecraftProfileTexture2) : CompletableFuture.completedFuture(null); MinecraftProfileTexture minecraftProfileTexture3 = textures.elytra(); CompletableFuture 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> textures = new Object2ObjectOpenHashMap<>(); TextureCache(Path root, Type type) { this.root = root; this.type = type; } public CompletableFuture getOrLoad(MinecraftProfileTexture texture) { String string = texture.getHash(); CompletableFuture completableFuture = (CompletableFuture)this.textures.get(string); if (completableFuture == null) { completableFuture = this.registerTexture(texture); this.textures.put(string, completableFuture); } return completableFuture; } private CompletableFuture 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); } } }