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.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 com.mojang.serialization.codecs.RecordCodecBuilder.Instance; 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.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 javax.annotation.Nullable; 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; 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 net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.slf4j.Logger; @OnlyIn(Dist.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 p_95005_) { this.textureManager = p_95005_; this.missingFontSet = Util.make(new FontSet(p_95005_, MISSING_FONT), p_325351_ -> p_325351_.reload(List.of(createFallbackProvider()), Set.of())); } private static GlyphProvider.Conditional createFallbackProvider() { return new GlyphProvider.Conditional(new AllMissingGlyphProvider(), FontOption.Filter.ALWAYS_PASS); } @Override public CompletableFuture reload( PreparableReloadListener.PreparationBarrier p_285160_, ResourceManager p_285231_, Executor p_284975_, Executor p_285218_ ) { return this.prepare(p_285231_, p_284975_) .thenCompose(p_285160_::wait) .thenAcceptAsync(p_357671_ -> this.apply(p_357671_, Profiler.get()), p_285218_); } private CompletableFuture prepare(ResourceManager p_285252_, Executor p_284969_) { List> list = new ArrayList<>(); for (Entry> entry : FONT_DEFINITIONS.listMatchingResourceStacks(p_285252_).entrySet()) { ResourceLocation resourcelocation = FONT_DEFINITIONS.fileToId(entry.getKey()); list.add(CompletableFuture.supplyAsync(() -> { List> list1 = loadResourceStack(entry.getValue(), resourcelocation); FontManager.UnresolvedBuilderBundle fontmanager$unresolvedbuilderbundle = new FontManager.UnresolvedBuilderBundle(resourcelocation); for (Pair pair : list1) { FontManager.BuilderId fontmanager$builderid = pair.getFirst(); FontOption.Filter fontoption$filter = pair.getSecond().filter(); pair.getSecond().definition().unpack().ifLeft(p_325337_ -> { CompletableFuture> completablefuture = this.safeLoad(fontmanager$builderid, p_325337_, p_285252_, p_284969_); fontmanager$unresolvedbuilderbundle.add(fontmanager$builderid, fontoption$filter, completablefuture); }).ifRight(p_325345_ -> fontmanager$unresolvedbuilderbundle.add(fontmanager$builderid, fontoption$filter, p_325345_)); } return fontmanager$unresolvedbuilderbundle; }, p_284969_)); } return Util.sequence(list) .thenCompose( p_325341_ -> { List>> list1 = p_325341_.stream() .flatMap(FontManager.UnresolvedBuilderBundle::listBuilders) .collect(Util.toMutableList()); GlyphProvider.Conditional glyphprovider$conditional = createFallbackProvider(); list1.add(CompletableFuture.completedFuture(Optional.of(glyphprovider$conditional.provider()))); return Util.sequence(list1) .thenCompose( p_284618_ -> { Map> map = this.resolveProviders(p_325341_); CompletableFuture[] completablefuture = map.values() .stream() .map(p_284585_ -> CompletableFuture.runAsync(() -> this.finalizeProviderLoading(p_284585_, glyphprovider$conditional), p_284969_)) .toArray(CompletableFuture[]::new); return CompletableFuture.allOf(completablefuture).thenApply(p_284595_ -> { List list2 = p_284618_.stream().flatMap(Optional::stream).toList(); return new FontManager.Preparation(map, list2); }); } ); } ); } private CompletableFuture> safeLoad( FontManager.BuilderId p_285113_, GlyphProviderDefinition.Loader p_286561_, ResourceManager p_285424_, Executor p_285371_ ) { return CompletableFuture.supplyAsync(() -> { try { return Optional.of(p_286561_.load(p_285424_)); } catch (Exception exception) { LOGGER.warn("Failed to load builder {}, rejecting", p_285113_, exception); return Optional.empty(); } }, p_285371_); } private Map> resolveProviders(List p_285282_) { Map> map = new HashMap<>(); DependencySorter dependencysorter = new DependencySorter<>(); p_285282_.forEach(p_284626_ -> dependencysorter.addEntry(p_284626_.fontId, p_284626_)); dependencysorter.orderByDependencies( (p_284620_, p_284621_) -> p_284621_.resolve(map::get).ifPresent(p_284590_ -> map.put(p_284620_, (List)p_284590_)) ); return map; } private void finalizeProviderLoading(List p_285520_, GlyphProvider.Conditional p_328834_) { p_285520_.add(0, p_328834_); IntSet intset = new IntOpenHashSet(); for (GlyphProvider.Conditional glyphprovider$conditional : p_285520_) { intset.addAll(glyphprovider$conditional.provider().getSupportedGlyphs()); } intset.forEach(p_325339_ -> { if (p_325339_ != 32) { for (GlyphProvider.Conditional glyphprovider$conditional1 : Lists.reverse(p_285520_)) { if (glyphprovider$conditional1.provider().getGlyph(p_325339_) != null) { break; } } } }); } private static Set getFontOptions(Options p_331588_) { Set set = EnumSet.noneOf(FontOption.class); if (p_331588_.forceUnicodeFont().get()) { set.add(FontOption.UNIFORM); } if (p_331588_.japaneseGlyphVariants().get()) { set.add(FontOption.JAPANESE_VARIANTS); } return set; } private void apply(FontManager.Preparation p_284939_, ProfilerFiller p_285407_) { p_285407_.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); p_285407_.popPush("reloading"); p_284939_.fontSets().forEach((p_325349_, p_325350_) -> { FontSet fontset = new FontSet(this.textureManager, p_325349_); fontset.reload(Lists.reverse((List)p_325350_), set); this.fontSets.put(p_325349_, fontset); }); this.providersToClose.addAll(p_284939_.allProviders); p_285407_.pop(); if (!this.fontSets.containsKey(Minecraft.DEFAULT_FONT)) { throw new IllegalStateException("Default font failed to load"); } } public void updateOptions(Options p_335215_) { Set set = getFontOptions(p_335215_); for (FontSet fontset : this.fontSets.values()) { fontset.reload(set); } } private static List> loadResourceStack(List p_284976_, ResourceLocation p_285272_) { List> list = new ArrayList<>(); for (Resource resource : p_284976_) { try (Reader reader = resource.openAsReader()) { JsonElement jsonelement = GSON.fromJson(reader, JsonElement.class); FontManager.FontDefinitionFile fontmanager$fontdefinitionfile = FontManager.FontDefinitionFile.CODEC .parse(JsonOps.INSTANCE, jsonelement) .getOrThrow(JsonParseException::new); List list1 = fontmanager$fontdefinitionfile.providers; for (int i = list1.size() - 1; i >= 0; i--) { FontManager.BuilderId fontmanager$builderid = new FontManager.BuilderId(p_285272_, resource.sourcePackId(), i); list.add(Pair.of(fontmanager$builderid, list1.get(i))); } } catch (Exception exception) { LOGGER.warn("Unable to load font '{}' in {} in resourcepack: '{}'", p_285272_, "fonts.json", resource.sourcePackId(), exception); } } return list; } public Font createFont() { return new Font(this::getFontSetCached, false); } public Font createFontFilterFishy() { return new Font(this::getFontSetCached, true); } private FontSet getFontSetRaw(ResourceLocation p_333296_) { return this.fontSets.getOrDefault(p_333296_, this.missingFontSet); } private FontSet getFontSetCached(ResourceLocation p_332431_) { FontSet fontset = this.lastFontSetCache; if (fontset != null && p_332431_.equals(fontset.name())) { return fontset; } else { FontSet fontset1 = this.getFontSetRaw(p_332431_); this.lastFontSetCache = fontset1; return fontset1; } } @Override public void close() { this.fontSets.values().forEach(FontSet::close); this.providersToClose.forEach(GlyphProvider::close); this.missingFontSet.close(); } @OnlyIn(Dist.CLIENT) static record BuilderId(ResourceLocation fontId, String pack, int index) { @Override public String toString() { return "(" + this.fontId + ": builder #" + this.index + " from pack " + this.pack + ")"; } } @OnlyIn(Dist.CLIENT) static record BuilderResult( FontManager.BuilderId id, FontOption.Filter filter, Either>, ResourceLocation> result ) { public Optional> resolve(Function> p_284942_) { return this.result .map( p_325356_ -> p_325356_.join().map(p_325357_ -> List.of(new GlyphProvider.Conditional(p_325357_, this.filter))), p_325359_ -> { List list = p_284942_.apply(p_325359_); 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", p_325359_, this.id ); return Optional.empty(); } else { return Optional.of(list.stream().map(this::mergeFilters).toList()); } } ); } private GlyphProvider.Conditional mergeFilters(GlyphProvider.Conditional p_330532_) { return new GlyphProvider.Conditional(p_330532_.provider(), this.filter.merge(p_330532_.filter())); } } @OnlyIn(Dist.CLIENT) static record FontDefinitionFile(List providers) { public static final Codec CODEC = RecordCodecBuilder.create( p_325360_ -> p_325360_.group( GlyphProviderDefinition.Conditional.CODEC.listOf().fieldOf("providers").forGetter(FontManager.FontDefinitionFile::providers) ) .apply(p_325360_, FontManager.FontDefinitionFile::new) ); } @OnlyIn(Dist.CLIENT) static record Preparation(Map> fontSets, List allProviders) { } @OnlyIn(Dist.CLIENT) static record UnresolvedBuilderBundle(ResourceLocation fontId, List builders, Set dependencies) implements DependencySorter.Entry { public UnresolvedBuilderBundle(ResourceLocation p_284984_) { this(p_284984_, new ArrayList<>(), new HashSet<>()); } public void add(FontManager.BuilderId p_284935_, FontOption.Filter p_336303_, GlyphProviderDefinition.Reference p_334249_) { this.builders.add(new FontManager.BuilderResult(p_284935_, p_336303_, Either.right(p_334249_.id()))); this.dependencies.add(p_334249_.id()); } public void add(FontManager.BuilderId p_286837_, FontOption.Filter p_334374_, CompletableFuture> p_331945_) { this.builders.add(new FontManager.BuilderResult(p_286837_, p_334374_, Either.left(p_331945_))); } private Stream>> listBuilders() { return this.builders.stream().flatMap(p_285041_ -> p_285041_.result.left().stream()); } public Optional> resolve(Function> p_285118_) { List list = new ArrayList<>(); for (FontManager.BuilderResult fontmanager$builderresult : this.builders) { Optional> optional = fontmanager$builderresult.resolve(p_285118_); if (!optional.isPresent()) { return Optional.empty(); } list.addAll(optional.get()); } return Optional.of(list); } @Override public void visitRequiredDependencies(Consumer p_285391_) { this.dependencies.forEach(p_285391_); } @Override public void visitOptionalDependencies(Consumer p_285405_) { } } }