diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java index a328d2a05..74ef625f4 100644 --- a/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java @@ -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; diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/BitmapGlyph.java b/src/main/java/com/gtnewhorizons/angelica/client/font/BitmapGlyph.java new file mode 100644 index 000000000..bb8291982 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/BitmapGlyph.java @@ -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) { + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/FontProvider.java b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProvider.java index 48ca1f899..04c38ecc8 100644 --- a/src/main/java/com/gtnewhorizons/angelica/client/font/FontProvider.java +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProvider.java @@ -18,4 +18,6 @@ public interface FontProvider { float getShadowOffset(); ResourceLocation getTexture(char chr); float getYScaleMultiplier(); + + default float getBaselineShift() { return 0.0f; } } diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderBitmap.java b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderBitmap.java new file mode 100644 index 000000000..fe570ad76 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderBitmap.java @@ -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 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 loadProviders() { + final List 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; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderUnicode.java b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderUnicode.java index 01790801f..09c7018e4 100644 --- a/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderUnicode.java +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/FontProviderUnicode.java @@ -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 diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/FontStrategist.java b/src/main/java/com/gtnewhorizons/angelica/client/font/FontStrategist.java index 12ede4371..78338b25e 100644 --- a/src/main/java/com/gtnewhorizons/angelica/client/font/FontStrategist.java +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/FontStrategist.java @@ -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 packMap = new HashMap<>(); + final HashMap 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 bitmapProviders; + + private static List bitmapProviders() { + List 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 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 { diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/SupplementaryGlyphSource.java b/src/main/java/com/gtnewhorizons/angelica/client/font/SupplementaryGlyphSource.java new file mode 100644 index 000000000..bcfc10dac --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/SupplementaryGlyphSource.java @@ -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(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/etfuturum/EtFuturumFontCompat.java b/src/main/java/com/gtnewhorizons/angelica/compat/etfuturum/EtFuturumFontCompat.java new file mode 100644 index 000000000..5456a768b --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/etfuturum/EtFuturumFontCompat.java @@ -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; + } + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java index e55c82374..2993a3ea8 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/fontrenderer/MixinFontRenderer.java @@ -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 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 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 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)