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.blaze3d.font.GlyphProvider.Conditional; 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.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.Map.Entry; 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.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.Options; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Loader; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Reference; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; 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.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class FontManager implements PreparableReloadListener, AutoCloseable { static final Logger LOGGER = LogUtils.getLogger(); private static final String FONTS_PATH = "fonts.json"; public static final ResourceLocation MISSING_FONT = ResourceLocation.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; @Nullable private volatile FontSet lastFontSetCache; public FontManager(TextureManager textureManager) { this.textureManager = textureManager; this.missingFontSet = Util.make(new FontSet(textureManager, MISSING_FONT), fontSet -> fontSet.reload(List.of(createFallbackProvider()), Set.of())); } private static Conditional createFallbackProvider() { return new Conditional(new AllMissingGlyphProvider(), FontOption.Filter.ALWAYS_PASS); } @Override public CompletableFuture reload( PreparableReloadListener.PreparationBarrier preparationBarrier, ResourceManager resourceManager, Executor executor, Executor executor2 ) { return this.prepare(resourceManager, executor) .thenCompose(preparationBarrier::wait) .thenAcceptAsync(preparation -> this.apply(preparation, Profiler.get()), executor2); } private CompletableFuture prepare(ResourceManager resourceManager, Executor executor) { List> list = new ArrayList(); for (Entry> entry : FONT_DEFINITIONS.listMatchingResourceStacks(resourceManager).entrySet()) { ResourceLocation resourceLocation = FONT_DEFINITIONS.fileToId((ResourceLocation)entry.getKey()); list.add( CompletableFuture.supplyAsync( () -> { List> listx = loadResourceStack( (List)entry.getValue(), resourceLocation ); FontManager.UnresolvedBuilderBundle unresolvedBuilderBundle = new FontManager.UnresolvedBuilderBundle(resourceLocation); for (Pair pair : listx) { FontManager.BuilderId builderId = pair.getFirst(); FontOption.Filter filter = pair.getSecond().filter(); pair.getSecond().definition().unpack().ifLeft(loader -> { CompletableFuture> completableFuture = this.safeLoad(builderId, loader, resourceManager, executor); unresolvedBuilderBundle.add(builderId, filter, completableFuture); }).ifRight(reference -> unresolvedBuilderBundle.add(builderId, filter, reference)); } return unresolvedBuilderBundle; }, executor ) ); } return Util.sequence(list) .thenCompose( listx -> { List>> list2 = (List>>)listx.stream() .flatMap(FontManager.UnresolvedBuilderBundle::listBuilders) .collect(Util.toMutableList()); Conditional conditional = createFallbackProvider(); list2.add(CompletableFuture.completedFuture(Optional.of(conditional.provider()))); return Util.sequence(list2) .thenCompose( list2x -> { Map> map = this.resolveProviders(listx); CompletableFuture[] completableFutures = (CompletableFuture[])map.values() .stream() .map(listxxx -> CompletableFuture.runAsync(() -> this.finalizeProviderLoading(listxxx, conditional), executor)) .toArray(CompletableFuture[]::new); return CompletableFuture.allOf(completableFutures).thenApply(void_ -> { List list2xx = list2x.stream().flatMap(Optional::stream).toList(); return new FontManager.Preparation(map, list2xx); }); } ); } ); } private CompletableFuture> safeLoad(FontManager.BuilderId builderId, Loader loader, ResourceManager resourceManager, Executor executor) { return CompletableFuture.supplyAsync(() -> { try { return Optional.of(loader.load(resourceManager)); } catch (Exception var4) { LOGGER.warn("Failed to load builder {}, rejecting", builderId, var4); return Optional.empty(); } }, executor); } private Map> resolveProviders(List unresolvedBuilderBundles) { Map> map = new HashMap(); DependencySorter dependencySorter = new DependencySorter<>(); unresolvedBuilderBundles.forEach(unresolvedBuilderBundle -> dependencySorter.addEntry(unresolvedBuilderBundle.fontId, unresolvedBuilderBundle)); dependencySorter.orderByDependencies( (resourceLocation, unresolvedBuilderBundle) -> unresolvedBuilderBundle.resolve(map::get).ifPresent(list -> map.put(resourceLocation, list)) ); return map; } private void finalizeProviderLoading(List providers, Conditional fallbackProvider) { providers.add(0, fallbackProvider); IntSet intSet = new IntOpenHashSet(); for (Conditional conditional : providers) { intSet.addAll(conditional.provider().getSupportedGlyphs()); } intSet.forEach(i -> { if (i != 32) { for (Conditional conditionalx : Lists.reverse(providers)) { if (conditionalx.provider().getGlyph(i) != null) { break; } } } }); } private static Set getFontOptions(Options options) { Set set = EnumSet.noneOf(FontOption.class); if (options.forceUnicodeFont().get()) { set.add(FontOption.UNIFORM); } if (options.japaneseGlyphVariants().get()) { set.add(FontOption.JAPANESE_VARIANTS); } return set; } private void apply(FontManager.Preparation preperation, ProfilerFiller profiler) { profiler.push("closing"); this.lastFontSetCache = null; this.fontSets.values().forEach(FontSet::close); this.fontSets.clear(); this.providersToClose.forEach(GlyphProvider::close); this.providersToClose.clear(); Set set = getFontOptions(Minecraft.getInstance().options); profiler.popPush("reloading"); preperation.fontSets().forEach((resourceLocation, list) -> { FontSet fontSet = new FontSet(this.textureManager, resourceLocation); fontSet.reload(Lists.reverse(list), set); this.fontSets.put(resourceLocation, fontSet); }); this.providersToClose.addAll(preperation.allProviders); profiler.pop(); if (!this.fontSets.containsKey(Minecraft.DEFAULT_FONT)) { throw new IllegalStateException("Default font failed to load"); } } public void updateOptions(Options options) { Set set = getFontOptions(options); for (FontSet fontSet : this.fontSets.values()) { fontSet.reload(set); } } private static List> loadResourceStack( List resources, ResourceLocation fontId ) { List> list = new ArrayList(); for (Resource resource : resources) { try { Reader reader = resource.openAsReader(); try { JsonElement jsonElement = GSON.fromJson(reader, JsonElement.class); FontManager.FontDefinitionFile fontDefinitionFile = FontManager.FontDefinitionFile.CODEC .parse(JsonOps.INSTANCE, jsonElement) .getOrThrow(JsonParseException::new); List list2 = fontDefinitionFile.providers; for (int i = list2.size() - 1; i >= 0; i--) { FontManager.BuilderId builderId = new FontManager.BuilderId(fontId, resource.sourcePackId(), i); list.add(Pair.of(builderId, (net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional)list2.get(i))); } } catch (Throwable var12) { if (reader != null) { try { reader.close(); } catch (Throwable var11) { var12.addSuppressed(var11); } } throw var12; } if (reader != null) { reader.close(); } } catch (Exception var13) { LOGGER.warn("Unable to load font '{}' in {} in resourcepack: '{}'", fontId, "fonts.json", resource.sourcePackId(), var13); } } return list; } public Font createFont() { return new Font(this::getFontSetCached, false); } public Font createFontFilterFishy() { return new Font(this::getFontSetCached, true); } private FontSet getFontSetRaw(ResourceLocation fontSet) { return (FontSet)this.fontSets.getOrDefault(fontSet, this.missingFontSet); } private FontSet getFontSetCached(ResourceLocation fontSet) { FontSet fontSet2 = this.lastFontSetCache; if (fontSet2 != null && fontSet.equals(fontSet2.name())) { return fontSet2; } else { FontSet fontSet3 = this.getFontSetRaw(fontSet); this.lastFontSetCache = fontSet3; return fontSet3; } } public void close() { this.fontSets.values().forEach(FontSet::close); this.providersToClose.forEach(GlyphProvider::close); this.missingFontSet.close(); } @Environment(EnvType.CLIENT) record BuilderId(ResourceLocation fontId, String pack, int index) { public String toString() { return "(" + this.fontId + ": builder #" + this.index + " from pack " + this.pack + ")"; } } @Environment(EnvType.CLIENT) record BuilderResult(FontManager.BuilderId id, FontOption.Filter filter, Either>, ResourceLocation> result) { public Optional> resolve(Function> providerResolver) { return this.result .map( completableFuture -> ((Optional)completableFuture.join()).map(glyphProvider -> List.of(new Conditional(glyphProvider, this.filter))), resourceLocation -> { List list = (List)providerResolver.apply(resourceLocation); if (list == null) { FontManager.LOGGER .warn("Can't find font {} referenced by builder {}, either because it's missing, failed to load or is part of loading cycle", resourceLocation, this.id); return Optional.empty(); } else { return Optional.of(list.stream().map(this::mergeFilters).toList()); } } ); } private Conditional mergeFilters(Conditional conditional) { return new Conditional(conditional.provider(), this.filter.merge(conditional.filter())); } } @Environment(EnvType.CLIENT) record FontDefinitionFile(List providers) { public static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group( net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional.CODEC .listOf() .fieldOf("providers") .forGetter(FontManager.FontDefinitionFile::providers) ) .apply(instance, FontManager.FontDefinitionFile::new) ); } @Environment(EnvType.CLIENT) record Preparation(Map> fontSets, List allProviders) { } @Environment(EnvType.CLIENT) record UnresolvedBuilderBundle(ResourceLocation fontId, List builders, Set dependencies) implements net.minecraft.util.DependencySorter.Entry { public UnresolvedBuilderBundle(ResourceLocation fontId) { this(fontId, new ArrayList(), new HashSet()); } public void add(FontManager.BuilderId builderId, FontOption.Filter filter, Reference glyphProvider) { this.builders.add(new FontManager.BuilderResult(builderId, filter, Either.right(glyphProvider.id()))); this.dependencies.add(glyphProvider.id()); } public void add(FontManager.BuilderId builderId, FontOption.Filter filter, CompletableFuture> glyphProvider) { this.builders.add(new FontManager.BuilderResult(builderId, filter, Either.left(glyphProvider))); } private Stream>> listBuilders() { return this.builders.stream().flatMap(builderResult -> builderResult.result.left().stream()); } public Optional> resolve(Function> providerResolver) { List list = new ArrayList(); for (FontManager.BuilderResult builderResult : this.builders) { Optional> optional = builderResult.resolve(providerResolver); if (!optional.isPresent()) { return Optional.empty(); } list.addAll((Collection)optional.get()); } return Optional.of(list); } @Override public void visitRequiredDependencies(Consumer visitor) { this.dependencies.forEach(visitor); } @Override public void visitOptionalDependencies(Consumer visitor) { } } }