minecraft-src/net/minecraft/client/gui/font/FontManager.java
2025-07-04 03:45:38 +03:00

389 lines
15 KiB
Java

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<GlyphProvider> providersToClose = new ArrayList();
private final Map<ResourceLocation, FontSet> 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<Void> 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<FontManager.Preparation> prepare(ResourceManager resourceManager, Executor executor) {
List<CompletableFuture<FontManager.UnresolvedBuilderBundle>> list = new ArrayList();
for (Entry<ResourceLocation, List<Resource>> entry : FONT_DEFINITIONS.listMatchingResourceStacks(resourceManager).entrySet()) {
ResourceLocation resourceLocation = FONT_DEFINITIONS.fileToId((ResourceLocation)entry.getKey());
list.add(
CompletableFuture.supplyAsync(
() -> {
List<Pair<FontManager.BuilderId, net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional>> listx = loadResourceStack(
(List<Resource>)entry.getValue(), resourceLocation
);
FontManager.UnresolvedBuilderBundle unresolvedBuilderBundle = new FontManager.UnresolvedBuilderBundle(resourceLocation);
for (Pair<FontManager.BuilderId, net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional> pair : listx) {
FontManager.BuilderId builderId = pair.getFirst();
FontOption.Filter filter = pair.getSecond().filter();
pair.getSecond().definition().unpack().ifLeft(loader -> {
CompletableFuture<Optional<GlyphProvider>> 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<CompletableFuture<Optional<GlyphProvider>>> list2 = (List<CompletableFuture<Optional<GlyphProvider>>>)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<ResourceLocation, List<Conditional>> 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<GlyphProvider> list2xx = list2x.stream().flatMap(Optional::stream).toList();
return new FontManager.Preparation(map, list2xx);
});
}
);
}
);
}
private CompletableFuture<Optional<GlyphProvider>> 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<ResourceLocation, List<Conditional>> resolveProviders(List<FontManager.UnresolvedBuilderBundle> unresolvedBuilderBundles) {
Map<ResourceLocation, List<Conditional>> map = new HashMap();
DependencySorter<ResourceLocation, FontManager.UnresolvedBuilderBundle> 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<Conditional> 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<FontOption> getFontOptions(Options options) {
Set<FontOption> 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<FontOption> 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<FontOption> set = getFontOptions(options);
for (FontSet fontSet : this.fontSets.values()) {
fontSet.reload(set);
}
}
private static List<Pair<FontManager.BuilderId, net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional>> loadResourceStack(
List<Resource> resources, ResourceLocation fontId
) {
List<Pair<FontManager.BuilderId, net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional>> 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<net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional> 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<CompletableFuture<Optional<GlyphProvider>>, ResourceLocation> result) {
public Optional<List<Conditional>> resolve(Function<ResourceLocation, List<Conditional>> providerResolver) {
return this.result
.map(
completableFuture -> ((Optional)completableFuture.join()).map(glyphProvider -> List.of(new Conditional(glyphProvider, this.filter))),
resourceLocation -> {
List<Conditional> list = (List<Conditional>)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<net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Conditional> providers) {
public static final Codec<FontManager.FontDefinitionFile> 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<ResourceLocation, List<Conditional>> fontSets, List<GlyphProvider> allProviders) {
}
@Environment(EnvType.CLIENT)
record UnresolvedBuilderBundle(ResourceLocation fontId, List<FontManager.BuilderResult> builders, Set<ResourceLocation> dependencies)
implements net.minecraft.util.DependencySorter.Entry<ResourceLocation> {
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<Optional<GlyphProvider>> glyphProvider) {
this.builders.add(new FontManager.BuilderResult(builderId, filter, Either.left(glyphProvider)));
}
private Stream<CompletableFuture<Optional<GlyphProvider>>> listBuilders() {
return this.builders.stream().flatMap(builderResult -> builderResult.result.left().stream());
}
public Optional<List<Conditional>> resolve(Function<ResourceLocation, List<Conditional>> providerResolver) {
List<Conditional> list = new ArrayList();
for (FontManager.BuilderResult builderResult : this.builders) {
Optional<List<Conditional>> optional = builderResult.resolve(providerResolver);
if (!optional.isPresent()) {
return Optional.empty();
}
list.addAll((Collection)optional.get());
}
return Optional.of(list);
}
@Override
public void visitRequiredDependencies(Consumer<ResourceLocation> visitor) {
this.dependencies.forEach(visitor);
}
@Override
public void visitOptionalDependencies(Consumer<ResourceLocation> visitor) {
}
}
}