435 lines
21 KiB
Java
435 lines
21 KiB
Java
/*
|
|
* 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<GlyphProvider> providersToClose = new ArrayList<GlyphProvider>();
|
|
private final Map<Identifier, FontSet> fontSets = new HashMap<Identifier, FontSet>();
|
|
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<Identifier, AtlasGlyphProvider> atlasProviders = new HashMap<Identifier, AtlasGlyphProvider>();
|
|
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<GlyphProvider.Conditional> providers, Set<FontOption> 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<Void> 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<Preparation> prepare(ResourceManager manager, Executor executor) {
|
|
ArrayList<CompletableFuture<UnresolvedBuilderBundle>> builderFutures = new ArrayList<CompletableFuture<UnresolvedBuilderBundle>>();
|
|
for (Map.Entry<Identifier, List<Resource>> fontStack : FONT_DEFINITIONS.listMatchingResourceStacks(manager).entrySet()) {
|
|
Identifier fontName = FONT_DEFINITIONS.fileToId(fontStack.getKey());
|
|
builderFutures.add(CompletableFuture.supplyAsync(() -> {
|
|
List<Pair<BuilderId, GlyphProviderDefinition.Conditional>> builderStack = FontManager.loadResourceStack((List)fontStack.getValue(), fontName);
|
|
UnresolvedBuilderBundle bundle = new UnresolvedBuilderBundle(fontName);
|
|
for (Pair<BuilderId, GlyphProviderDefinition.Conditional> stackEntry : builderStack) {
|
|
BuilderId id = (BuilderId)stackEntry.getFirst();
|
|
FontOption.Filter options = ((GlyphProviderDefinition.Conditional)stackEntry.getSecond()).filter();
|
|
((GlyphProviderDefinition.Conditional)stackEntry.getSecond()).definition().unpack().ifLeft(provider -> {
|
|
CompletableFuture<Optional<GlyphProvider>> 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<Identifier, List<GlyphProvider.Conditional>> resolved = this.resolveProviders((List<UnresolvedBuilderBundle>)builders);
|
|
CompletableFuture[] finalizers = (CompletableFuture[])resolved.values().stream().map(providers -> CompletableFuture.runAsync(() -> this.finalizeProviderLoading((List<GlyphProvider.Conditional>)providers, fallback), executor)).toArray(CompletableFuture[]::new);
|
|
return CompletableFuture.allOf(finalizers).thenApply(ignored -> {
|
|
List<GlyphProvider> providersToClose = allProviders.stream().flatMap(Optional::stream).toList();
|
|
return new Preparation(resolved, providersToClose);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
private CompletableFuture<Optional<GlyphProvider>> 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<Identifier, List<GlyphProvider.Conditional>> resolveProviders(List<UnresolvedBuilderBundle> unresolvedProviders) {
|
|
HashMap<Identifier, List<GlyphProvider.Conditional>> result = new HashMap<Identifier, List<GlyphProvider.Conditional>>();
|
|
DependencySorter<Identifier, UnresolvedBuilderBundle> sorter = new DependencySorter<Identifier, UnresolvedBuilderBundle>();
|
|
unresolvedProviders.forEach(e -> sorter.addEntry(e.fontId, (UnresolvedBuilderBundle)e));
|
|
sorter.orderByDependencies((id, bundle) -> bundle.resolve(result::get).ifPresent(r -> result.put((Identifier)id, (List<GlyphProvider.Conditional>)r)));
|
|
return result;
|
|
}
|
|
|
|
private void finalizeProviderLoading(List<GlyphProvider.Conditional> 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<FontOption> getFontOptions(Options options) {
|
|
EnumSet<FontOption> 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<FontOption> 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<FontOption> fontOptions = FontManager.getFontOptions(options);
|
|
for (FontSet value : this.fontSets.values()) {
|
|
value.reload(fontOptions);
|
|
}
|
|
}
|
|
|
|
private static List<Pair<BuilderId, GlyphProviderDefinition.Conditional>> loadResourceStack(List<Resource> resourceStack, Identifier fontName) {
|
|
ArrayList<Pair<BuilderId, GlyphProviderDefinition.Conditional>> builderStack = new ArrayList<Pair<BuilderId, GlyphProviderDefinition.Conditional>>();
|
|
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<GlyphProviderDefinition.Conditional> providers = definition.providers;
|
|
for (int i = providers.size() - 1; i >= 0; --i) {
|
|
BuilderId id = new BuilderId(fontName, resource.sourcePackId(), i);
|
|
builderStack.add((Pair<BuilderId, GlyphProviderDefinition.Conditional>)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<Identifier, List<GlyphProvider.Conditional>> fontSets, List<GlyphProvider> allProviders) {
|
|
}
|
|
|
|
private record FontDefinitionFile(List<GlyphProviderDefinition.Conditional> providers) {
|
|
public static final Codec<FontDefinitionFile> 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<BuilderResult> builders, Set<Identifier> dependencies) implements DependencySorter.Entry<Identifier>
|
|
{
|
|
public UnresolvedBuilderBundle(Identifier fontId) {
|
|
this(fontId, new ArrayList<BuilderResult>(), new HashSet<Identifier>());
|
|
}
|
|
|
|
public void add(BuilderId builderId, FontOption.Filter filter, GlyphProviderDefinition.Reference reference) {
|
|
this.builders.add(new BuilderResult(builderId, filter, (Either<CompletableFuture<Optional<GlyphProvider>>, Identifier>)Either.right((Object)reference.id())));
|
|
this.dependencies.add(reference.id());
|
|
}
|
|
|
|
public void add(BuilderId builderId, FontOption.Filter filter, CompletableFuture<Optional<GlyphProvider>> provider) {
|
|
this.builders.add(new BuilderResult(builderId, filter, (Either<CompletableFuture<Optional<GlyphProvider>>, Identifier>)Either.left(provider)));
|
|
}
|
|
|
|
private Stream<CompletableFuture<Optional<GlyphProvider>>> listBuilders() {
|
|
return this.builders.stream().flatMap(e -> e.result.left().stream());
|
|
}
|
|
|
|
public Optional<List<GlyphProvider.Conditional>> resolve(Function<Identifier, List<GlyphProvider.Conditional>> resolver) {
|
|
ArrayList resolved = new ArrayList();
|
|
for (BuilderResult builder : this.builders) {
|
|
Optional<List<GlyphProvider.Conditional>> resolvedBuilder = builder.resolve(resolver);
|
|
if (resolvedBuilder.isPresent()) {
|
|
resolved.addAll(resolvedBuilder.get());
|
|
continue;
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
return Optional.of(resolved);
|
|
}
|
|
|
|
@Override
|
|
public void visitRequiredDependencies(Consumer<Identifier> output) {
|
|
this.dependencies.forEach(output);
|
|
}
|
|
|
|
@Override
|
|
public void visitOptionalDependencies(Consumer<Identifier> output) {
|
|
}
|
|
}
|
|
|
|
private record BuilderResult(BuilderId id, FontOption.Filter filter, Either<CompletableFuture<Optional<GlyphProvider>>, Identifier> result) {
|
|
public Optional<List<GlyphProvider.Conditional>> resolve(Function<Identifier, @Nullable List<GlyphProvider.Conditional>> 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()));
|
|
}
|
|
}
|
|
}
|
|
|