package net.minecraft.client.gui.font; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.mojang.blaze3d.font.GlyphInfo; import com.mojang.blaze3d.font.GlyphProvider; import com.mojang.blaze3d.font.SheetGlyphInfo; import com.mojang.blaze3d.font.GlyphProvider.Conditional; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.ints.Int2ObjectFunction; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.IntFunction; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.font.glyphs.BakedGlyph; import net.minecraft.client.gui.font.glyphs.SpecialGlyphs; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class FontSet implements AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private static final RandomSource RANDOM = RandomSource.create(); private static final float LARGE_FORWARD_ADVANCE = 32.0F; private final TextureManager textureManager; private final ResourceLocation name; private BakedGlyph missingGlyph; private BakedGlyph whiteGlyph; private List allProviders = List.of(); private List activeProviders = List.of(); private final CodepointMap glyphs = new CodepointMap<>(BakedGlyph[]::new, BakedGlyph[][]::new); private final CodepointMap glyphInfos = new CodepointMap<>(FontSet.GlyphInfoFilter[]::new, FontSet.GlyphInfoFilter[][]::new); private final Int2ObjectMap glyphsByWidth = new Int2ObjectOpenHashMap<>(); private final List textures = Lists.newArrayList(); private final IntFunction glyphInfoGetter = this::computeGlyphInfo; private final IntFunction glyphGetter = this::computeBakedGlyph; public FontSet(TextureManager textureManager, ResourceLocation name) { this.textureManager = textureManager; this.name = name; } public void reload(List allProviders, Set options) { this.allProviders = allProviders; this.reload(options); } public void reload(Set options) { this.activeProviders = List.of(); this.resetTextures(); this.activeProviders = this.selectProviders(this.allProviders, options); } private void resetTextures() { this.textures.clear(); this.glyphs.clear(); this.glyphInfos.clear(); this.glyphsByWidth.clear(); this.missingGlyph = SpecialGlyphs.MISSING.bake(this::stitch); this.whiteGlyph = SpecialGlyphs.WHITE.bake(this::stitch); } private List selectProviders(List providers, Set options) { IntSet intSet = new IntOpenHashSet(); List list = new ArrayList(); for (Conditional conditional : providers) { if (conditional.filter().apply(options)) { list.add(conditional.provider()); intSet.addAll(conditional.provider().getSupportedGlyphs()); } } Set set = Sets.newHashSet(); intSet.forEach(i -> { for (GlyphProvider glyphProvider : list) { GlyphInfo glyphInfo = glyphProvider.getGlyph(i); if (glyphInfo != null) { set.add(glyphProvider); if (glyphInfo != SpecialGlyphs.MISSING) { this.glyphsByWidth.computeIfAbsent(Mth.ceil(glyphInfo.getAdvance(false)), (Int2ObjectFunction)(ix -> new IntArrayList())).add(i); } break; } } }); return list.stream().filter(set::contains).toList(); } public void close() { this.textures.clear(); } private static boolean hasFishyAdvance(GlyphInfo glyph) { float f = glyph.getAdvance(false); if (!(f < 0.0F) && !(f > 32.0F)) { float g = glyph.getAdvance(true); return g < 0.0F || g > 32.0F; } else { return true; } } private FontSet.GlyphInfoFilter computeGlyphInfo(int character) { GlyphInfo glyphInfo = null; for (GlyphProvider glyphProvider : this.activeProviders) { GlyphInfo glyphInfo2 = glyphProvider.getGlyph(character); if (glyphInfo2 != null) { if (glyphInfo == null) { glyphInfo = glyphInfo2; } if (!hasFishyAdvance(glyphInfo2)) { return new FontSet.GlyphInfoFilter(glyphInfo, glyphInfo2); } } } return glyphInfo != null ? new FontSet.GlyphInfoFilter(glyphInfo, SpecialGlyphs.MISSING) : FontSet.GlyphInfoFilter.MISSING; } public GlyphInfo getGlyphInfo(int character, boolean filterFishyGlyphs) { return this.glyphInfos.computeIfAbsent(character, this.glyphInfoGetter).select(filterFishyGlyphs); } private BakedGlyph computeBakedGlyph(int character) { for (GlyphProvider glyphProvider : this.activeProviders) { GlyphInfo glyphInfo = glyphProvider.getGlyph(character); if (glyphInfo != null) { return glyphInfo.bake(this::stitch); } } LOGGER.warn("Couldn't find glyph for character {} (\\u{})", Character.toString(character), String.format("%04x", character)); return this.missingGlyph; } public BakedGlyph getGlyph(int character) { return this.glyphs.computeIfAbsent(character, this.glyphGetter); } private BakedGlyph stitch(SheetGlyphInfo glyphInfo) { for (FontTexture fontTexture : this.textures) { BakedGlyph bakedGlyph = fontTexture.add(glyphInfo); if (bakedGlyph != null) { return bakedGlyph; } } ResourceLocation resourceLocation = this.name.withSuffix("/" + this.textures.size()); boolean bl = glyphInfo.isColored(); GlyphRenderTypes glyphRenderTypes = bl ? GlyphRenderTypes.createForColorTexture(resourceLocation) : GlyphRenderTypes.createForIntensityTexture(resourceLocation); FontTexture fontTexture2 = new FontTexture(resourceLocation::toString, glyphRenderTypes, bl); this.textures.add(fontTexture2); this.textureManager.register(resourceLocation, fontTexture2); BakedGlyph bakedGlyph2 = fontTexture2.add(glyphInfo); return bakedGlyph2 == null ? this.missingGlyph : bakedGlyph2; } public BakedGlyph getRandomGlyph(GlyphInfo glyph) { IntList intList = this.glyphsByWidth.get(Mth.ceil(glyph.getAdvance(false))); return intList != null && !intList.isEmpty() ? this.getGlyph(intList.getInt(RANDOM.nextInt(intList.size()))) : this.missingGlyph; } public ResourceLocation name() { return this.name; } public BakedGlyph whiteGlyph() { return this.whiteGlyph; } @Environment(EnvType.CLIENT) record GlyphInfoFilter(GlyphInfo glyphInfo, GlyphInfo glyphInfoNotFishy) { static final FontSet.GlyphInfoFilter MISSING = new FontSet.GlyphInfoFilter(SpecialGlyphs.MISSING, SpecialGlyphs.MISSING); GlyphInfo select(boolean filterFishyGlyphs) { return filterFishyGlyphs ? this.glyphInfoNotFishy : this.glyphInfo; } } }