Files
CustomNPC-Plus/src/main/java/noppes/npcs/config/StringCache.java
T
2025-03-13 17:14:45 -04:00

1219 lines
57 KiB
Java

package noppes.npcs.config;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import java.awt.*;
import java.awt.font.GlyphVector;
import java.lang.ref.WeakReference;
import java.text.Bidi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.WeakHashMap;
/**
* @author https://github.com/secretdataz/BetterFonts
* The StringCache is the public interface for rendering of all Unicode strings using OpenType fonts. It caches the glyph layout
* of individual strings, and it uses a GlyphCache instance to cache the pre-rendered images for individual glyphs. Once a string
* and its glyph images are cached, the critical path in renderString() will draw the glyphs as fast as if using a bitmap font.
* Strings are cached using weak references through a two layer string cache. Strings that are no longer in use by Minecraft will
* be evicted from the cache, while the pre-rendered images of individual glyphs remains cached forever. The following diagram
* illustrates how this works:
*
* <pre>
* String passed to Key object considers Entry object holds Each Glyph object GlyphCache.Entry stores
* renderString(); all ASCII digits equal an array of Glyph belongs to only one the texture ID, image
* mapped by weak to zero ('0'); objects which may Entry object; it has width/height and
* weakRefCache mapped by weak not directly the glyph x/y pos normalized texture
* to Key object stringCahe to Entry correspond to Unicode within the string coordinates.
* chars in string
* String("Fi1") ------------\ ---> Glyph("F") ----------> GlyphCache.Entry("F")
* N:1 \ 1:1 1:N / N:1
* String("Fi4") ------------> Key("Fi0") -------------> Entry("Fi0") -----+----> Glyph("i") ----------> GlyphCache.Entry("i")
* \ N:1
* ---> Glyph("0") -----\
* ----> GlyphCache.Entry("0")
* ---> Glyph("0") -----/
* N:1 1:1 1:N / N:1
* String("Be1") ------------> Key("Be0") -------------> Entry("Be0") -----+----> Glyph("e") ----------> GlyphCache.Entry("e")
* \ N:1
* ---> Glyph("B") ----------> GlyphCache.Entry("B")
* </pre>
*/
public class StringCache {
/**
* Vertical adjustment (in pixels * 2) to string position because Minecraft uses top of string instead of baseline
*/
private static final int BASELINE_OFFSET = 7;
/**
* Offset from the string's baseline as which to draw the underline (in pixels)
*/
private static final int UNDERLINE_OFFSET = 1;
/**
* Thickness of the underline (in pixels)
*/
private static final int UNDERLINE_THICKNESS = 2;
/**
* Offset from the string's baseline as which to draw the strikethrough line (in pixels)
*/
private static final int STRIKETHROUGH_OFFSET = -6;
/**
* Thickness of the strikethrough line (in pixels)
*/
private static final int STRIKETHROUGH_THICKNESS = 2;
/**
* Reference to the unicode.FontRenderer class. Needed for creating GlyphVectors and retrieving glyph texture coordinates.
*/
private GlyphCache glyphCache;
/**
* Color codes from original FontRender class. First 16 entries are the primary chat colors; second 16 are darker versions
* used for drop shadows.
*/
private int colorTable[];
/**
* A cache of recently seen strings to their fully layed-out state, complete with color changes and texture coordinates of
* all pre-rendered glyph images needed to display this string. The weakRefCache holds strong references to the Key
* objects used in this map.
*/
private WeakHashMap<Key, Entry> stringCache = new WeakHashMap();
/**
* Every String passed to the public renderString() function is added to this WeakHashMap. As long as As long as Minecraft
* continues to hold a strong reference to the String object (i.e. from TileEntitySign and ChatLine) passed here, the
* weakRefCache map will continue to hold a strong reference to the Key object that said strings all map to (multiple strings
* in weakRefCache can map to a single Key if those strings only differ by their ASCII digits).
*/
private WeakHashMap<String, Key> weakRefCache = new WeakHashMap();
/**
* Temporary Key object re-used for lookups with stringCache.get(). Using a temporary object like this avoids the overhead
* of allocating new objects in the critical rendering path. Of course, new Key objects are always created when adding
* a mapping to stringCache.
*/
private Key lookupKey = new Key();
/**
* Pre-cached glyphs for the ASCII digits 0-9 (in that order). Used by renderString() to substiture digit glyphs on the fly
* as a performance boost. The speed up is most noticable on the F3 screen which rapidly displays lots of changing numbers.
* The 4 element array is index by the font style (combination of Font.PLAIN, Font.BOLD, and Font.ITALIC), and each of the
* nested elements is index by the digit value 0-9.
*/
private Glyph[][] digitGlyphs = new Glyph[4][];
/**
* True if digitGlyphs[] has been assigned and cacheString() can begin replacing all digits with '0' in the string.
*/
private boolean digitGlyphsReady = false;
/**
* If true, then enble GL_BLEND in renderString() so anti-aliasing font glyphs show up properly.
*/
private boolean antiAliasEnabled = false;
/**
* Reference to the main Minecraft thread that created this GlyphCache object. Starting with Minecraft 1.3.1, it is possible
* for GlyphCache.cacheGlyphs() to be invoked from the TcpReaderThread while processing a chat packet and computing the width
* of the incoming chat text. Unfortunately, if cacheGlyphs() makes any OpenGL calls from any thread except the main one,
* it will crash LWJGL with a NullPointerException. By remembering the initial thread and comparing it later against
* Thread.currentThread(), the StringCache code can avoid calling cacheGlyphs() when it's not safe to do so.
*/
private Thread mainThread;
public int fontHeight;
/**
* Wraps a String and acts as the key into stringCache. The hashCode() and equals() methods consider all ASCII digits
* to be equal when hashing and comparing Key objects together. Therefore, Strings which only differ in their digits will
* be all hashed together into the same entry. The renderString() method will then substitute the correct digit glyph on
* the fly. This special digit handling gives a significant speedup on the F3 debug screen.
*/
static private class Key {
/**
* A copy of the String which this Key is indexing. A copy is used to avoid creating a strong reference to the original
* passed into renderString(). When the original String is no longer needed by Minecraft, it will be garbage collected
* and the WeakHashMaps in StringCache will allow this Key object and its associated Entry object to be garbage
* collected as well.
*/
public String str;
/**
* Computes a hash code on str in the same manner as the String class, except all ASCII digits hash as '0'
*
* @return the augmented hash code on str
*/
@Override
public int hashCode() {
int code = 0, length = str.length();
/*
* True if a section mark character was last seen. In this case, if the next character is a digit, it must
* not be considered equal to any other digit. This forces any string that differs in color codes only to
* have a separate entry in the StringCache.
*/
boolean colorCode = false;
for (int index = 0; index < length; index++) {
char c = str.charAt(index);
if (c >= '0' && c <= '9' && !colorCode) {
c = '0';
}
code = (code * 31) + c;
colorCode = (c == '\u00A7');
}
return code;
}
/**
* Compare str against another object (specifically, the object's string representation as returned by toString).
* All ASCII digits are considered equal by this method, as long as they are at the same index within the string.
*
* @return true if the strings are the identical, or only differ in their ASCII digits
*/
@Override
public boolean equals(Object o) {
/*
* There seems to be a timing window inside WeakHashMap itself where a null object can be passed to this
* equals() method. Presumably it happens between computing a hash code for the weakly referenced Key object
* while it still exists and calling its equals() method after it was garbage collected.
*/
if (o == null) {
return false;
}
/* Calling toString on a String object simply returns itself so no new object allocation is performed */
String other = o.toString();
int length = str.length();
if (length != other.length()) {
return false;
}
/*
* True if a section mark character was last seen. In this case, if the next character is a digit, it must
* not be considered equal to any other digit. This forces any string that differs in color codes only to
* have a separate entry in the StringCache.
*/
boolean colorCode = false;
for (int index = 0; index < length; index++) {
char c1 = str.charAt(index);
char c2 = other.charAt(index);
if (c1 != c2 && (c1 < '0' || c1 > '9' || c2 < '0' || c2 > '9' || colorCode)) {
return false;
}
colorCode = (c1 == '\u00A7');
}
return true;
}
/**
* Returns the contained String object within this Key.
*
* @return the str object
*/
@Override
public String toString() {
return str;
}
}
/**
* This entry holds the layed out glyph positions for the cached string along with some relevant metadata.
*/
static private class Entry {
/**
* A weak reference back to the Key object in stringCache that maps to this Entry.
*/
public WeakReference<Key> keyRef;
/**
* The total horizontal advance (i.e. width) for this string in pixels.
*/
public int advance;
/**
* Array of fully layed out glyphs for the string. Sorted by logical order of characters (i.e. glyph.stringIndex)
*/
public Glyph glyphs[];
/**
* Array of color code locations from the original string
*/
public ColorCode colors[];
/**
* True if the string uses strikethrough or underlines anywhere and needs an extra pass in renderString()
*/
public boolean specialRender;
}
/**
* Identifies the location and value of a single color code in the original string
*/
static private class ColorCode implements Comparable<Integer> {
/**
* Bit flag used with renderStyle to request the underline style
*/
public static final byte UNDERLINE = 1;
/**
* Bit flag used with renderStyle to request the strikethrough style
*/
public static final byte STRIKETHROUGH = 2;
/**
* The index into the original string (i.e. with color codes) for the location of this color code.
*/
public int stringIndex;
/**
* The index into the stripped string (i.e. with no color codes) of where this color code would have appeared
*/
public int stripIndex;
/**
* The numeric color code (i.e. index into the colorCode[] array); -1 to reset default color
*/
public byte colorCode;
/**
* Combination of Font.PLAIN, Font.BOLD, and Font.ITALIC specifying font specific syles
*/
public byte fontStyle;
/**
* Combination of UNDERLINE and STRIKETHROUGH flags specifying effects performed by renderString()
*/
public byte renderStyle;
/**
* Performs numeric comparison on stripIndex. Allows binary search on ColorCode arrays in layoutStyle.
*
* @param i the Integer object being compared
* @return either -1, 0, or 1 if this < other, this == other, or this > other
*/
@Override
public int compareTo(Integer i) {
return (stringIndex == i.intValue()) ? 0 : (stringIndex < i.intValue()) ? -1 : 1;
}
}
/**
* Identifies a single glyph in the layed-out string. Includes a reference to a GlyphCache.Entry with the OpenGL texture ID
* and position of the pre-rendered glyph image, and includes the x/y pixel coordinates of where this glyph occurs within
* the string to which this Glyph object belongs.
*/
static private class Glyph implements Comparable<Glyph> {
/**
* The index into the original string (i.e. with color codes) for the character that generated this glyph.
*/
public int stringIndex;
/**
* Texture ID and position/size of the glyph's pre-rendered image within the cache texture.
*/
public GlyphCache.Entry texture;
/**
* Glyph's horizontal position (in pixels) relative to the entire string's baseline
*/
public int x;
/**
* Glyph's vertical position (in pixels) relative to the entire string's baseline
*/
public int y;
/**
* Glyph's horizontal advance (in pixels) used for strikethrough and underline effects
*/
public int advance;
/**
* Allows arrays of Glyph objects to be sorted. Performs numeric comparison on stringIndex.
*
* @param o the other Glyph object being compared with this one
* @return either -1, 0, or 1 if this < other, this == other, or this > other
*/
@Override
public int compareTo(Glyph o) {
return (stringIndex == o.stringIndex) ? 0 : (stringIndex < o.stringIndex) ? -1 : 1;
}
}
/**
* A single StringCache object is allocated by Minecraft's FontRenderer which forwards all string drawing and requests for
* string width to this class.
*
* @param colors 32 element array of RGBA colors corresponding to the 16 text color codes followed by 16 darker version of the
* color codes for use as drop shadows
*/
public StringCache() {
/* StringCache is created by the main game thread; remember it for later thread safety checks */
mainThread = Thread.currentThread();
glyphCache = new GlyphCache();
colorTable = new int[32];
for (int i = 0; i < 32; ++i) {
int j = (i >> 3 & 1) * 85;
int k = (i >> 2 & 1) * 170 + j;
int l = (i >> 1 & 1) * 170 + j;
int i1 = (i >> 0 & 1) * 170 + j;
if (i == 6) {
k += 85;
}
if (Minecraft.getMinecraft().gameSettings.anaglyph) {
int j1 = (k * 30 + l * 59 + i1 * 11) / 100;
int k1 = (k * 30 + l * 70) / 100;
int l1 = (k * 30 + i1 * 70) / 100;
k = j1;
l = k1;
i1 = l1;
}
if (i >= 16) {
k /= 4;
l /= 4;
i1 /= 4;
}
this.colorTable[i] = (k & 255) << 16 | (l & 255) << 8 | i1 & 255;
}
/* Pre-cache the ASCII digits to allow for fast glyph substitution */
cacheDightGlyphs();
}
/**
* Change the default font used to pre-render glyph images. If this method is called at runtime, the string cache is flushed so that
* all visible strings will be immediately re-layed out using the new font selection.
*
* @param fontName the new font name
* @param fontSize the new point size
* @param antiAlias turn on anti aliasing
*/
public void setDefaultFont(String fontName, int fontSize, boolean antiAlias) {
/* Change the font in the glyph cache and clear the string cache so all strings have to be re-layed out and re-rendered */
glyphCache.setDefaultFont(fontName, fontSize, antiAlias);
antiAliasEnabled = antiAlias;
weakRefCache.clear();
stringCache.clear();
/* Pre-cache the ASCII digits to allow for fast glyph substitution */
cacheDightGlyphs();
updateHeight();
}
public Font usedFont() {
return glyphCache.usedFonts.get(0);
}
public void setCustomFont(ResourceLocation resource, int fontSize, boolean antiAlias) throws Exception {
/* Change the font in the glyph cache and clear the string cache so all strings have to be re-layed out and re-rendered */
glyphCache.setCustomFont(resource, fontSize, antiAlias);
antiAliasEnabled = antiAlias;
weakRefCache.clear();
stringCache.clear();
/* Pre-cache the ASCII digits to allow for fast glyph substitution */
cacheDightGlyphs();
updateHeight();
}
public void updateHeight() {
int height = 0;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (Glyph g : cacheString("AaBbCcDdEeHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz").glyphs) {
if (g.texture.height > height)
height = g.texture.height;
if (g.y < minY)
minY = g.y;
if (g.y > maxY)
maxY = g.y;
}
fontHeight = (int) ((maxY + height) / 2.0F - minY / 2.0F);
}
/**
* Pre-cache the ASCII digits to allow for fast glyph substitution. Called once from the constructor and called any time the font selection
* changes at runtime via setDefaultFont().
*/
private void cacheDightGlyphs() {
/* Need to cache each font style combination; the digitGlyphsReady = false disabled the normal glyph substitution mechanism */
digitGlyphsReady = false;
digitGlyphs[Font.PLAIN] = cacheString("0123456789").glyphs;
digitGlyphs[Font.BOLD] = cacheString("\u00A7l0123456789").glyphs;
digitGlyphs[Font.ITALIC] = cacheString("\u00A7o0123456789").glyphs;
digitGlyphs[Font.BOLD | Font.ITALIC] = cacheString("\u00A7l\u00A7o0123456789").glyphs;
digitGlyphsReady = true;
}
/**
* Render a single-line string to the screen using the current OpenGL color. The (x,y) coordinates are of the uppet-left
* corner of the string's bounding box, rather than the baseline position as is typical with fonts. This function will also
* add the string to the cache so the next renderString() call with the same string is faster.
*
* @param str the string being rendered; it can contain color codes
* @param startX the x coordinate to draw at
* @param startY the y coordinate to draw at
* @param initialColor the initial RGBA color to use when drawing the string; embedded color codes can override the RGB component
* @param shadowFlag if true, color codes are replaces by a darker version used for drop shadows
* @return the total advance (horizontal distance) of this string
* @todo Add optional NumericShaper to replace ASCII digits with locale specific ones
* @todo Add support for the "k" code which randomly replaces letters on each render (used only by splash screen)
* @todo Pre-sort by texture to minimize binds; can store colors per glyph in string cache
* @todo Optimize the underline/strikethrough drawing to draw a single line for each run
*/
public int renderString(String str, int startX, int startY, int initialColor, boolean shadowFlag) {
/* Check for invalid arguments */
if (str == null || str.isEmpty()) {
return 0;
}
/* Make sure the entire string is cached before rendering and return its glyph representation */
Entry entry = cacheString(str);
/* Adjust the baseline of the string because the startY coordinate in Minecraft is for the top of the string */
startY += BASELINE_OFFSET;
/* Color currently selected by color code; reapplied to Tessellator instance after glBindTexture() */
if ((initialColor >> 24 & 0xff) == 0)
initialColor += 0xFF000000;
int color = initialColor;
/* Track which texture is currently bound to minimize the number of glBindTexture() and Tessellator.draw() calls needed */
int boundTextureName = 0;
/*
* This color change will have no effect on the actual text (since colors are included in the Tessellator vertex
* array), however GuiEditSign of all things depends on having the current color set to white when it renders its
* "Edit sign message:" text. Otherwise, the sign which is rendered underneath would look too dark.
*/
GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); //fixes black text
GL11.glColor4f((color >> 16 & 0xff) / 255f, (color >> 8 & 0xff) / 255f, (color & 0xff) / 255f, 1);
/*
* Enable GL_BLEND in case the font is drawn anti-aliased because Minecraft itself only enables blending for chat text
* (so it can fade out), but not GUI text or signs. Minecraft uses multiple blend functions so it has to be specified here
* as well for consistent blending. To reduce the overhead of OpenGL state changes and making native LWJGL calls, this
* function doesn't try to save/restore the blending state. Hopefully everything else that depends on blending in Minecraft
* will set its own state as needed.
*/
if (antiAliasEnabled) {
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
}
/* Using the Tessellator to queue up data in a vertex array and then draw all at once should be faster than immediate mode */
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff);
/* The currently active font syle is needed to select the proper ASCII digit style for fast replacement */
int fontStyle = Font.PLAIN;
for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) {
/*
* If the original string had a color code at this glyph's position, then change the current GL color that gets added
* to the vertex array. Note that only the RGB component of the color is replaced by a color code; the alpha component
* of the original color passed into this function will remain. The while loop handles multiple consecutive color codes,
* in which case only the last such color code takes effect.
*/
while (colorIndex < entry.colors.length && entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex) {
color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag);
fontStyle = entry.colors[colorIndex].fontStyle;
colorIndex++;
}
/* Select the current glyph's texture information and horizontal layout position within this string */
Glyph glyph = entry.glyphs[glyphIndex];
GlyphCache.Entry texture = glyph.texture;
int glyphX = glyph.x;
/*
* Replace ASCII digits in the string with their respective glyphs; strings differing by digits are only cached once.
* If the new replacement glyph has a different width than the original placeholder glyph (e.g. the '1' glyph is often
* narrower than other digits), re-center the new glyph over the placeholder's position to minimize the visual impact
* of the width mismatch.
*/
char c = str.charAt(glyph.stringIndex);
if (c >= '0' && c <= '9') {
int oldWidth = texture.width;
texture = digitGlyphs[fontStyle][c - '0'].texture;
int newWidth = texture.width;
glyphX += (oldWidth - newWidth) >> 1;
}
/*
* Make sure the OpenGL texture storing this glyph's image is bound (if not already bound). All pending glyphs in the
* Tessellator's vertex array must be drawn before switching textures, otherwise they would erroneously use the new
* texture as well.
*/
if (boundTextureName != texture.textureName)
//if(true)
{
tessellator.draw();
tessellator.startDrawingQuads();
tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture.textureName);
boundTextureName = texture.textureName;
}
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
float x1 = startX + (glyphX) / 2.0F;
float x2 = startX + (glyphX + texture.width) / 2.0F;
float y1 = startY + (glyph.y) / 2.0F;
float y2 = startY + (glyph.y + texture.height) / 2.0F;
tessellator.addVertexWithUV(x1, y1, 0, texture.u1, texture.v1);
tessellator.addVertexWithUV(x1, y2, 0, texture.u1, texture.v2);
tessellator.addVertexWithUV(x2, y2, 0, texture.u2, texture.v2);
tessellator.addVertexWithUV(x2, y1, 0, texture.u2, texture.v1);
}
/* Draw any remaining glyphs in the Tessellator's vertex array (there should be at least one glyph pending) */
tessellator.draw();
/* Draw strikethrough and underlines if the string uses them anywhere */
if (entry.specialRender) {
int renderStyle = 0;
/* Use initial color passed to renderString(); disable texturing to draw solid color lines */
color = initialColor;
GL11.glDisable(GL11.GL_TEXTURE_2D);
tessellator.startDrawingQuads();
tessellator.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff);
for (int glyphIndex = 0, colorIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) {
/*
* If the original string had a color code at this glyph's position, then change the current GL color that gets added
* to the vertex array. The while loop handles multiple consecutive color codes, in which case only the last such
* color code takes effect.
*/
while (colorIndex < entry.colors.length && entry.glyphs[glyphIndex].stringIndex >= entry.colors[colorIndex].stringIndex) {
color = applyColorCode(entry.colors[colorIndex].colorCode, initialColor, shadowFlag);
renderStyle = entry.colors[colorIndex].renderStyle;
colorIndex++;
}
/* Select the current glyph within this string for its layout position */
Glyph glyph = entry.glyphs[glyphIndex];
/* The strike/underlines are drawn beyond the glyph's width to include the extra space between glyphs */
int glyphSpace = glyph.advance - glyph.texture.width;
/* Draw underline under glyph if the style is enabled */
if ((renderStyle & ColorCode.UNDERLINE) != 0) {
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
float x1 = startX + (glyph.x - glyphSpace) / 2.0F;
float x2 = startX + (glyph.x + glyph.advance) / 2.0F;
float y1 = startY + (UNDERLINE_OFFSET) / 2.0F;
float y2 = startY + (UNDERLINE_OFFSET + UNDERLINE_THICKNESS) / 2.0F;
tessellator.addVertex(x1, y1, 0);
tessellator.addVertex(x1, y2, 0);
tessellator.addVertex(x2, y2, 0);
tessellator.addVertex(x2, y1, 0);
}
/* Draw strikethrough in the middle of glyph if the style is enabled */
if ((renderStyle & ColorCode.STRIKETHROUGH) != 0) {
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
float x1 = startX + (glyph.x - glyphSpace) / 2.0F;
float x2 = startX + (glyph.x + glyph.advance) / 2.0F;
float y1 = startY + (STRIKETHROUGH_OFFSET) / 2.0F;
float y2 = startY + (STRIKETHROUGH_OFFSET + STRIKETHROUGH_THICKNESS) / 2.0F;
tessellator.addVertex(x1, y1, 0);
tessellator.addVertex(x1, y2, 0);
tessellator.addVertex(x2, y2, 0);
tessellator.addVertex(x2, y1, 0);
}
}
/* Finish drawing the last strikethrough/underline segments */
tessellator.draw();
GL11.glEnable(GL11.GL_TEXTURE_2D);
}
/* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */
return entry.advance / 2;
}
/**
* Return the width of a string in pixels. Used for centering strings inside GUI buttons.
*
* @param str compute the width of this string
* @return the width in pixels (divided by 2; this matches the scaled coordinate system used by GUIs in Minecraft)
*/
public int getStringWidth(String str) {
/* Check for invalid arguments */
if (str == null || str.isEmpty()) {
return 0;
}
/* Make sure the entire string is cached and rendered since it will probably be used again in a renderString() call */
Entry entry = cacheString(str);
/* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */
return entry.advance / 2;
}
/**
* Return the number of characters in a string that will completly fit inside the specified width when rendered, with
* or without prefering to break the line at whitespace instead of breaking in the middle of a word. This private provides
* the real implementation of both sizeStringToWidth() and trimStringToWidth().
*
* @param str the String to analyze
* @param width the desired string width (in GUI coordinate system)
* @param breakAtSpaces set to prefer breaking line at spaces than in the middle of a word
* @return the number of characters from str that will fit inside width
*/
private int sizeString(String str, int width, boolean breakAtSpaces) {
/* Check for invalid arguments */
if (str == null || str.isEmpty()) {
return 0;
}
/* Convert the width from GUI coordinate system to pixels */
width += width;
/* The glyph array for a string is sorted by the string's logical character position */
Glyph glyphs[] = cacheString(str).glyphs;
/* Index of the last whitespace found in the string; used if breakAtSpaces is true */
int wsIndex = -1;
/* Add up the individual advance of each glyph until it exceeds the specified width */
int advance = 0, index = 0;
while (index < glyphs.length && advance <= width) {
/* Keep track of spaces if breakAtSpaces it set */
if (breakAtSpaces) {
char c = str.charAt(glyphs[index].stringIndex);
if (c == ' ') {
wsIndex = index;
} else if (c == '\n') {
wsIndex = index;
break;
}
}
advance += glyphs[index].advance;
index++;
}
/* Avoid splitting individual words if breakAtSpaces set; same test condition as in Minecraft's FontRenderer */
if (index < glyphs.length && wsIndex != -1 && wsIndex < index) {
index = wsIndex;
}
/* The string index of the last glyph that wouldn't fit gives the total desired length of the string in characters */
return index < glyphs.length ? glyphs[index].stringIndex : str.length();
}
/**
* Return the number of characters in a string that will completly fit inside the specified width when rendered.
*
* @param str the String to analyze
* @param width the desired string width (in GUI coordinate system)
* @return the number of characters from str that will fit inside width
*/
public int sizeStringToWidth(String str, int width) {
return sizeString(str, width, true);
}
/**
* Trim a string so that it fits in the specified width when rendered, optionally reversing the string
*
* @param str the String to trim
* @param width the desired string width (in GUI coordinate system)
* @param reverse if true, the returned string will also be reversed
* @return the trimmed and optionally reversed string
*/
public String trimStringToWidth(String str, int width, boolean reverse) {
int length = sizeString(str, width, false);
str = str.substring(0, length);
if (reverse) {
str = (new StringBuilder(str)).reverse().toString();
}
return str;
}
/**
* Apply a new vertex color to the Tessellator instance based on the numeric chat color code. Only the RGB component of the
* color is replaced by a color code; the alpha component of the original default color will remain.
*
* @param colorCode the chat color code as a number 0-15 or -1 to reset the default color
* @param color the default color used when the colorCode is -1
* @param shadowFlag ir true, the color code will select a darker version of the color suitable for drop shadows
* @return the new RGBA color set by this function
*/
private int applyColorCode(int colorCode, int color, boolean shadowFlag) {
/* A -1 color code indicates a reset to the initial color passed into renderString() */
if (colorCode != -1) {
colorCode = shadowFlag ? colorCode + 16 : colorCode;
color = colorTable[colorCode] & 0xffffff | color & 0xff000000;
}
Tessellator.instance.setColorRGBA(color >> 16 & 0xff, color >> 8 & 0xff, color & 0xff, color >> 24 & 0xff);
return color;
}
/**
* Add a string to the string cache by perform full layout on it, remembering its glyph positions, and making sure that
* every font glyph used by the string is pre-rendering. If this string has already been cached, then simply return its
* existing Entry from the cahe. Note that for caching purposes, this method considers two strings to be identical if they
* only differ in their ASCII digits; the renderString() method performs fast glyph substitution based on the actual digits
* in the string at the time.
*
* @param str this String will be layed out and added to the cache (or looked up, if alraedy cached)
* @return the string's cache entry containing all the glyph positions
*/
private Entry cacheString(String str) {
/*
* New Key object allocated only if the string was not found in the StringCache using lookupKey. This variable must
* be outside the (entry == null) code block to have a temporary strong reference between the time when the Key is
* added to stringCache and added to weakRefCache.
*/
Key key;
/* Either a newly created Entry object for the string, or the cached Entry if the string is already in the cache */
Entry entry = null;
/* Don't perform a cache lookup from other threads because the stringCache is not synchronized */
if (mainThread == Thread.currentThread()) {
/* Re-use existing lookupKey to avoid allocation overhead on the critical rendering path */
lookupKey.str = str;
/* If this string is already in the cache, simply return the cached Entry object */
entry = stringCache.get(lookupKey);
}
/* If string is not cached (or not on main thread) then layout the string */
if (entry == null) {
/* layoutGlyphVector() requires a char[] so create it here and pass it around to avoid duplication later on */
char text[] = str.toCharArray();
/* Strip all color codes from the string */
entry = new Entry();
int length = stripColorCodes(entry, str, text);
/* Layout the entire string, splitting it up by color codes and the Unicode bidirectional algorithm */
List<Glyph> glyphList = new ArrayList();
entry.advance = (int) layoutBidiString(glyphList, text, 0, length, entry.colors);
/* Convert the accumulated Glyph list to an array for efficient storage */
entry.glyphs = new Glyph[glyphList.size()];
entry.glyphs = glyphList.toArray(entry.glyphs);
/*
* Sort Glyph array by stringIndex so it can be compared during rendering to the already sorted ColorCode array.
* This will apply color codes in the string's logical character order and not the visual order on screen.
*/
Arrays.sort(entry.glyphs);
/* Do some post-processing on each Glyph object */
int colorIndex = 0, shift = 0;
for (int glyphIndex = 0; glyphIndex < entry.glyphs.length; glyphIndex++) {
Glyph glyph = entry.glyphs[glyphIndex];
/*
* Adjust the string index for each glyph to point into the original string with unstripped color codes. The while
* loop is necessary to handle multiple consecutive color codes with no visible glyphs between them. These new adjusted
* stringIndex can now be compared against the color stringIndex during rendering. It also allows lookups of ASCII
* digits in the original string for fast glyph replacement during rendering.
*/
while (colorIndex < entry.colors.length && glyph.stringIndex + shift >= entry.colors[colorIndex].stringIndex) {
shift += 2;
colorIndex++;
}
glyph.stringIndex += shift;
}
/*
* Do not actually cache the string when called from other threads because GlyphCache.cacheGlyphs() will not have been called
* and the cache entry does not contain any texture data needed for rendering.
*/
if (mainThread == Thread.currentThread()) {
/* Wrap the string in a Key object (to change how ASCII digits are compared) and cache it along with the newly generated Entry */
key = new Key();
/* Make a copy of the original String to avoid creating a strong reference to it */
key.str = new String(str);
entry.keyRef = new WeakReference(key);
stringCache.put(key, entry);
}
}
/* Do not access weakRefCache from other threads since it is unsynchronized, and for a newly created entry, the keyRef is null */
if (mainThread == Thread.currentThread()) {
/*
* Add the String passed into this method to the stringWeakMap so it keeps the Key reference live as long as the String is in use.
* If an existing Entry was already found in the stringCache, it's possible that its Key has already been garbage collected. The
* code below checks for this to avoid adding (str, null) entries into weakRefCache. Note that if a new Key object was created, it
* will still be live because of the strong reference created by the "key" variable.
*/
Key oldKey = entry.keyRef.get();
if (oldKey != null) {
weakRefCache.put(str, oldKey);
}
lookupKey.str = null;
}
/* Return either the existing or the newly created entry so it can be accessed immediately */
return entry;
}
/**
* Remove all color codes from the string by shifting data in the text[] array over so it overwrites them. The value of each
* color code and its position (relative to the new stripped text[]) is also recorded in a separate array. The color codes must
* be removed for a font's context sensitive glyph substitution to work (like Arabic letter middle form).
*
* @param cacheEntry each color change in the string will add a new ColorCode object to this list
* @param str the string from which color codes will be stripped
* @param text on input it should be an identical copy of str; on output it will be string with all color codes removed
* @return the length of the new stripped string in text[]; actual text.length will not change because the array is not reallocated
*/
private int stripColorCodes(Entry cacheEntry, String str, char text[]) {
List<ColorCode> colorList = new ArrayList();
int start = 0, shift = 0, next;
byte fontStyle = Font.PLAIN;
byte renderStyle = 0;
byte colorCode = -1;
/* Search for section mark characters indicating the start of a color code (but only if followed by at least one character) */
while ((next = str.indexOf('\u00A7', start)) != -1 && next + 1 < str.length()) {
/*
* Remove the two char color code from text[] by shifting the remaining data in the array over on top of it.
* The "start" and "next" variables all contain offsets into the original unmodified "str" string. The "shift"
* variable keeps track of how many characters have been sripped so far, and it's used to compute offsets into
* the text[] array based on the start/next offsets in the original string.
*/
System.arraycopy(text, next - shift + 2, text, next - shift, text.length - next - 2);
/* Decode escape code used in the string and change current font style / color based on it */
int code = "0123456789abcdefklmnor".indexOf(Character.toLowerCase(str.charAt(next + 1)));
switch (code) {
/* Random style; TODO: NOT IMPLEMENTED YET */
case 16:
break;
/* Bold style */
case 17:
fontStyle |= Font.BOLD;
break;
/* Strikethrough style */
case 18:
renderStyle |= ColorCode.STRIKETHROUGH;
cacheEntry.specialRender = true;
break;
/* Underline style */
case 19:
renderStyle |= ColorCode.UNDERLINE;
cacheEntry.specialRender = true;
break;
/* Italic style */
case 20:
fontStyle |= Font.ITALIC;
break;
/* Plain style */
case 21:
fontStyle = Font.PLAIN;
renderStyle = 0;
colorCode = -1; // This may be a bug in Minecraft's original FontRenderer
break;
/* Otherwise, must be a color code or some other unsupported code */
default:
if (code >= 0 && code <= 15) {
colorCode = (byte) code;
fontStyle = Font.PLAIN; // This may be a bug in Minecraft's original FontRenderer
renderStyle = 0; // This may be a bug in Minecraft's original FontRenderer
}
break;
}
/* Create a new ColorCode object that tracks the position of the code in the original string */
ColorCode entry = new ColorCode();
entry.stringIndex = next;
entry.stripIndex = next - shift;
entry.colorCode = colorCode;
entry.fontStyle = fontStyle;
entry.renderStyle = renderStyle;
colorList.add(entry);
/* Resume search for section marks after skipping this one */
start = next + 2;
shift += 2;
}
/* Convert the accumulated ColorCode list to an array for efficient storage */
cacheEntry.colors = new ColorCode[colorList.size()];
cacheEntry.colors = colorList.toArray(cacheEntry.colors);
/* Return the new length of the string after all color codes were removed */
return text.length - shift;
}
/**
* Split a string into contiguous LTR or RTL sections by applying the Unicode Bidirectional Algorithm. Calls layoutString()
* for each contiguous run to perform further analysis.
*
* @param glyphList will hold all new Glyph objects allocated by layoutFont()
* @param text the string to lay out
* @param start the offset into text at which to start the layout
* @param limit the (offset + length) at which to stop performing the layout
* @return the total advance (horizontal distance) of this string
*/
private int layoutBidiString(List<Glyph> glyphList, char text[], int start, int limit, ColorCode colors[]) {
int advance = 0;
/* Avoid performing full bidirectional analysis if text has no "strong" right-to-left characters */
if (Bidi.requiresBidi(text, start, limit)) {
/* Note that while requiresBidi() uses start/limit the Bidi constructor uses start/length */
Bidi bidi = new Bidi(text, start, null, 0, limit - start, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
/* If text is entirely right-to-left, then insert an EntryText node for the entire string */
if (bidi.isRightToLeft()) {
return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_RIGHT_TO_LEFT, advance, colors);
}
/* Otherwise text has a mixture of LTR and RLT, and it requires full bidirectional analysis */
else {
int runCount = bidi.getRunCount();
byte levels[] = new byte[runCount];
Integer ranges[] = new Integer[runCount];
/* Reorder contiguous runs of text into their display order from left to right */
for (int index = 0; index < runCount; index++) {
levels[index] = (byte) bidi.getRunLevel(index);
ranges[index] = new Integer(index);
}
Bidi.reorderVisually(levels, 0, ranges, 0, runCount);
/*
* Every GlyphVector must be created on a contiguous run of left-to-right or right-to-left text. Keep track of
* the horizontal advance between each run of text, so that the glyphs in each run can be assigned a position relative
* to the start of the entire string and not just relative to that run.
*/
for (int visualIndex = 0; visualIndex < runCount; visualIndex++) {
int logicalIndex = ranges[visualIndex];
/* An odd numbered level indicates right-to-left ordering */
int layoutFlag = (bidi.getRunLevel(logicalIndex) & 1) == 1 ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
advance = layoutStyle(glyphList, text, start + bidi.getRunStart(logicalIndex), start + bidi.getRunLimit(logicalIndex),
layoutFlag, advance, colors);
}
}
return advance;
}
/* If text is entirely left-to-right, then insert an EntryText node for the entire string */
else {
return layoutStyle(glyphList, text, start, limit, Font.LAYOUT_LEFT_TO_RIGHT, advance, colors);
}
}
private int layoutStyle(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, ColorCode colors[]) {
int currentFontStyle = Font.PLAIN;
/* Find ColorCode object with stripIndex <= start; that will have the font style in effect at the beginning of this text run */
int colorIndex = Arrays.binarySearch(colors, start);
/*
* If no exact match is found, Arrays.binarySearch() returns (-(insertion point) - 1) where the insertion point is the index
* of the first ColorCode with a stripIndex > start. In that case, colorIndex is adjusted to select the immediately preceding
* ColorCode whose stripIndex < start.
*/
if (colorIndex < 0) {
colorIndex = -colorIndex - 2;
}
/* Break up the string into segments, where each segment has the same font style in use */
while (start < limit) {
int next = limit;
/* In case of multiple consecutive color codes with the same stripIndex, select the last one which will have active font style */
while (colorIndex >= 0 && colorIndex < (colors.length - 1) && colors[colorIndex].stripIndex == colors[colorIndex + 1].stripIndex) {
colorIndex++;
}
/* If an actual ColorCode object was found (colorIndex within the array), use its fontStyle for layout and render */
if (colorIndex >= 0 && colorIndex < colors.length) {
currentFontStyle = colors[colorIndex].fontStyle;
}
/*
* Search for the next ColorCode that uses a different fontStyle than the current one. If found, the stripIndex of that
* new code is the split point where the string must be split into a separately styled segment.
*/
while (++colorIndex < colors.length) {
if (colors[colorIndex].fontStyle != currentFontStyle) {
next = colors[colorIndex].stripIndex;
break;
}
}
/* Layout the string segment with the style currently selected by the last color code */
advance = layoutString(glyphList, text, start, next, layoutFlags, advance, currentFontStyle);
start = next;
}
return advance;
}
/**
* Given a string that runs contiguously LTR or RTL, break it up into individual segments based on which fonts can render
* which characters in the string. Calls layoutFont() for each portion of the string that can be layed out with a single
* font.
*
* @param glyphList will hold all new Glyph objects allocated by layoutFont()
* @param text the string to lay out
* @param start the offset into text at which to start the layout
* @param limit the (offset + length) at which to stop performing the layout
* @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT
* @param advance the horizontal advance (i.e. X position) returned by previous call to layoutString()
* @param style combination of Font.PLAIN, Font.BOLD, and Font.ITALIC to select a fonts with some specific style
* @return the advance (horizontal distance) of this string plus the advance passed in as an argument
* @todo Correctly handling RTL font selection requires scanning the sctring from RTL as well.
* @todo Use bitmap fonts as a fallback if no OpenType font could be found
*/
private int layoutString(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, int style) {
/*
* Convert all digits in the string to a '0' before layout to ensure that any glyphs replaced on the fly will all have
* the same positions. Under Windows, Java's "SansSerif" logical font uses the "Arial" font for digits, in which the "1"
* digit is slightly narrower than all other digits. Checking the digitGlyphsReady flag prevents a chicken-and-egg
* problem where the digit glyphs have to be initially cached and the digitGlyphs[] array initialized without replacing
* every digit with '0'.
*/
if (digitGlyphsReady) {
for (int index = start; index < limit; index++) {
if (text[index] >= '0' && text[index] <= '9') {
text[index] = '0';
}
}
}
/* Break the string up into segments, where each segment can be displayed using a single font */
while (start < limit) {
Font font = glyphCache.lookupFont(text, start, limit, style);
int next = font.canDisplayUpTo(text, start, limit);
/* canDisplayUpTo returns -1 if the entire string range is supported by this font */
if (next == -1) {
next = limit;
}
/*
* canDisplayUpTo() returns start if the starting character is not supported at all. In that case, draw just the
* one unsupported character (which will use the font's "missing glyph code"), then retry the lookup again at the
* next character after that.
*/
if (next == start) {
next++;
}
advance = layoutFont(glyphList, text, start, next, layoutFlags, advance, font);
start = next;
}
return advance;
}
/**
* Allocate new Glyph objects and add them to the glyph list. This sequence of Glyphs represents a portion of the
* string where all glyphs run contiguously in either LTR or RTL and come from the same physical/logical font.
*
* @param glyphList all newly created Glyph objects are added to this list
* @param text the string to layout
* @param start the offset into text at which to start the layout
* @param limit the (offset + length) at which to stop performing the layout
* @param layoutFlags either Font.LAYOUT_RIGHT_TO_LEFT or Font.LAYOUT_LEFT_TO_RIGHT
* @param advance the horizontal advance (i.e. X position) returned by previous call to layoutString()
* @param font the Font used to layout a GlyphVector for the string
* @return the advance (horizontal distance) of this string plus the advance passed in as an argument
* @todo need to ajust position of all glyphs if digits are present, by assuming every digit should be 0 in length
*/
private int layoutFont(List<Glyph> glyphList, char text[], int start, int limit, int layoutFlags, int advance, Font font) {
/*
* Ensure that all glyphs used by the string are pre-rendered and cached in the texture. Only safe to do so from the
* main thread because cacheGlyphs() can crash LWJGL if it makes OpenGL calls from any other thread. In this case,
* cacheString() will also not insert the entry into the stringCache since it may be incomplete if lookupGlyph()
* returns null for any glyphs not yet stored in the glyph cache.
*/
if (mainThread == Thread.currentThread()) {
glyphCache.cacheGlyphs(font, text, start, limit, layoutFlags);
}
/* Creating a GlyphVector takes care of all language specific OpenType glyph substitutions and positionings */
GlyphVector vector = glyphCache.layoutGlyphVector(font, text, start, limit, layoutFlags);
/*
* Extract all needed information for each glyph from the GlyphVector so it won't be needed for actual rendering.
* Note that initially, glyph.start holds the character index into the stripped text array. But after the entire
* string is layed out, this field will be adjusted on every Glyph object to correctly index the original unstripped
* string.
*/
Glyph glyph = null;
int numGlyphs = vector.getNumGlyphs();
for (int index = 0; index < numGlyphs; index++) {
Point position = vector.getGlyphPixelBounds(index, null, advance, 0).getLocation();
/* Compute horizontal advance for the previous glyph based on this glyph's position */
if (glyph != null) {
glyph.advance = position.x - glyph.x;
}
/*
* Allocate a new glyph object and add to the glyphList. The glyph.stringIndex here is really like stripIndex but
* it will be corrected later to account for the color codes that have been stripped out.
*/
glyph = new Glyph();
glyph.stringIndex = start + vector.getGlyphCharIndex(index);
glyph.texture = glyphCache.lookupGlyph(font, vector.getGlyphCode(index));
glyph.x = position.x;
glyph.y = position.y;
glyphList.add(glyph);
}
/* Compute the advance position of the last glyph (or only glyph) since it can't be done by the above loop */
advance += (int) vector.getGlyphPosition(numGlyphs).getX();
if (glyph != null) {
glyph.advance = advance - glyph.x;
}
/* Return the overall horizontal advance in pixels from the start of string */
return advance;
}
}