minecraft-src/net/minecraft/client/resources/SkinManager.java
2025-07-04 03:15:13 +03:00

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