/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.collect.Lists * com.google.gson.Gson * com.google.gson.GsonBuilder * com.google.gson.JsonElement * com.google.gson.JsonParseException * com.mojang.datafixers.kinds.App * com.mojang.datafixers.kinds.Applicative * com.mojang.datafixers.util.Either * com.mojang.datafixers.util.Pair * com.mojang.logging.LogUtils * com.mojang.serialization.Codec * com.mojang.serialization.DynamicOps * com.mojang.serialization.JsonOps * com.mojang.serialization.codecs.RecordCodecBuilder * it.unimi.dsi.fastutil.ints.IntCollection * it.unimi.dsi.fastutil.ints.IntOpenHashSet * org.jspecify.annotations.Nullable * org.slf4j.Logger */ package net.minecraft.client.gui.font; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.mojang.blaze3d.font.GlyphProvider; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Applicative; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.ints.IntCollection; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import java.io.BufferedReader; import java.io.Reader; import java.lang.runtime.SwitchBootstraps; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; import net.minecraft.client.Minecraft; import net.minecraft.client.Options; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GlyphSource; import net.minecraft.client.gui.font.AllMissingGlyphProvider; import net.minecraft.client.gui.font.AtlasGlyphProvider; import net.minecraft.client.gui.font.FontOption; import net.minecraft.client.gui.font.FontSet; import net.minecraft.client.gui.font.GlyphStitcher; import net.minecraft.client.gui.font.PlayerGlyphProvider; import net.minecraft.client.gui.font.glyphs.EffectGlyph; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; import net.minecraft.client.renderer.PlayerSkinRenderCache; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.resources.model.AtlasManager; import net.minecraft.network.chat.FontDescription; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.DependencySorter; import net.minecraft.util.Util; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class FontManager implements AutoCloseable, PreparableReloadListener { private static final Logger LOGGER = LogUtils.getLogger(); private static final String FONTS_PATH = "fonts.json"; public static final Identifier MISSING_FONT = Identifier.withDefaultNamespace("missing"); private static final FileToIdConverter FONT_DEFINITIONS = FileToIdConverter.json("font"); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); private final FontSet missingFontSet; private final List providersToClose = new ArrayList(); private final Map fontSets = new HashMap(); private final TextureManager textureManager; private final CachedFontProvider anyGlyphs = new CachedFontProvider(false); private final CachedFontProvider nonFishyGlyphs = new CachedFontProvider(true); private final AtlasManager atlasManager; private final Map atlasProviders = new HashMap(); private final PlayerGlyphProvider playerProvider; public FontManager(TextureManager textureManager, AtlasManager atlasManager, PlayerSkinRenderCache playerSkinRenderCache) { this.textureManager = textureManager; this.atlasManager = atlasManager; this.missingFontSet = this.createFontSet(MISSING_FONT, List.of(FontManager.createFallbackProvider()), Set.of()); this.playerProvider = new PlayerGlyphProvider(playerSkinRenderCache); } private FontSet createFontSet(Identifier id, List providers, Set options) { GlyphStitcher stitcher = new GlyphStitcher(this.textureManager, id); FontSet result = new FontSet(stitcher); result.reload(providers, options); return result; } private static GlyphProvider.Conditional createFallbackProvider() { return new GlyphProvider.Conditional(new AllMissingGlyphProvider(), FontOption.Filter.ALWAYS_PASS); } @Override public CompletableFuture reload(PreparableReloadListener.SharedState currentReload, Executor taskExecutor, PreparableReloadListener.PreparationBarrier preparationBarrier, Executor reloadExecutor) { return ((CompletableFuture)this.prepare(currentReload.resourceManager(), taskExecutor).thenCompose(preparationBarrier::wait)).thenAcceptAsync(preparations -> this.apply((Preparation)preparations, Profiler.get()), reloadExecutor); } private CompletableFuture prepare(ResourceManager manager, Executor executor) { ArrayList> builderFutures = new ArrayList>(); for (Map.Entry> fontStack : FONT_DEFINITIONS.listMatchingResourceStacks(manager).entrySet()) { Identifier fontName = FONT_DEFINITIONS.fileToId(fontStack.getKey()); builderFutures.add(CompletableFuture.supplyAsync(() -> { List> builderStack = FontManager.loadResourceStack((List)fontStack.getValue(), fontName); UnresolvedBuilderBundle bundle = new UnresolvedBuilderBundle(fontName); for (Pair stackEntry : builderStack) { BuilderId id = (BuilderId)stackEntry.getFirst(); FontOption.Filter options = ((GlyphProviderDefinition.Conditional)stackEntry.getSecond()).filter(); ((GlyphProviderDefinition.Conditional)stackEntry.getSecond()).definition().unpack().ifLeft(provider -> { CompletableFuture> loadResult = this.safeLoad(id, (GlyphProviderDefinition.Loader)provider, manager, executor); bundle.add(id, options, loadResult); }).ifRight(reference -> bundle.add(id, options, (GlyphProviderDefinition.Reference)reference)); } return bundle; }, executor)); } return Util.sequence(builderFutures).thenCompose(builders -> { List allProviderFutures = builders.stream().flatMap(UnresolvedBuilderBundle::listBuilders).collect(Util.toMutableList()); GlyphProvider.Conditional fallback = FontManager.createFallbackProvider(); allProviderFutures.add(CompletableFuture.completedFuture(Optional.of(fallback.provider()))); return Util.sequence(allProviderFutures).thenCompose(allProviders -> { Map> resolved = this.resolveProviders((List)builders); CompletableFuture[] finalizers = (CompletableFuture[])resolved.values().stream().map(providers -> CompletableFuture.runAsync(() -> this.finalizeProviderLoading((List)providers, fallback), executor)).toArray(CompletableFuture[]::new); return CompletableFuture.allOf(finalizers).thenApply(ignored -> { List providersToClose = allProviders.stream().flatMap(Optional::stream).toList(); return new Preparation(resolved, providersToClose); }); }); }); } private CompletableFuture> safeLoad(BuilderId id, GlyphProviderDefinition.Loader provider, ResourceManager manager, Executor executor) { return CompletableFuture.supplyAsync(() -> { try { return Optional.of(provider.load(manager)); } catch (Exception e) { LOGGER.warn("Failed to load builder {}, rejecting", (Object)id, (Object)e); return Optional.empty(); } }, executor); } private Map> resolveProviders(List unresolvedProviders) { HashMap> result = new HashMap>(); DependencySorter sorter = new DependencySorter(); unresolvedProviders.forEach(e -> sorter.addEntry(e.fontId, (UnresolvedBuilderBundle)e)); sorter.orderByDependencies((id, bundle) -> bundle.resolve(result::get).ifPresent(r -> result.put((Identifier)id, (List)r))); return result; } private void finalizeProviderLoading(List list, GlyphProvider.Conditional fallback) { list.add(0, fallback); IntOpenHashSet supportedGlyphs = new IntOpenHashSet(); for (GlyphProvider.Conditional provider : list) { supportedGlyphs.addAll((IntCollection)provider.provider().getSupportedGlyphs()); } supportedGlyphs.forEach(codepoint -> { GlyphProvider.Conditional provider; if (codepoint == 32) { return; } Iterator iterator = Lists.reverse((List)list).iterator(); while (iterator.hasNext() && (provider = (GlyphProvider.Conditional)iterator.next()).provider().getGlyph(codepoint) == null) { } }); } private static Set getFontOptions(Options options) { EnumSet result = EnumSet.noneOf(FontOption.class); if (options.forceUnicodeFont().get().booleanValue()) { result.add(FontOption.UNIFORM); } if (options.japaneseGlyphVariants().get().booleanValue()) { result.add(FontOption.JAPANESE_VARIANTS); } return result; } private void apply(Preparation preparations, ProfilerFiller profiler) { profiler.push("closing"); this.anyGlyphs.invalidate(); this.nonFishyGlyphs.invalidate(); this.fontSets.values().forEach(FontSet::close); this.fontSets.clear(); this.providersToClose.forEach(GlyphProvider::close); this.providersToClose.clear(); Set fontOptions = FontManager.getFontOptions(Minecraft.getInstance().options); profiler.popPush("reloading"); preparations.fontSets().forEach((id, newProviders) -> this.fontSets.put((Identifier)id, this.createFontSet((Identifier)id, Lists.reverse((List)newProviders), fontOptions))); this.providersToClose.addAll(preparations.allProviders); profiler.pop(); if (!this.fontSets.containsKey(Minecraft.DEFAULT_FONT)) { throw new IllegalStateException("Default font failed to load"); } this.atlasProviders.clear(); this.atlasManager.forEach((atlasId, atlasTexture) -> this.atlasProviders.put((Identifier)atlasId, new AtlasGlyphProvider((TextureAtlas)atlasTexture))); } public void updateOptions(Options options) { Set fontOptions = FontManager.getFontOptions(options); for (FontSet value : this.fontSets.values()) { value.reload(fontOptions); } } private static List> loadResourceStack(List resourceStack, Identifier fontName) { ArrayList> builderStack = new ArrayList>(); for (Resource resource : resourceStack) { try { BufferedReader reader = resource.openAsReader(); try { JsonElement jsonContents = (JsonElement)GSON.fromJson((Reader)reader, JsonElement.class); FontDefinitionFile definition = (FontDefinitionFile)FontDefinitionFile.CODEC.parse((DynamicOps)JsonOps.INSTANCE, (Object)jsonContents).getOrThrow(JsonParseException::new); List providers = definition.providers; for (int i = providers.size() - 1; i >= 0; --i) { BuilderId id = new BuilderId(fontName, resource.sourcePackId(), i); builderStack.add((Pair)Pair.of((Object)id, (Object)providers.get(i))); } } finally { if (reader == null) continue; ((Reader)reader).close(); } } catch (Exception e) { LOGGER.warn("Unable to load font '{}' in {} in resourcepack: '{}'", new Object[]{fontName, FONTS_PATH, resource.sourcePackId(), e}); } } return builderStack; } public Font createFont() { return new Font(this.anyGlyphs); } public Font createFontFilterFishy() { return new Font(this.nonFishyGlyphs); } private FontSet getFontSetRaw(Identifier id) { return this.fontSets.getOrDefault(id, this.missingFontSet); } private GlyphSource getSpriteFont(FontDescription.AtlasSprite contents) { AtlasGlyphProvider provider = this.atlasProviders.get(contents.atlasId()); if (provider == null) { return this.missingFontSet.source(false); } return provider.sourceForSprite(contents.spriteId()); } @Override public void close() { this.anyGlyphs.close(); this.nonFishyGlyphs.close(); this.fontSets.values().forEach(FontSet::close); this.providersToClose.forEach(GlyphProvider::close); this.missingFontSet.close(); } private class CachedFontProvider implements Font.Provider, AutoCloseable { private final boolean nonFishyOnly; private volatile @Nullable CachedEntry lastEntry; private volatile @Nullable EffectGlyph whiteGlyph; private CachedFontProvider(boolean nonFishyOnly) { this.nonFishyOnly = nonFishyOnly; } public void invalidate() { this.lastEntry = null; this.whiteGlyph = null; } @Override public void close() { this.invalidate(); } private GlyphSource getGlyphSource(FontDescription description) { FontDescription fontDescription = description; Objects.requireNonNull(fontDescription); FontDescription fontDescription2 = fontDescription; int n = 0; return switch (SwitchBootstraps.typeSwitch("typeSwitch", new Object[]{FontDescription.Resource.class, FontDescription.AtlasSprite.class, FontDescription.PlayerSprite.class}, (Object)fontDescription2, n)) { case 0 -> { FontDescription.Resource resource = (FontDescription.Resource)fontDescription2; yield FontManager.this.getFontSetRaw(resource.id()).source(this.nonFishyOnly); } case 1 -> { FontDescription.AtlasSprite sprite = (FontDescription.AtlasSprite)fontDescription2; yield FontManager.this.getSpriteFont(sprite); } case 2 -> { FontDescription.PlayerSprite player = (FontDescription.PlayerSprite)fontDescription2; yield FontManager.this.playerProvider.sourceForPlayer(player); } default -> FontManager.this.missingFontSet.source(this.nonFishyOnly); }; } @Override public GlyphSource glyphs(FontDescription description) { CachedEntry lastEntry = this.lastEntry; if (lastEntry != null && description.equals(lastEntry.description)) { return lastEntry.source; } GlyphSource result = this.getGlyphSource(description); this.lastEntry = new CachedEntry(description, result); return result; } @Override public EffectGlyph effect() { EffectGlyph whiteGlyph = this.whiteGlyph; if (whiteGlyph == null) { this.whiteGlyph = whiteGlyph = FontManager.this.getFontSetRaw(FontDescription.DEFAULT.id()).whiteGlyph(); } return whiteGlyph; } private record CachedEntry(FontDescription description, GlyphSource source) { } } private record BuilderId(Identifier fontId, String pack, int index) { @Override public String toString() { return "(" + String.valueOf(this.fontId) + ": builder #" + this.index + " from pack " + this.pack + ")"; } } private record Preparation(Map> fontSets, List allProviders) { } private record FontDefinitionFile(List providers) { public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group((App)GlyphProviderDefinition.Conditional.CODEC.listOf().fieldOf("providers").forGetter(FontDefinitionFile::providers)).apply((Applicative)i, FontDefinitionFile::new)); } private record UnresolvedBuilderBundle(Identifier fontId, List builders, Set dependencies) implements DependencySorter.Entry { public UnresolvedBuilderBundle(Identifier fontId) { this(fontId, new ArrayList(), new HashSet()); } public void add(BuilderId builderId, FontOption.Filter filter, GlyphProviderDefinition.Reference reference) { this.builders.add(new BuilderResult(builderId, filter, (Either>, Identifier>)Either.right((Object)reference.id()))); this.dependencies.add(reference.id()); } public void add(BuilderId builderId, FontOption.Filter filter, CompletableFuture> provider) { this.builders.add(new BuilderResult(builderId, filter, (Either>, Identifier>)Either.left(provider))); } private Stream>> listBuilders() { return this.builders.stream().flatMap(e -> e.result.left().stream()); } public Optional> resolve(Function> resolver) { ArrayList resolved = new ArrayList(); for (BuilderResult builder : this.builders) { Optional> resolvedBuilder = builder.resolve(resolver); if (resolvedBuilder.isPresent()) { resolved.addAll(resolvedBuilder.get()); continue; } return Optional.empty(); } return Optional.of(resolved); } @Override public void visitRequiredDependencies(Consumer output) { this.dependencies.forEach(output); } @Override public void visitOptionalDependencies(Consumer output) { } } private record BuilderResult(BuilderId id, FontOption.Filter filter, Either>, Identifier> result) { public Optional> resolve(Function> resolver) { return (Optional)this.result.map(provider -> ((Optional)provider.join()).map(p -> List.of(new GlyphProvider.Conditional((GlyphProvider)p, this.filter))), reference -> { List resolvedReferences = (List)resolver.apply((Identifier)reference); if (resolvedReferences == null) { LOGGER.warn("Can't find font {} referenced by builder {}, either because it's missing, failed to load or is part of loading cycle", reference, (Object)this.id); return Optional.empty(); } return Optional.of(resolvedReferences.stream().map(this::mergeFilters).toList()); }); } private GlyphProvider.Conditional mergeFilters(GlyphProvider.Conditional original) { return new GlyphProvider.Conditional(original.provider(), this.filter.merge(original.filter())); } } }