package net.minecraft.client.gui.font.providers; import com.google.common.annotations.VisibleForTesting; import com.mojang.blaze3d.font.GlyphInfo; import com.mojang.blaze3d.font.GlyphProvider; import com.mojang.blaze3d.font.SheetGlyphInfo; import com.mojang.datafixers.util.Either; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteList; import it.unimi.dsi.fastutil.ints.IntSet; import java.io.IOException; import java.io.InputStream; import java.nio.IntBuffer; import java.util.List; import java.util.function.Function; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.font.CodepointMap; import net.minecraft.client.gui.font.glyphs.BakedGlyph; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Loader; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition.Reference; import net.minecraft.client.gui.font.providers.UnihexProvider.Glyph.1; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.ExtraCodecs; import net.minecraft.util.FastBufferedInputStream; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class UnihexProvider implements GlyphProvider { static final Logger LOGGER = LogUtils.getLogger(); private static final int GLYPH_HEIGHT = 16; private static final int DIGITS_PER_BYTE = 2; private static final int DIGITS_FOR_WIDTH_8 = 32; private static final int DIGITS_FOR_WIDTH_16 = 64; private static final int DIGITS_FOR_WIDTH_24 = 96; private static final int DIGITS_FOR_WIDTH_32 = 128; private final CodepointMap glyphs; UnihexProvider(CodepointMap glyph) { this.glyphs = glyph; } @Nullable @Override public GlyphInfo getGlyph(int character) { return this.glyphs.get(character); } @Override public IntSet getSupportedGlyphs() { return this.glyphs.keySet(); } @VisibleForTesting static void unpackBitsToBytes(IntBuffer buffer, int lineData, int left, int right) { int i = 32 - left - 1; int j = 32 - right - 1; for (int k = i; k >= j; k--) { if (k < 32 && k >= 0) { boolean bl = (lineData >> k & 1) != 0; buffer.put(bl ? -1 : 0); } else { buffer.put(0); } } } static void unpackBitsToBytes(IntBuffer buffer, UnihexProvider.LineData lineData, int left, int right) { for (int i = 0; i < 16; i++) { int j = lineData.line(i); unpackBitsToBytes(buffer, j, left, right); } } @VisibleForTesting static void readFromStream(InputStream stream, UnihexProvider.ReaderOutput output) throws IOException { int i = 0; ByteList byteList = new ByteArrayList(128); while (true) { boolean bl = copyUntil(stream, byteList, 58); int j = byteList.size(); if (j == 0 && !bl) { return; } if (!bl || j != 4 && j != 5 && j != 6) { throw new IllegalArgumentException("Invalid entry at line " + i + ": expected 4, 5 or 6 hex digits followed by a colon"); } int k = 0; for (int l = 0; l < j; l++) { k = k << 4 | decodeHex(i, byteList.getByte(l)); } byteList.clear(); copyUntil(stream, byteList, 10); int l = byteList.size(); UnihexProvider.LineData lineData = switch (l) { case 32 -> UnihexProvider.ByteContents.read(i, byteList); case 64 -> UnihexProvider.ShortContents.read(i, byteList); case 96 -> UnihexProvider.IntContents.read24(i, byteList); case 128 -> UnihexProvider.IntContents.read32(i, byteList); default -> throw new IllegalArgumentException( "Invalid entry at line " + i + ": expected hex number describing (8,16,24,32) x 16 bitmap, followed by a new line" ); }; output.accept(k, lineData); i++; byteList.clear(); } } static int decodeHex(int lineNumber, ByteList byteList, int index) { return decodeHex(lineNumber, byteList.getByte(index)); } private static int decodeHex(int lineNumber, byte data) { return switch (data) { case 48 -> 0; case 49 -> 1; case 50 -> 2; case 51 -> 3; case 52 -> 4; case 53 -> 5; case 54 -> 6; case 55 -> 7; case 56 -> 8; case 57 -> 9; default -> throw new IllegalArgumentException("Invalid entry at line " + lineNumber + ": expected hex digit, got " + (char)data); case 65 -> 10; case 66 -> 11; case 67 -> 12; case 68 -> 13; case 69 -> 14; case 70 -> 15; }; } private static boolean copyUntil(InputStream stream, ByteList byteList, int value) throws IOException { while (true) { int i = stream.read(); if (i == -1) { return false; } if (i == value) { return true; } byteList.add((byte)i); } } @Environment(EnvType.CLIENT) record ByteContents(byte[] contents) implements UnihexProvider.LineData { @Override public int line(int index) { return this.contents[index] << 24; } static UnihexProvider.LineData read(int index, ByteList byteList) { byte[] bs = new byte[16]; int i = 0; for (int j = 0; j < 16; j++) { int k = UnihexProvider.decodeHex(index, byteList, i++); int l = UnihexProvider.decodeHex(index, byteList, i++); byte b = (byte)(k << 4 | l); bs[j] = b; } return new UnihexProvider.ByteContents(bs); } @Override public int bitWidth() { return 8; } } @Environment(EnvType.CLIENT) public static class Definition implements GlyphProviderDefinition { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( instance -> instance.group( ResourceLocation.CODEC.fieldOf("hex_file").forGetter(definition -> definition.hexFile), UnihexProvider.OverrideRange.CODEC.listOf().fieldOf("size_overrides").forGetter(definition -> definition.sizeOverrides) ) .apply(instance, UnihexProvider.Definition::new) ); private final ResourceLocation hexFile; private final List sizeOverrides; private Definition(ResourceLocation hexFile, List sizeOverrides) { this.hexFile = hexFile; this.sizeOverrides = sizeOverrides; } @Override public GlyphProviderType type() { return GlyphProviderType.UNIHEX; } @Override public Either unpack() { return Either.left(this::load); } private GlyphProvider load(ResourceManager resourceManager) throws IOException { InputStream inputStream = resourceManager.open(this.hexFile); UnihexProvider var3; try { var3 = this.loadData(inputStream); } catch (Throwable var6) { if (inputStream != null) { try { inputStream.close(); } catch (Throwable var5) { var6.addSuppressed(var5); } } throw var6; } if (inputStream != null) { inputStream.close(); } return var3; } private UnihexProvider loadData(InputStream inputStream) throws IOException { CodepointMap codepointMap = new CodepointMap<>(UnihexProvider.LineData[]::new, UnihexProvider.LineData[][]::new); UnihexProvider.ReaderOutput readerOutput = codepointMap::put; ZipInputStream zipInputStream = new ZipInputStream(inputStream); UnihexProvider var17; try { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { String string = zipEntry.getName(); if (string.endsWith(".hex")) { UnihexProvider.LOGGER.info("Found {}, loading", string); UnihexProvider.readFromStream(new FastBufferedInputStream(zipInputStream), readerOutput); } } CodepointMap codepointMap2 = new CodepointMap<>(UnihexProvider.Glyph[]::new, UnihexProvider.Glyph[][]::new); for (UnihexProvider.OverrideRange overrideRange : this.sizeOverrides) { int i = overrideRange.from; int j = overrideRange.to; UnihexProvider.Dimensions dimensions = overrideRange.dimensions; for (int k = i; k <= j; k++) { UnihexProvider.LineData lineData = codepointMap.remove(k); if (lineData != null) { codepointMap2.put(k, new UnihexProvider.Glyph(lineData, dimensions.left, dimensions.right)); } } } codepointMap.forEach((ix, lineDatax) -> { int jx = lineDatax.calculateWidth(); int kx = UnihexProvider.Dimensions.left(jx); int l = UnihexProvider.Dimensions.right(jx); codepointMap2.put(ix, new UnihexProvider.Glyph(lineDatax, kx, l)); }); var17 = new UnihexProvider(codepointMap2); } catch (Throwable var15) { try { zipInputStream.close(); } catch (Throwable var14) { var15.addSuppressed(var14); } throw var15; } zipInputStream.close(); return var17; } } @Environment(EnvType.CLIENT) public record Dimensions(int left, int right) { public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( instance -> instance.group( Codec.INT.fieldOf("left").forGetter(UnihexProvider.Dimensions::left), Codec.INT.fieldOf("right").forGetter(UnihexProvider.Dimensions::right) ) .apply(instance, UnihexProvider.Dimensions::new) ); public static final Codec CODEC = MAP_CODEC.codec(); public int pack() { return pack(this.left, this.right); } public static int pack(int left, int right) { return (left & 0xFF) << 8 | right & 0xFF; } public static int left(int packedDimensions) { return (byte)(packedDimensions >> 8); } public static int right(int packedDimensions) { return (byte)packedDimensions; } } @Environment(EnvType.CLIENT) record Glyph(UnihexProvider.LineData contents, int left, int right) implements GlyphInfo { public int width() { return this.right - this.left + 1; } @Override public float getAdvance() { return this.width() / 2 + 1; } @Override public float getShadowOffset() { return 0.5F; } @Override public float getBoldOffset() { return 0.5F; } @Override public BakedGlyph bake(Function function) { return (BakedGlyph)function.apply(new 1(this)); } } @Environment(EnvType.CLIENT) record IntContents(int[] contents, int bitWidth) implements UnihexProvider.LineData { private static final int SIZE_24 = 24; @Override public int line(int index) { return this.contents[index]; } static UnihexProvider.LineData read24(int index, ByteList byteList) { int[] is = new int[16]; int i = 0; int j = 0; for (int k = 0; k < 16; k++) { int l = UnihexProvider.decodeHex(index, byteList, j++); int m = UnihexProvider.decodeHex(index, byteList, j++); int n = UnihexProvider.decodeHex(index, byteList, j++); int o = UnihexProvider.decodeHex(index, byteList, j++); int p = UnihexProvider.decodeHex(index, byteList, j++); int q = UnihexProvider.decodeHex(index, byteList, j++); int r = l << 20 | m << 16 | n << 12 | o << 8 | p << 4 | q; is[k] = r << 8; i |= r; } return new UnihexProvider.IntContents(is, 24); } public static UnihexProvider.LineData read32(int index, ByteList byteList) { int[] is = new int[16]; int i = 0; int j = 0; for (int k = 0; k < 16; k++) { int l = UnihexProvider.decodeHex(index, byteList, j++); int m = UnihexProvider.decodeHex(index, byteList, j++); int n = UnihexProvider.decodeHex(index, byteList, j++); int o = UnihexProvider.decodeHex(index, byteList, j++); int p = UnihexProvider.decodeHex(index, byteList, j++); int q = UnihexProvider.decodeHex(index, byteList, j++); int r = UnihexProvider.decodeHex(index, byteList, j++); int s = UnihexProvider.decodeHex(index, byteList, j++); int t = l << 28 | m << 24 | n << 20 | o << 16 | p << 12 | q << 8 | r << 4 | s; is[k] = t; i |= t; } return new UnihexProvider.IntContents(is, 32); } } @Environment(EnvType.CLIENT) public interface LineData { int line(int index); int bitWidth(); default int mask() { int i = 0; for (int j = 0; j < 16; j++) { i |= this.line(j); } return i; } default int calculateWidth() { int i = this.mask(); int j = this.bitWidth(); int k; int l; if (i == 0) { k = 0; l = j; } else { k = Integer.numberOfLeadingZeros(i); l = 32 - Integer.numberOfTrailingZeros(i) - 1; } return UnihexProvider.Dimensions.pack(k, l); } } @Environment(EnvType.CLIENT) record OverrideRange(int from, int to, UnihexProvider.Dimensions dimensions) { private static final Codec RAW_CODEC = RecordCodecBuilder.create( instance -> instance.group( ExtraCodecs.CODEPOINT.fieldOf("from").forGetter(UnihexProvider.OverrideRange::from), ExtraCodecs.CODEPOINT.fieldOf("to").forGetter(UnihexProvider.OverrideRange::to), UnihexProvider.Dimensions.MAP_CODEC.forGetter(UnihexProvider.OverrideRange::dimensions) ) .apply(instance, UnihexProvider.OverrideRange::new) ); public static final Codec CODEC = RAW_CODEC.validate( overrideRange -> overrideRange.from >= overrideRange.to ? DataResult.error(() -> "Invalid range: [" + overrideRange.from + ";" + overrideRange.to + "]") : DataResult.success(overrideRange) ); } @FunctionalInterface @Environment(EnvType.CLIENT) public interface ReaderOutput { void accept(int i, UnihexProvider.LineData lineData); } @Environment(EnvType.CLIENT) record ShortContents(short[] contents) implements UnihexProvider.LineData { @Override public int line(int index) { return this.contents[index] << 16; } static UnihexProvider.LineData read(int index, ByteList byteList) { short[] ss = new short[16]; int i = 0; for (int j = 0; j < 16; j++) { int k = UnihexProvider.decodeHex(index, byteList, i++); int l = UnihexProvider.decodeHex(index, byteList, i++); int m = UnihexProvider.decodeHex(index, byteList, i++); int n = UnihexProvider.decodeHex(index, byteList, i++); short s = (short)(k << 12 | l << 8 | m << 4 | n); ss[j] = s; } return new UnihexProvider.ShortContents(ss); } @Override public int bitWidth() { return 16; } } }