Skip to content

Commit 888743f

Browse files
committed
Custom Fonts (assets + mixins)
1 parent b91c004 commit 888743f

36 files changed

Lines changed: 9474 additions & 0 deletions
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package net.minecraft.client.gui.font;
2+
3+
import com.google.common.collect.Sets;
4+
import com.mojang.blaze3d.font.GlyphBitmap;
5+
import com.mojang.blaze3d.font.GlyphInfo;
6+
import com.mojang.blaze3d.font.GlyphProvider;
7+
import com.mojang.blaze3d.font.UnbakedGlyph;
8+
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction;
9+
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
10+
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
11+
import it.unimi.dsi.fastutil.ints.IntArrayList;
12+
import it.unimi.dsi.fastutil.ints.IntList;
13+
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
14+
import it.unimi.dsi.fastutil.ints.IntSet;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Objects;
18+
import java.util.Set;
19+
import java.util.function.IntFunction;
20+
import java.util.function.Supplier;
21+
import net.fabricmc.api.EnvType;
22+
import net.fabricmc.api.Environment;
23+
import net.minecraft.client.gui.GlyphSource;
24+
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
25+
import net.minecraft.client.gui.font.glyphs.EffectGlyph;
26+
import net.minecraft.client.gui.font.glyphs.SpecialGlyphs;
27+
import net.minecraft.network.chat.Style;
28+
import net.minecraft.util.Mth;
29+
import net.minecraft.util.RandomSource;
30+
import org.jspecify.annotations.Nullable;
31+
32+
@Environment(EnvType.CLIENT)
33+
public class FontSet implements AutoCloseable {
34+
private static final float LARGE_FORWARD_ADVANCE = 32.0F;
35+
private static final BakedGlyph INVISIBLE_MISSING_GLYPH = new BakedGlyph() {
36+
@Override
37+
public GlyphInfo info() {
38+
return SpecialGlyphs.MISSING;
39+
}
40+
41+
@Nullable
42+
@Override
43+
public TextRenderable.Styled createGlyph(float f, float g, int i, int j, Style style, float h, float k) {
44+
return null;
45+
}
46+
};
47+
final GlyphStitcher stitcher;
48+
final UnbakedGlyph.Stitcher wrappedStitcher = new UnbakedGlyph.Stitcher() {
49+
@Override
50+
public BakedGlyph stitch(GlyphInfo glyphInfo, GlyphBitmap glyphBitmap) {
51+
return (BakedGlyph)Objects.requireNonNullElse(FontSet.this.stitcher.stitch(glyphInfo, glyphBitmap), FontSet.this.missingGlyph);
52+
}
53+
54+
@Override
55+
public BakedGlyph getMissing() {
56+
return FontSet.this.missingGlyph;
57+
}
58+
};
59+
private List<GlyphProvider.Conditional> allProviders = List.of();
60+
private List<GlyphProvider> activeProviders = List.of();
61+
private final Int2ObjectMap<IntList> glyphsByWidth = new Int2ObjectOpenHashMap<>();
62+
private final CodepointMap<FontSet.SelectedGlyphs> glyphCache = new CodepointMap<>(FontSet.SelectedGlyphs[]::new, FontSet.SelectedGlyphs[][]::new);
63+
private final IntFunction<FontSet.SelectedGlyphs> glyphGetter = this::computeGlyphInfo;
64+
BakedGlyph missingGlyph = INVISIBLE_MISSING_GLYPH;
65+
private final Supplier<BakedGlyph> missingGlyphGetter = () -> this.missingGlyph;
66+
private final FontSet.SelectedGlyphs missingSelectedGlyphs = new FontSet.SelectedGlyphs(this.missingGlyphGetter, this.missingGlyphGetter);
67+
@Nullable
68+
private EffectGlyph whiteGlyph;
69+
private final GlyphSource anyGlyphs = new FontSet.Source(false);
70+
private final GlyphSource nonFishyGlyphs = new FontSet.Source(true);
71+
72+
public FontSet(GlyphStitcher glyphStitcher) {
73+
this.stitcher = glyphStitcher;
74+
}
75+
76+
public void reload(List<GlyphProvider.Conditional> list, Set<FontOption> set) {
77+
this.allProviders = list;
78+
this.reload(set);
79+
}
80+
81+
public void reload(Set<FontOption> set) {
82+
this.activeProviders = List.of();
83+
this.resetTextures();
84+
this.activeProviders = this.selectProviders(this.allProviders, set);
85+
}
86+
87+
private void resetTextures() {
88+
this.stitcher.reset();
89+
this.glyphCache.clear();
90+
this.glyphsByWidth.clear();
91+
this.missingGlyph = (BakedGlyph)Objects.requireNonNull(SpecialGlyphs.MISSING.bake(this.stitcher));
92+
this.whiteGlyph = SpecialGlyphs.WHITE.bake(this.stitcher);
93+
}
94+
95+
private List<GlyphProvider> selectProviders(List<GlyphProvider.Conditional> list, Set<FontOption> set) {
96+
IntSet intSet = new IntOpenHashSet();
97+
List<GlyphProvider> list2 = new ArrayList();
98+
99+
for (GlyphProvider.Conditional conditional : list) {
100+
if (conditional.filter().apply(set)) {
101+
list2.add(conditional.provider());
102+
intSet.addAll(conditional.provider().getSupportedGlyphs());
103+
}
104+
}
105+
106+
Set<GlyphProvider> set2 = Sets.<GlyphProvider>newHashSet();
107+
intSet.forEach(
108+
i -> {
109+
for (GlyphProvider glyphProvider : list2) {
110+
UnbakedGlyph unbakedGlyph = glyphProvider.getGlyph(i);
111+
if (unbakedGlyph != null) {
112+
set2.add(glyphProvider);
113+
if (unbakedGlyph.info() != SpecialGlyphs.MISSING) {
114+
this.glyphsByWidth
115+
.computeIfAbsent(Mth.ceil(unbakedGlyph.info().getAdvance(false)), (Int2ObjectFunction<? extends IntList>)(ix -> new IntArrayList()))
116+
.add(i);
117+
}
118+
break;
119+
}
120+
}
121+
}
122+
);
123+
return list2.stream().filter(set2::contains).toList();
124+
}
125+
126+
public void close() {
127+
this.stitcher.close();
128+
}
129+
130+
private static boolean hasFishyAdvance(GlyphInfo glyphInfo) {
131+
float f = glyphInfo.getAdvance(false);
132+
if (!(f < 0.0F) && !(f > 32.0F)) {
133+
float g = glyphInfo.getAdvance(true);
134+
return g < 0.0F || g > 32.0F;
135+
} else {
136+
return true;
137+
}
138+
}
139+
140+
private FontSet.SelectedGlyphs computeGlyphInfo(int i) {
141+
FontSet.DelayedBake delayedBake = null;
142+
143+
for (GlyphProvider glyphProvider : this.activeProviders) {
144+
UnbakedGlyph unbakedGlyph = glyphProvider.getGlyph(i);
145+
if (unbakedGlyph != null) {
146+
if (delayedBake == null) {
147+
delayedBake = new FontSet.DelayedBake(unbakedGlyph);
148+
}
149+
150+
if (!hasFishyAdvance(unbakedGlyph.info())) {
151+
if (delayedBake.unbaked == unbakedGlyph) {
152+
return new FontSet.SelectedGlyphs(delayedBake, delayedBake);
153+
}
154+
155+
return new FontSet.SelectedGlyphs(delayedBake, new FontSet.DelayedBake(unbakedGlyph));
156+
}
157+
}
158+
}
159+
160+
return delayedBake != null ? new FontSet.SelectedGlyphs(delayedBake, this.missingGlyphGetter) : this.missingSelectedGlyphs;
161+
}
162+
163+
FontSet.SelectedGlyphs getGlyph(int i) {
164+
return this.glyphCache.computeIfAbsent(i, this.glyphGetter);
165+
}
166+
167+
public BakedGlyph getRandomGlyph(RandomSource randomSource, int i) {
168+
IntList intList = this.glyphsByWidth.get(i);
169+
return intList != null && !intList.isEmpty()
170+
? (BakedGlyph)this.getGlyph(intList.getInt(randomSource.nextInt(intList.size()))).nonFishy().get()
171+
: this.missingGlyph;
172+
}
173+
174+
public EffectGlyph whiteGlyph() {
175+
return (EffectGlyph)Objects.requireNonNull(this.whiteGlyph);
176+
}
177+
178+
public GlyphSource source(boolean bl) {
179+
return bl ? this.nonFishyGlyphs : this.anyGlyphs;
180+
}
181+
182+
@Environment(EnvType.CLIENT)
183+
class DelayedBake implements Supplier<BakedGlyph> {
184+
final UnbakedGlyph unbaked;
185+
@Nullable
186+
private BakedGlyph baked;
187+
188+
DelayedBake(final UnbakedGlyph unbakedGlyph) {
189+
this.unbaked = unbakedGlyph;
190+
}
191+
192+
public BakedGlyph get() {
193+
if (this.baked == null) {
194+
this.baked = this.unbaked.bake(FontSet.this.wrappedStitcher);
195+
}
196+
197+
return this.baked;
198+
}
199+
}
200+
201+
@Environment(EnvType.CLIENT)
202+
record SelectedGlyphs(Supplier<BakedGlyph> any, Supplier<BakedGlyph> nonFishy) {
203+
Supplier<BakedGlyph> select(boolean bl) {
204+
return bl ? this.nonFishy : this.any;
205+
}
206+
}
207+
208+
@Environment(EnvType.CLIENT)
209+
public class Source implements GlyphSource {
210+
private final boolean filterFishyGlyphs;
211+
212+
public Source(final boolean bl) {
213+
this.filterFishyGlyphs = bl;
214+
}
215+
216+
@Override
217+
public BakedGlyph getGlyph(int i) {
218+
return (BakedGlyph)FontSet.this.getGlyph(i).select(this.filterFishyGlyphs).get();
219+
}
220+
221+
@Override
222+
public BakedGlyph getRandomGlyph(RandomSource randomSource, int i) {
223+
return FontSet.this.getRandomGlyph(randomSource, i);
224+
}
225+
}
226+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package net.minecraft.client.gui.font.providers;
2+
3+
import com.mojang.blaze3d.font.GlyphProvider;
4+
import com.mojang.blaze3d.font.TrueTypeGlyphProvider;
5+
import com.mojang.blaze3d.platform.TextureUtil;
6+
import com.mojang.datafixers.util.Either;
7+
import com.mojang.serialization.Codec;
8+
import com.mojang.serialization.MapCodec;
9+
import com.mojang.serialization.codecs.RecordCodecBuilder;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.nio.ByteBuffer;
13+
import java.util.List;
14+
import net.fabricmc.api.EnvType;
15+
import net.fabricmc.api.Environment;
16+
import net.minecraft.resources.Identifier;
17+
import net.minecraft.server.packs.resources.ResourceManager;
18+
import net.minecraft.util.Util;
19+
import org.lwjgl.PointerBuffer;
20+
import org.lwjgl.system.MemoryStack;
21+
import org.lwjgl.system.MemoryUtil;
22+
import org.lwjgl.util.freetype.FT_Face;
23+
import org.lwjgl.util.freetype.FreeType;
24+
25+
@Environment(EnvType.CLIENT)
26+
public record TrueTypeGlyphProviderDefinition(Identifier location, float size, float oversample, TrueTypeGlyphProviderDefinition.Shift shift, String skip)
27+
implements GlyphProviderDefinition {
28+
private static final Codec<String> SKIP_LIST_CODEC = Codec.withAlternative(Codec.STRING, Codec.STRING.listOf(), list -> String.join("", list));
29+
public static final MapCodec<TrueTypeGlyphProviderDefinition> CODEC = RecordCodecBuilder.mapCodec(
30+
instance -> instance.group(
31+
Identifier.CODEC.fieldOf("file").forGetter(TrueTypeGlyphProviderDefinition::location),
32+
Codec.FLOAT.optionalFieldOf("size", 11.0F).forGetter(TrueTypeGlyphProviderDefinition::size),
33+
Codec.FLOAT.optionalFieldOf("oversample", 1.0F).forGetter(TrueTypeGlyphProviderDefinition::oversample),
34+
TrueTypeGlyphProviderDefinition.Shift.CODEC
35+
.optionalFieldOf("shift", TrueTypeGlyphProviderDefinition.Shift.NONE)
36+
.forGetter(TrueTypeGlyphProviderDefinition::shift),
37+
SKIP_LIST_CODEC.optionalFieldOf("skip", "").forGetter(TrueTypeGlyphProviderDefinition::skip)
38+
)
39+
.apply(instance, TrueTypeGlyphProviderDefinition::new)
40+
);
41+
42+
@Override
43+
public GlyphProviderType type() {
44+
return GlyphProviderType.TTF;
45+
}
46+
47+
@Override
48+
public Either<GlyphProviderDefinition.Loader, GlyphProviderDefinition.Reference> unpack() {
49+
return Either.left(this::load);
50+
}
51+
52+
private GlyphProvider load(ResourceManager resourceManager) throws IOException {
53+
FT_Face fT_Face = null;
54+
ByteBuffer byteBuffer = null;
55+
56+
try {
57+
InputStream inputStream = resourceManager.open(this.location.withPrefix("font/"));
58+
59+
TrueTypeGlyphProvider var20;
60+
try {
61+
byteBuffer = TextureUtil.readResource(inputStream);
62+
synchronized (FreeTypeUtil.LIBRARY_LOCK) {
63+
try (MemoryStack memoryStack = MemoryStack.stackPush()) {
64+
PointerBuffer pointerBuffer = memoryStack.mallocPointer(1);
65+
FreeTypeUtil.assertError(FreeType.FT_New_Memory_Face(FreeTypeUtil.getLibrary(), byteBuffer, 0L, pointerBuffer), "Initializing font face");
66+
fT_Face = FT_Face.create(pointerBuffer.get());
67+
}
68+
69+
String string = FreeType.FT_Get_Font_Format(fT_Face);
70+
if (!"TrueType".equals(string)) {
71+
throw new IOException("Font is not in TTF format, was " + string);
72+
}
73+
74+
FreeTypeUtil.assertError(FreeType.FT_Select_Charmap(fT_Face, FreeType.FT_ENCODING_UNICODE), "Find unicode charmap");
75+
var20 = new TrueTypeGlyphProvider(byteBuffer, fT_Face, this.size, this.oversample, this.shift.x, this.shift.y, this.skip);
76+
}
77+
} catch (Throwable var16) {
78+
if (inputStream != null) {
79+
try {
80+
inputStream.close();
81+
} catch (Throwable var11) {
82+
var16.addSuppressed(var11);
83+
}
84+
}
85+
86+
throw var16;
87+
}
88+
89+
if (inputStream != null) {
90+
inputStream.close();
91+
}
92+
93+
return var20;
94+
} catch (Exception var17) {
95+
synchronized (FreeTypeUtil.LIBRARY_LOCK) {
96+
if (fT_Face != null) {
97+
FreeType.FT_Done_Face(fT_Face);
98+
}
99+
}
100+
101+
MemoryUtil.memFree(byteBuffer);
102+
throw var17;
103+
}
104+
}
105+
106+
@Environment(EnvType.CLIENT)
107+
public record Shift(float x, float y) {
108+
public static final TrueTypeGlyphProviderDefinition.Shift NONE = new TrueTypeGlyphProviderDefinition.Shift(0.0F, 0.0F);
109+
public static final Codec<TrueTypeGlyphProviderDefinition.Shift> CODEC = Codec.floatRange(-512.0F, 512.0F)
110+
.listOf()
111+
.comapFlatMap(
112+
list -> Util.fixedSize(list, 2).map(listx -> new TrueTypeGlyphProviderDefinition.Shift((Float)listx.get(0), (Float)listx.get(1))),
113+
shift -> List.of(shift.x, shift.y)
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)