Initial commit for modern bitmap and character providers

This commit is contained in:
Logan Phillips committed 2026-07-14 20:07:02 -04:00
1 parent 7017786a7c
commit 5a9eb8dc55
9 files changed
+700 -75

No files matched your search

@@ -10,6 +10,7 @@ import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LE
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.compat.etfuturum.EtFuturumFontCompat;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
@@ -702,30 +703,79 @@ public class BatchingFontRenderer {
if (chr == ColorCodeUtils.ESCAPED_AMPERSAND) { chr = '&'; }
if (FontConfig.enableCustomFont && FontConfig.enableGlyphReplacements) {
final char replacement = GlyphReplacements.getReplacementGlyph(chr);
if (replacement != 0) {
if (FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement)
) {
chr = replacement;
final boolean surrogatePair = EtFuturumFontCompat.MODERN_FONT_ENABLED && !unicodeFlag
&& Character.isHighSurrogate(chr) && (charIdx + 1) < stringEnd
&& Character.isLowSurrogate(string.charAt(charIdx + 1));
final int codepoint = surrogatePair ? Character.toCodePoint(chr, string.charAt(charIdx + 1)) : chr;
final SupplementaryGlyphSource supGlyph = surrogatePair ? supplementarySource(codepoint) : null;
final boolean supplementary = supGlyph != null;
if (!supplementary) {
if (EtFuturumFontCompat.MODERN_FONT_ENABLED && Character.isSurrogate(chr)) {
// Substitute U+FFFD for a bad malformed glyph
final boolean pairedHigh = Character.isHighSurrogate(chr) && charIdx + 1 < stringEnd
&& Character.isLowSurrogate(string.charAt(charIdx + 1));
final boolean pairedLow = Character.isLowSurrogate(chr) && charIdx > stringOffset
&& Character.isHighSurrogate(string.charAt(charIdx - 1));
if (!pairedHigh && !pairedLow) { warnIsolatedSurrogate(chr, string); }
chr = REPLACEMENT_GLYPH;
}
if (FontConfig.enableCustomFont && FontConfig.enableGlyphReplacements) {
final char replacement = GlyphReplacements.getReplacementGlyph(chr);
if (replacement != 0) {
if (FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement)
) {
chr = replacement;
}
}
}
if (curRandom) {
chr = FontProviderMC.get(this.isSGA).getRandomReplacement(chr);
}
}
if (curRandom) {
chr = FontProviderMC.get(this.isSGA).getRandomReplacement(chr);
float uStart, vStart, xAdvance, glyphW, uSz, vSz, shadowOffset, provYScale, provBaselineShift;
ResourceLocation texture;
boolean unifontGlyph;
if (supplementary) {
unifontGlyph = supGlyph.isUnifont();
uStart = supGlyph.getUStartCp(codepoint);
vStart = supGlyph.getVStartCp(codepoint);
xAdvance = supGlyph.getXAdvanceCp(codepoint) * glyphScaleX;
if (unifontGlyph) { xAdvance = (int) xAdvance; }
glyphW = supGlyph.getGlyphWCp(codepoint) * glyphScaleX;
uSz = supGlyph.getUSizeCp(codepoint);
vSz = supGlyph.getVSizeCp(codepoint);
shadowOffset = supGlyph.getShadowOffset();
provYScale = supGlyph.getYScaleMultiplier();
provBaselineShift = supGlyph.getBaselineShift();
texture = supGlyph.getTextureCp(codepoint);
} else {
final FontProvider fontProvider = FontStrategist.getFontProvider(this, chr, FontConfig.enableCustomFont, unicodeFlag);
unifontGlyph = fontProvider == FontProviderUnicode.get();
uStart = fontProvider.getUStart(chr);
vStart = fontProvider.getVStart(chr);
xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
if (unifontGlyph) { xAdvance = (int) xAdvance; }
glyphW = fontProvider.getGlyphW(chr) * glyphScaleX;
uSz = fontProvider.getUSize(chr);
vSz = fontProvider.getVSize(chr);
shadowOffset = fontProvider.getShadowOffset();
provYScale = fontProvider.getYScaleMultiplier();
provBaselineShift = fontProvider.getBaselineShift();
texture = fontProvider.getTexture(chr);
}
FontProvider fontProvider = FontStrategist.getFontProvider(this, chr, FontConfig.enableCustomFont, unicodeFlag);
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2);
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * fontProvider.getYScaleMultiplier();
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * provYScale / 2)
+ provBaselineShift * glyphScaleY;
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * provYScale;
visibleCharIndex++;
// Check ASCII space, NBSP, NNBSP
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
if (!supplementary && (chr == ' ' || chr == '\u00A0' || chr == '\u202F')) {
curX += 4 * this.getWhitespaceScale() + (curBold ? 1 : 0);
continue;
}
@@ -747,17 +797,9 @@ public class BatchingFontRenderer {
gradientCharIndex++;
}
final float uStart = fontProvider.getUStart(chr);
final float vStart = fontProvider.getVStart(chr);
final float xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
final float glyphW = fontProvider.getGlyphW(chr) * glyphScaleX;
final float uSz = fontProvider.getUSize(chr);
final float vSz = fontProvider.getVSize(chr);
final float itOff = curItalic ? 1.0F : 0.0F; // italic offset
final float shadowOffset = fontProvider.getShadowOffset();
final int shadowCopies = FontConfig.shadowCopies;
final int boldCopies = FontConfig.boldCopies;
final ResourceLocation texture = fontProvider.getTexture(chr);
final int idxId = idxWriterIndex;
// Wave: Y offset via sine wave
@@ -802,7 +844,7 @@ public class BatchingFontRenderer {
if (drawShadow) { charCount += shadowCopies * (curBold ? 2 : 1); }
if (curBold) { charCount += boldCopies; }
final int vtxCount = 4 * charCount;
pushDrawCmd(idxId, vtxCount / 2 * 3, texture, chr > 255);
pushDrawCmd(idxId, vtxCount / 2 * 3, texture, unifontGlyph);
curX += (xAdvance + (curBold ? 1.0f : 0.0f)) + getGlyphSpacing();
if (bookMode) { curX = (int) curX; }
@@ -821,6 +863,7 @@ public class BatchingFontRenderer {
pushDrawCmd(stIdx, 6, null, false);
strikethroughStartX = strikethroughEndX;
}
if (supplementary) { charIdx++; }
}
if (curUnderline && underlineStartX != underlineEndX) {
@@ -845,9 +888,64 @@ public class BatchingFontRenderer {
return curX + (enableShadow ? 1.0f : 0.0f);
}
public int getStringWidthBatched(String text) {
if (text == null) return 0;
text = ColorCodeUtils.convertAmpersandToSectionX(text);
final boolean unicodeFlag = underlying.getUnicodeFlag();
final float glyphScaleX = getGlyphScaleX();
final int len = text.length();
boolean bold = false;
int width = 0;
for (int i = 0; i < len; i++) {
final char c0 = text.charAt(i);
if (EtFuturumFontCompat.MODERN_FONT_ENABLED && !unicodeFlag && Character.isHighSurrogate(c0)
&& i + 1 < len && Character.isLowSurrogate(text.charAt(i + 1))) {
final int cp = Character.toCodePoint(c0, text.charAt(i + 1));
final SupplementaryGlyphSource src = supplementarySource(cp);
if (src != null) {
final int w = (int) (src.getXAdvanceCp(cp) * glyphScaleX);
width += w;
if (bold && w > 0) width += 1;
i++;
continue;
}
}
int k = (int) getCharWidthFine(c0);
if (k < 0 && i < len - 1) {
// §-format code
i++;
final char code = text.charAt(i);
if (code == 'l' || code == 'L') {
bold = true;
} else if (code == 'r' || code == 'R') {
bold = false;
}
k = 0;
}
width += k;
if (bold && k > 0) width += 1;
}
return width;
}
private static final char REPLACEMENT_GLYPH = '\uFFFD';
private static boolean warnedIsolatedSurrogate;
private static void warnIsolatedSurrogate(char chr, CharSequence string) {
if (warnedIsolatedSurrogate) { return; }
warnedIsolatedSurrogate = true;
FontStrategist.LOGGER.warn(
"Split emoji/extended char U+{} in \"{}\". A GUI split a supplementary character into separate Java chars during cursor/text ops. Rendering U+FFFD instead.",
Integer.toHexString(chr).toUpperCase(), string, new Throwable("isolated-surrogate draw site"));
}
public float getCharWidthFine(char chr) {
if (chr == ColorCodeUtils.ESCAPED_AMPERSAND) { chr = '&'; }
if (chr == FORMATTING_CHAR) { return -1; }
if (EtFuturumFontCompat.MODERN_FONT_ENABLED && Character.isSurrogate(chr)) { chr = REPLACEMENT_GLYPH; }
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
return 4 * this.getWhitespaceScale();
@@ -858,6 +956,20 @@ public class BatchingFontRenderer {
return fp.getXAdvance(chr) * this.getGlyphScaleX();
}
public int supplementaryAdvance(int codepoint) {
if (!EtFuturumFontCompat.MODERN_FONT_ENABLED) return -1;
final SupplementaryGlyphSource src = supplementarySource(codepoint);
return src != null ? (int) (src.getXAdvanceCp(codepoint) * getGlyphScaleX()) : -1;
}
private SupplementaryGlyphSource supplementarySource(int codepoint) {
final FontProviderBitmap bitmap = (!bookMode && !isSplash) ? FontStrategist.findSupplementaryBitmap(codepoint) : null;
if (bitmap != null) return bitmap;
final FontProviderUnicode unicode = FontProviderUnicode.get();
return unicode.hasCodepoint(codepoint) ? unicode : null;
}
public void overrideBlendFunc(int srcRgb, int dstRgb) {
blendSrcRGB = srcRgb;
blendDstRGB = dstRgb;
@@ -0,0 +1,8 @@
package com.gtnewhorizons.angelica.client.font;
import net.minecraft.util.ResourceLocation;
public record BitmapGlyph(ResourceLocation texture, float uStart, float vStart, float uSize, float vSize, float glyphW,
float advance) {
}
@@ -18,4 +18,6 @@ public interface FontProvider {
float getShadowOffset();
ResourceLocation getTexture(char chr);
float getYScaleMultiplier();
default float getBaselineShift() { return 0.0f; }
}
@@ -0,0 +1,211 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.gtnewhorizons.angelica.config.FontConfig;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* A backported modern hand-drawn bitmap font provider.
* Et Futurum: Requium provides the resources.
*/
public final class FontProviderBitmap implements SupplementaryGlyphSource {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final ResourceLocation HANDDRAWN_DEFINITION = new ResourceLocation("etfuturum", "font/handdrawn.json");
private static final float ASCII_CELL = 8.0f;
private static final float ASCII_ASCENT = 7.0f;
private final Int2ObjectOpenHashMap<BitmapGlyph> glyphs = new Int2ObjectOpenHashMap<>();
private final float yScale;
private final float baselineShift;
private FontProviderBitmap(int renderHeight, int ascent) {
this.yScale = renderHeight / ASCII_CELL;
this.baselineShift = (ASCII_ASCENT - 4.0f) + 4.0f * this.yScale - ascent;
}
public static List<FontProviderBitmap> loadProviders() {
final List<FontProviderBitmap> providers = new ArrayList<>();
final IResourceManager rm = Minecraft.getMinecraft().getResourceManager();
try (InputStreamReader reader = new InputStreamReader(
rm.getResource(HANDDRAWN_DEFINITION).getInputStream(), StandardCharsets.UTF_8)) {
final JsonObject root = new JsonParser().parse(reader).getAsJsonObject();
final JsonArray defs = root.getAsJsonArray("providers");
int glyphCount = 0;
for (int i = 0; i < defs.size(); i++) {
final FontProviderBitmap p = fromDefinition(defs.get(i).getAsJsonObject(), rm);
if (p != null && !p.glyphs.isEmpty()) {
providers.add(p);
glyphCount += p.glyphs.size();
}
}
if (!providers.isEmpty()) {
LOGGER.info("Loaded {} modern hand-drawn font glyph(s) across {} provider(s)", glyphCount, providers.size());
}
} catch (Exception e) {
LOGGER.debug("Modern hand-drawn font sheets not loaded ({})", e.toString());
}
return providers;
}
private static FontProviderBitmap fromDefinition(JsonObject def, IResourceManager rm) {
try {
final String file = def.get("file").getAsString();
final int height = def.has("height") ? def.get("height").getAsInt() : (int) ASCII_CELL;
final int ascent = def.get("ascent").getAsInt();
final JsonArray charRows = def.getAsJsonArray("chars");
// "namespace:font/x.png" -> "namespace:textures/font/x.png"
final int colon = file.indexOf(':');
final String namespace = colon >= 0 ? file.substring(0, colon) : "minecraft";
final String path = colon >= 0 ? file.substring(colon + 1) : file;
final ResourceLocation texture = new ResourceLocation(namespace, "textures/" + path);
final BufferedImage image = ImageIO.read(rm.getResource(texture).getInputStream());
final int texW = image.getWidth();
final int texH = image.getHeight();
final int rows = charRows.size();
final String firstRow = charRows.get(0).getAsString();
final int cols = firstRow.codePointCount(0, firstRow.length());
final int cellW = texW / cols;
final int cellH = texH / rows;
final float scale = (float) height / cellH;
final FontProviderBitmap provider = new FontProviderBitmap(height, ascent);
for (int row = 0; row < rows; row++) {
final String line = charRows.get(row).getAsString();
int col = 0;
for (int offset = 0; offset < line.length(); ) {
final int cp = line.codePointAt(offset);
offset += Character.charCount(cp);
if (cp != 0 && !provider.glyphs.containsKey(cp)) {
final int cellX = col * cellW;
final int cellY = row * cellH;
final int ink = inkWidth(image, cellX, cellY, cellW, cellH);
final float advance = (int) (0.5F + ink * scale) + 1.0f;
final float glyphW = cellW * scale + 1.0f;
provider.glyphs.put(cp, new BitmapGlyph(texture,
(float) cellX / texW, (float) cellY / texH, (float) cellW / texW, (float) cellH / texH,
glyphW, advance));
}
col++;
}
}
return provider;
} catch (Exception e) {
LOGGER.debug("Skipping a hand-drawn font provider ({})", e.toString());
return null;
}
}
/** Rightmost inked column + 1 within a cell. */
private static int inkWidth(BufferedImage image, int cellX, int cellY, int cellW, int cellH) {
for (int x = cellW - 1; x >= 0; x--) {
for (int y = 0; y < cellH; y++) {
if ((image.getRGB(cellX + x, cellY + y) >>> 24) != 0) {
return x + 1;
}
}
}
return 0;
}
private int lastCp = -1;
private BitmapGlyph lastGlyph;
private BitmapGlyph glyph(int cp) {
if (cp == this.lastCp && this.lastGlyph != null) {
return this.lastGlyph;
}
final BitmapGlyph g = this.glyphs.get(cp);
this.lastCp = cp;
this.lastGlyph = g;
return g;
}
public boolean hasGlyph(int cp) { return this.glyphs.containsKey(cp); }
@Override
public float getUStartCp(int cp) { return glyph(cp).uStart(); }
@Override
public float getVStartCp(int cp) { return glyph(cp).vStart(); }
@Override
public float getXAdvanceCp(int cp) { return glyph(cp).advance(); }
@Override
public float getGlyphWCp(int cp) { return glyph(cp).glyphW(); }
@Override
public float getUSizeCp(int cp) { return glyph(cp).uSize(); }
@Override
public float getVSizeCp(int cp) { return glyph(cp).vSize(); }
@Override
public ResourceLocation getTextureCp(int cp) { return glyph(cp).texture(); }
@Override
public boolean isUnifont() { return false; }
@Override
public boolean isGlyphAvailable(char chr) { return hasGlyph(chr); }
@Override
public char getRandomReplacement(char chr) {
return chr;
}
@Override
public float getUStart(char chr) { return getUStartCp(chr); }
@Override
public float getVStart(char chr) { return getVStartCp(chr); }
@Override
public float getXAdvance(char chr) { return getXAdvanceCp(chr); }
@Override
public float getGlyphW(char chr) { return getGlyphWCp(chr); }
@Override
public float getUSize(char chr) { return getUSizeCp(chr); }
@Override
public float getVSize(char chr) { return getVSizeCp(chr); }
@Override
public float getShadowOffset() {
return FontConfig.fontShadowOffset;
}
@Override
public ResourceLocation getTexture(char chr) { return getTextureCp(chr); }
@Override
public float getYScaleMultiplier() {
return this.yScale;
}
@Override
public float getBaselineShift() {
return this.baselineShift;
}
}
@@ -3,43 +3,104 @@ package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.config.FontConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
public final class FontProviderUnicode implements FontProvider {
public final class FontProviderUnicode implements SupplementaryGlyphSource {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private FontProviderUnicode() {
try {
InputStream inputstream = Minecraft.getMinecraft().getResourceManager()
.getResource(new ResourceLocation("font/glyph_sizes.bin")).getInputStream();
//noinspection ResultOfMethodCallIgnored
inputstream.read(this.glyphWidth);
}
private void loadBmpWidths() {
try (InputStream in = Minecraft.getMinecraft().getResourceManager()
.getResource(new ResourceLocation("font/glyph_sizes.bin")).getInputStream()) {
readWidths(in, this.glyphWidth);
} catch (IOException e) {
LOGGER.warn("Failed to load font/glyph_sizes.bin; BMP glyph widths default to 0", e);
}
catch (IOException ioexception) {
throw new RuntimeException(ioexception);
}
public static void readWidths(InputStream in, byte[] dest) throws IOException {
int off = 0, n;
while (off < dest.length && (n = in.read(dest, off, dest.length - off)) != -1) {
off += n;
}
}
private static class InstLoader { static final FontProviderUnicode instance = new FontProviderUnicode(); }
public static FontProviderUnicode get() { return FontProviderUnicode.InstLoader.instance; }
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[256];
private static final int SUPPLEMENTARY_BASE = 0x10000;
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[0x1100];
private final byte[] glyphWidth = new byte[65536];
private boolean bmpLoaded;
private byte[] hiGlyphWidth;
private boolean hiLoaded;
private ResourceLocation getUnicodePageLocation(int page) {
final ResourceLocation lookup = unicodePageLocations[page];
if (lookup == null) {
final ResourceLocation rl = new ResourceLocation(String.format(
"textures/font/unicode_page_%02x.png",
page));
if (page >= 0 && page < unicodePageLocations.length) {
final ResourceLocation lookup = unicodePageLocations[page];
if (lookup != null) return lookup;
// %02x is a minimum width, so supplementary pages still format correctly
final ResourceLocation rl = new ResourceLocation(String.format("textures/font/unicode_page_%02x.png", page));
unicodePageLocations[page] = rl;
return rl;
} else {
return lookup;
}
return new ResourceLocation(String.format("textures/font/unicode_page_%02x.png", page));
}
private int widthByte(int codepoint) {
if (codepoint < SUPPLEMENTARY_BASE) {
ensureBmpWidths();
return this.glyphWidth[codepoint];
}
ensureHiWidths();
final int idx = codepoint - SUPPLEMENTARY_BASE;
return idx < this.hiGlyphWidth.length ? this.hiGlyphWidth[idx] : 0;
}
private void ensureBmpWidths() {
if (this.bmpLoaded) return;
this.bmpLoaded = true;
loadBmpWidths();
}
private void ensureHiWidths() {
if (this.hiLoaded) return;
this.hiLoaded = true;
this.hiGlyphWidth = new byte[0];
try (InputStream in = Minecraft.getMinecraft().getResourceManager()
.getResource(new ResourceLocation("font/glyph_sizes_hi.bin")).getInputStream()) {
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
byte[] chunk = new byte[8192];
int n;
while ((n = in.read(chunk)) != -1) buf.write(chunk, 0, n);
this.hiGlyphWidth = buf.toByteArray();
} catch (IOException e) {
LOGGER.debug("font/glyph_sizes_hi.bin not loaded ({}); supplementary glyph widths default to 0", e.toString());
}
}
public void reload() {
this.bmpLoaded = false;
this.hiLoaded = false;
this.hiGlyphWidth = null;
}
public boolean hasCodepoint(int codepoint) {
return widthByte(codepoint) != 0;
}
@Override
public boolean isUnifont() { return true; }
@Override
public boolean isGlyphAvailable(char chr) {
return true;
@@ -50,46 +111,61 @@ public final class FontProviderUnicode implements FontProvider {
return chr;
}
@Override
public float getUStart(char chr) {
final float startColumnF = (float)((this.glyphWidth[chr] >>> 4) & 15);
return ((float) (chr % 16 * 16) + startColumnF + 0.21f) / 256.0f;
public float getUStartCp(int cp) {
final float startColumnF = (float) ((widthByte(cp) >>> 4) & 15);
return ((float) ((cp & 15) * 16) + startColumnF + 0.21f) / 256.0f;
}
@Override
public float getVStart(char chr) {
return ((float) ((chr & 255) / 16 * 16) + 0.21f) / 256.0f;
public float getVStartCp(int cp) {
return ((float) (((cp & 255) >> 4) * 16) + 0.21f) / 256.0f;
}
@Override
public float getXAdvance(char chr) {
final int startColumn = (this.glyphWidth[chr] >>> 4) & 15;
final int endColumn = this.glyphWidth[chr] & 15;
final float startColumnF = (float) startColumn;
final float endColumnF = (float) (endColumn + 1);
return (endColumnF - startColumnF) / 2.0F + 1.0F;
public float getXAdvanceCp(int cp) {
final int w = widthByte(cp);
final int startColumn = (w >>> 4) & 15;
final int endColumn = w & 15;
return ((endColumn + 1) - startColumn) / 2.0F + 1.0F;
}
@Override
public float getGlyphW(char chr) {
final int startColumn = (this.glyphWidth[chr] >>> 4) & 15;
final int endColumn = this.glyphWidth[chr] & 15;
final float startColumnF = (float) startColumn;
final float endColumnF = (float) (endColumn + 1);
final float chrWidth = endColumnF - startColumnF - 0.02F;
public float getGlyphWCp(int cp) {
final int w = widthByte(cp);
final int startColumn = (w >>> 4) & 15;
final int endColumn = w & 15;
final float chrWidth = (endColumn + 1) - startColumn - 0.02F;
return chrWidth / 2.0f + 1.0f;
}
@Override
public float getUSize(char chr) {
final int startColumn = (this.glyphWidth[chr] >>> 4) & 15;
final int endColumn = this.glyphWidth[chr] & 15;
final float startColumnF = (float) startColumn;
final float endColumnF = (float) (endColumn + 1);
final float chrWidth = endColumnF - startColumnF - 0.02F;
public float getUSizeCp(int cp) {
final int w = widthByte(cp);
final int startColumn = (w >>> 4) & 15;
final int endColumn = w & 15;
final float chrWidth = (endColumn + 1) - startColumn - 0.02F;
return (chrWidth - 0.42f) / 256.0f;
}
public float getVSizeCp(int cp) {
return getVSize((char) 0);
}
public ResourceLocation getTextureCp(int cp) {
return getUnicodePageLocation(cp >>> 8);
}
@Override
public float getUStart(char chr) { return getUStartCp(chr); }
@Override
public float getVStart(char chr) { return getVStartCp(chr); }
@Override
public float getXAdvance(char chr) { return getXAdvanceCp(chr); }
@Override
public float getGlyphW(char chr) { return getGlyphWCp(chr); }
@Override
public float getUSize(char chr) { return getUSizeCp(chr); }
@Override
public float getVSize(char chr) {
return (16.0f - 0.42f) / 256.0f;
@@ -102,8 +178,7 @@ public final class FontProviderUnicode implements FontProvider {
@Override
public ResourceLocation getTexture(char chr) {
final int uniPage = chr / 256;
return getUnicodePageLocation(uniPage);
return getTextureCp(chr);
}
@Override
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.HashMultiset;
import com.gtnewhorizons.angelica.compat.etfuturum.EtFuturumFontCompat;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.mixins.interfaces.ResourceAccessor;
import cpw.mods.fml.client.SplashProgress;
@@ -8,6 +9,7 @@ import lombok.Getter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.resources.DefaultResourcePack;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -17,7 +19,10 @@ import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
@@ -70,13 +75,28 @@ public class FontStrategist {
availableFonts = fontSet.values().stream().sorted(Comparator.comparing(Font::getFontName)).toArray(Font[]::new);
// create and add the resource pack that provides fonts
HashMap<String, File> packMap = new HashMap<>();
final HashMap<String, File> packMap = new HashMap<>();
for (int i = 0; i < FontProviderCustom.ATLAS_COUNT; i++) {
packMap.put(FontProviderCustom.getPrimary().getAtlasResourceName(i), new File(FontProviderCustom.getPrimary().getAtlasFullPath(i)));
packMap.put(FontProviderCustom.getFallback().getAtlasResourceName(i), new File(FontProviderCustom.getFallback().getAtlasFullPath(i)));
}
DefaultResourcePack fontResourcePack = new DefaultResourcePack(packMap);
// Override vanilla resource pack with our textures
DefaultResourcePack fontResourcePack = new DefaultResourcePack(packMap) {
@Override
public InputStream getInputStream(ResourceLocation location) throws IOException {
final File file = packMap.get(location.toString());
if (file != null && file.isFile()) {
return new FileInputStream(file);
}
throw new FileNotFoundException(location.getResourcePath());
}
@Override
public boolean resourceExists(ResourceLocation location) {
return packMap.containsKey(location.toString());
}
};
List defaultResourcePacks = ((ResourceAccessor) Minecraft.getMinecraft()).angelica$getDefaultResourcePacks();
defaultResourcePacks.add(fontResourcePack);
}
@@ -119,6 +139,43 @@ public class FontStrategist {
if (loaded > 0) LOGGER.info("Loaded {} bundled font(s) from the pack", loaded);
}
private static volatile List<FontProviderBitmap> bitmapProviders;
private static List<FontProviderBitmap> bitmapProviders() {
List<FontProviderBitmap> local = bitmapProviders;
if (local == null) {
synchronized (FontStrategist.class) {
local = bitmapProviders;
if (local == null) {
local = FontProviderBitmap.loadProviders();
bitmapProviders = local;
}
}
}
return local;
}
private static FontProviderBitmap findBitmap(int codepoint) {
final List<FontProviderBitmap> providers = bitmapProviders();
for (final FontProviderBitmap provider : providers) {
if (provider.hasGlyph(codepoint)) {
return provider;
}
}
return null;
}
public static FontProviderBitmap findSupplementaryBitmap(int codepoint) {
return EtFuturumFontCompat.MODERN_FONT_ENABLED ? findBitmap(codepoint) : null;
}
public static void reloadFontResources() {
synchronized (FontStrategist.class) {
bitmapProviders = null;
}
FontProviderUnicode.get().reload();
}
/**
Lets you get a FontProvider per char while respecting font priority and fallbacks, the unicode flag, whether
SGA is on, if we're in a splash screen, if a font can even display a character in the first place, etc.
@@ -136,8 +193,16 @@ public class FontStrategist {
if (fp.isGlyphAvailable(chr)) { return fp; }
fp = FontProviderCustom.getFallback();
if (fp.isGlyphAvailable(chr)) { return fp; }
if (!forceUnicode && EtFuturumFontCompat.MODERN_FONT_ENABLED) {
final FontProviderBitmap bitmap = findBitmap(chr);
if (bitmap != null) { return bitmap; }
}
return FontProviderUnicode.get();
} else {
if (!forceUnicode && !me.isSplash && EtFuturumFontCompat.MODERN_FONT_ENABLED) {
final FontProviderBitmap bitmap = findBitmap(chr);
if (bitmap != null) { return bitmap; }
}
if (!forceUnicode && FontProviderMC.get(false).isGlyphAvailable(chr)) {
return FontProviderMC.get(false);
} else {
@@ -0,0 +1,16 @@
package com.gtnewhorizons.angelica.client.font;
import net.minecraft.util.ResourceLocation;
public interface SupplementaryGlyphSource extends FontProvider {
float getUStartCp(int cp);
float getVStartCp(int cp);
float getXAdvanceCp(int cp);
float getGlyphWCp(int cp);
float getUSizeCp(int cp);
float getVSizeCp(int cp);
ResourceLocation getTextureCp(int cp);
boolean isUnifont();
}
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.compat.etfuturum;
import ganymedes01.etfuturum.configuration.configs.ConfigMixins;
public final class EtFuturumFontCompat {
public static final boolean MODERN_FONT_ENABLED = detect();
private EtFuturumFontCompat() {
}
private static boolean detect() {
try {
return ConfigMixins.unicodeFontPages;
} catch (LinkageError e) {
return false;
}
}
}
@@ -2,6 +2,12 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.fontrenderer;
import com.gtnewhorizon.gtnhlib.util.font.IFontParameters;
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
import com.gtnewhorizons.angelica.client.font.FontProviderUnicode;
import com.gtnewhorizons.angelica.client.font.FontStrategist;
import com.gtnewhorizons.angelica.compat.etfuturum.EtFuturumFontCompat;
import net.minecraft.client.resources.IResourceManager;
import java.io.IOException;
import java.io.InputStream;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_LENGTH;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
@@ -127,6 +133,17 @@ public abstract class MixinFontRenderer implements FontRendererAccessor, IFontPa
}
}
@Inject(method = "onResourceManagerReload", at = @At("TAIL"))
private void angelica$reloadModernFont(IResourceManager manager, CallbackInfo ci) {
// FML's SplashFontRenderer calls onResourceManagerReload(null) on the splash thread
if (manager == null) return;
FontStrategist.reloadFontResources();
try (InputStream in = manager.getResource(new ResourceLocation("font/glyph_sizes.bin")).getInputStream()) {
FontProviderUnicode.readWidths(in, this.glyphWidth);
} catch (IOException ignored) {
}
}
@Unique
private static boolean angelica$charInRange(char what, char fromInclusive, char toInclusive) {
return (what >= fromInclusive) && (what <= toInclusive);
@@ -311,15 +328,115 @@ public abstract class MixinFontRenderer implements FontRendererAccessor, IFontPa
}
}
if (styles.length() == 0 && effects.length() == 0) return lastColor;
if (styles.isEmpty() && effects.isEmpty()) return lastColor;
final StringBuilder result = EXTRACT_RESULT; result.setLength(0);
result.append(lastColor).append(styles).append(effects);
return result.toString();
}
@ModifyVariable(method = "getStringWidth", at = @At("HEAD"), argsOnly = true, ordinal = 0)
private String angelica$convertBeforeWidth(String str) {
return ColorCodeUtils.convertAmpersandToSectionX(str);
@Inject(method = "getStringWidth", at = @At("HEAD"), cancellable = true)
private void angelica$batchedStringWidth(String text, CallbackInfoReturnable<Integer> cir) {
cir.setReturnValue(angelica$getBatcher().getStringWidthBatched(text));
}
@Unique
private static boolean angelica$isFormatColor(char c) {
return angelica$charInRange(c, '0', '9') || angelica$charInRange(c, 'a', 'f') || angelica$charInRange(c, 'A', 'F');
}
@Unique
private boolean angelica$doesNotNeedsSurrogateWidth(String str) {
if (str == null || !EtFuturumFontCompat.MODERN_FONT_ENABLED || this.unicodeFlag) return true;
final int len = str.length();
for (int i = 0; i + 1 < len; i++) {
if (Character.isHighSurrogate(str.charAt(i)) && Character.isLowSurrogate(str.charAt(i + 1))) {
return false;
}
}
return true;
}
@Inject(method = "sizeStringToWidth", at = @At("HEAD"), cancellable = true)
private void angelica$surrogateAwareSize(String str, int wrapWidth, CallbackInfoReturnable<Integer> cir) {
if (angelica$doesNotNeedsSurrogateWidth(str)) return;
final BatchingFontRenderer batcher = angelica$getBatcher();
final int len = str.length();
int width = 0;
int lastSpace = -1;
boolean bold = false;
int idx = 0;
for (; idx < len; idx++) {
final char c0 = str.charAt(idx);
if (c0 == '\n') {
lastSpace = idx;
break;
} else if (c0 == FORMATTING_CHAR) {
if (idx < len - 1) {
final char code = str.charAt(idx + 1);
idx++;
if (code == 'l' || code == 'L') bold = true;
else if (code == 'r' || code == 'R' || angelica$isFormatColor(code)) bold = false;
}
continue;
} else {
if (c0 == ' ') lastSpace = idx;
if (EtFuturumFontCompat.MODERN_FONT_ENABLED && !this.unicodeFlag && Character.isHighSurrogate(c0)
&& idx + 1 < len && Character.isLowSurrogate(str.charAt(idx + 1))) {
final int sup = batcher.supplementaryAdvance(Character.toCodePoint(c0, str.charAt(idx + 1)));
if (sup >= 0) {
width += sup;
if (bold) width++;
if (width > wrapWidth) break;
idx++;
continue;
}
}
width += (int) batcher.getCharWidthFine(c0);
if (bold) width++;
}
if (width > wrapWidth) break;
}
cir.setReturnValue(idx != len && lastSpace != -1 && lastSpace < idx ? lastSpace : idx);
}
@Inject(method = "trimStringToWidth(Ljava/lang/String;IZ)Ljava/lang/String;", at = @At("HEAD"), cancellable = true)
private void angelica$surrogateAwareTrim(String str, int width, boolean reverse, CallbackInfoReturnable<String> cir) {
if (reverse || angelica$doesNotNeedsSurrogateWidth(str)) return;
final BatchingFontRenderer batcher = angelica$getBatcher();
final StringBuilder sb = new StringBuilder();
final int len = str.length();
int accum = 0;
boolean prevFormat = false;
boolean bold = false;
for (int i = 0; i < len && accum < width; i++) {
final char c0 = str.charAt(i);
if (prevFormat) {
// Vanilla trim only toggles bold on §l / §r here; color codes do not reset it
prevFormat = false;
if (c0 == 'l' || c0 == 'L') bold = true;
else if (c0 == 'r' || c0 == 'R') bold = false;
} else if (c0 == FORMATTING_CHAR) {
prevFormat = true;
} else {
boolean pair = false;
int adv;
if (EtFuturumFontCompat.MODERN_FONT_ENABLED && !this.unicodeFlag && Character.isHighSurrogate(c0)
&& i + 1 < len && Character.isLowSurrogate(str.charAt(i + 1))) {
final int sup = batcher.supplementaryAdvance(Character.toCodePoint(c0, str.charAt(i + 1)));
if (sup >= 0) { adv = sup; pair = true; } else adv = (int) batcher.getCharWidthFine(c0);
} else {
adv = (int) batcher.getCharWidthFine(c0);
}
accum += adv;
if (bold) accum++;
if (accum > width) break;
sb.append(c0);
if (pair) { sb.append(str.charAt(i + 1)); i++; }
continue;
}
sb.append(c0);
}
cir.setReturnValue(sb.toString());
}
@ModifyVariable(method = "listFormattedStringToWidth", at = @At("HEAD"), argsOnly = true, ordinal = 0)