176 lines
		
	
	
	
		
			7.5 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			176 lines
		
	
	
	
		
			7.5 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.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 = this.getInsecureSkin(profile, null);
 | |
| 		return playerSkin != null ? playerSkin : DefaultPlayerSkin.get(profile);
 | |
| 	}
 | |
| 
 | |
| 	@Nullable
 | |
| 	public PlayerSkin getInsecureSkin(GameProfile profile, @Nullable PlayerSkin defaultSkin) {
 | |
| 		return (PlayerSkin)((Optional)this.getOrLoad(profile).getNow(Optional.empty())).orElse(defaultSkin);
 | |
| 	}
 | |
| 
 | |
| 	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;
 | |
| 		PlayerSkin.Model model;
 | |
| 		if (minecraftProfileTexture != null) {
 | |
| 			completableFuture = this.skinTextures.getOrLoad(minecraftProfileTexture);
 | |
| 			model = PlayerSkin.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);
 | |
| 		}
 | |
| 	}
 | |
| }
 |