package tc.oc.commons.core.chat;
import com.google.common.base.Strings;
import net.md_5.bungee.api.ChatColor;
import java.util.ArrayList;
import java.util.List;
public class ChatUtils {
public static final int MAX_CHAT_WIDTH = 300;
/**
* Get the pixel width of the given character in the Minecraft font, excluding
* the space between characters and drop shadow. Handles alphanumerics and most
* common english punctuation.
*/
public static int pixelWidth(char c) {
if(Character.isUpperCase(c)) {
return c == 'I' ? 3 : 5;
} else if(Character.isDigit(c)) {
return 5;
} else if(Character.isLowerCase(c)) {
switch(c) {
case 'i':
return 1;
case 'l':
return 2;
case 't':
return 3;
case 'f':
case 'k':
return 4;
default:
return 5;
}
} else {
switch(c) {
case '!':
case '.':
case ',':
case ';':
case ':':
case '|':
return 1;
case '\'':
return 2;
case '[':
case ']':
case ' ':
return 3;
case '*':
case '(':
case ')':
case '{':
case '}':
case '<':
case '>':
return 4;
case '@':
return 6;
default:
return 5;
}
}
}
/**
* A complete text formatting state, used by Parser
*/
private static class Format {
public boolean obfuscated = false;
public boolean bold = false;
public boolean strikethrough = false;
public boolean underline = false;
public boolean italic = false;
public ChatColor color;
private Format(boolean obfuscated,
boolean bold,
boolean strikethrough,
boolean underline,
boolean italic,
ChatColor color) {
this.obfuscated = obfuscated;
this.bold = bold;
this.strikethrough = strikethrough;
this.underline = underline;
this.italic = italic;
this.color = color;
}
private Format(Format format) {
this(format.obfuscated,
format.bold,
format.strikethrough,
format.underline,
format.italic,
format.color);
}
private Format() {
}
@Override
public String toString() {
String str = "";
if(this.color != null) { str += this.color; }
if(this.obfuscated) { str += ChatColor.MAGIC; }
if(this.bold) { str += ChatColor.BOLD; }
if(this.strikethrough) { str += ChatColor.STRIKETHROUGH; }
if(this.underline) { str += ChatColor.UNDERLINE; }
if(this.italic) { str += ChatColor.ITALIC; }
return str;
}
}
/**
* Holds the state while parsing formatted text, while calculating
* the pixel width at various points.
*/
private static class Parser {
public final String text;
public boolean formatting = false; // last char was §
public int chars = 0; // consumed char count
public int pixels = 0; // total pixel width of consumed chars
public Format format; // formatting state at current pos
public int charsVisible = 0; // char count at last visible char
public int pixelsVisible = 0; // pixel width at last visible char
public int charsWord = 0; // char count at last word ending
public int pixelsWord = 0; // pixel width at last word ending
public Format wordFormat; // formatting state at last word ending
private Parser(String text) {
this.text = text;
this.format = new Format();
this.wordFormat = new Format();
}
public boolean atEnd() {
return this.chars == this.text.length();
}
/**
* Return the pixels that will be added to width if the given char is consumed next
*/
public int getAdvance(char c) {
if(c == ChatColor.COLOR_CHAR && this.chars < this.text.length() - 1) {
return 0;
} else {
int width = pixelWidth(c);
if(this.format.bold) {
// Bold chars are one pixel wider
width += 1;
}
if(this.chars != 0) {
// If not the first char, add the gap between chars
width += 1;
}
return width;
}
}
/**
* Return the number of pixels that will be added to width by the next char
*/
public int nextAdvance() {
return this.getAdvance(this.text.charAt(this.chars));
}
/**
* Consume a character
*/
public void advance() {
char c = this.text.charAt(this.chars++);
if(this.formatting) {
// Previous char was §, so this char will always be hidden
switch(c) {
case 'k':
this.format.obfuscated = true;
break;
case 'l':
this.format.bold = true;
break;
case 'm':
this.format.strikethrough = true;
break;
case 'n':
this.format.underline = true;
break;
case 'o':
this.format.italic = true;
break;
default:
this.format.color = ChatColor.getByChar(c);
this.format.obfuscated = false;
this.format.bold = false;
this.format.strikethrough = false;
this.format.underline = false;
this.format.italic = false;
break;
case 'r':
this.format.obfuscated = false;
this.format.bold = false;
this.format.strikethrough = false;
this.format.underline = false;
this.format.italic = false;
this.format.color = null;
break;
}
this.formatting = false;
} else {
if(c == ChatColor.COLOR_CHAR && this.chars != this.text.length() - 1) {
// If we encounter a § and it's not the last char in the string,
// switch to the format state
this.formatting = true;
} else {
// Otherwise, this char will be visible, so update the position
// pointer and increment the width
this.charsVisible = this.chars;
this.pixelsVisible = this.pixels;
if(this.chars == this.text.length() || Character.isWhitespace(this.text.charAt(this.chars))) {
this.charsWord = this.chars;
this.pixelsWord = this.pixels;
this.wordFormat = new Format(this.format);
}
this.pixels += getAdvance(c);
}
}
}
/**
* Consume the maximum characters that will increase the width by no more than the given amount
*/
public void advance(int width) {
int startWidth = this.pixels;
while(this.pixels + this.nextAdvance() <= startWidth + width) {
this.advance();
if(this.atEnd()) {
return;
}
}
}
/**
* Consume the maximum words that will increase the width by no more than the given amount
*/
public void advanceWords(int width) {
int startWidth = this.pixels;
while(this.pixels + this.nextAdvance() <= startWidth + width) {
this.advance();
if(this.atEnd()) {
return;
}
}
this.chars = this.charsWord;
this.pixels = this.pixelsWord;
this.format = new Format(this.wordFormat);
}
}
/**
* Get the pixel width of the given text in the Minecraft font, excluding the drop shadow.
* Formatting codes are accounted for, and so is bold text.
*/
public static int pixelWidth(String text) {
return pixelWidth(text, false);
}
public static int pixelWidth(String text, boolean bold) {
Parser parser = new Parser(text);
if(bold) parser.format.bold = true;
while(!parser.atEnd()) {
parser.advance();
}
return parser.pixels;
}
/**
* Return the length of the longest prefix of the given string that fits
* within the given pixel width when rendered in the Minecraft font. If
* the prefix is shorter than the entire string, then it is guaranteed to
* end with a visible character, and not any formatting character.
*/
public static int longestPrefix(String text, int maxWidth) {
int pos = 0;
Parser parser = new Parser(text);
while(parser.pixels < maxWidth) {
if(parser.atEnd()) {
return text.length();
}
pos = parser.charsVisible;
parser.advance();
}
return pos;
}
/**
* Return the length of the longest prefix of the given string that ends
* on a word boundary and fits within the given pixel width when rendered
* in the Minecraft font. If the prefix is shorter than the entire string,
* then it is guaranteed to end with a visible character, and not any
* formatting character.
*/
public static int longestWordPrefix(String text, int maxWidth) {
int pos = 0;
Parser parser = new Parser(text);
while(parser.pixels < maxWidth) {
if(parser.atEnd()) {
return text.length();
}
pos = parser.charsWord;
parser.advance();
}
return pos;
}
public static final int SPACE_PIXEL_WIDTH = pixelWidth(' ');
/**
* Return a horizontal line spanning the width of the chat window
* @param lineColor color of the line
* @param width width of the line in pixels
* @return the line as a string
*/
public static String horizontalLine(ChatColor lineColor, int width) {
return lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", (width + 1) / (SPACE_PIXEL_WIDTH + 1));
}
/**
* Return some text centered in a horizontal line spanning the chat window
* @param text the text to center in the line (can contain formatting codes)
* @param lineColor the color of the line
* @param width width of the line in pixels
* @return the heading as a string
*/
public static String horizontalLineHeading(String text, ChatColor lineColor, int width) {
text = ChatColor.RESET + " " + text + ChatColor.RESET + " ";
int textWidth = pixelWidth(text);
int spaceCount = Math.max(0, ((width - textWidth) / 2 + 1) / (SPACE_PIXEL_WIDTH + 1));
return lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", spaceCount) +
text +
lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", spaceCount);
}
public static List<String> wordWrap(String text, int width) {
ArrayList<String> lines = new ArrayList<>();
return wordWrap(text, width, lines);
}
/**
* Word-wrap the given text to the given pixel width. Output is appended to the
* given String list, which is also returned. Formatting codes are correctly
* handled and propagated across lines.
*
* @param text to wrap
* @param width pixel width of the chat window
* @param lines list of lines to append the wrapped text to
* @return lines
*/
public static List<String> wordWrap(String text, int width, List<String> lines) {
Parser parser = new Parser(text);
Format format = null;
int lineStart = 0;
while(!parser.atEnd()) {
parser.advanceWords(width);
if(parser.chars == lineStart) {
parser.advance(width);
if(parser.chars == lineStart) {
parser.advance();
}
}
if(format == null) {
lines.add(text.substring(lineStart, parser.chars));
} else {
lines.add(format.toString() + text.substring(lineStart, parser.chars));
}
lineStart = parser.chars;
format = new Format(parser.format);
}
return lines;
}
/**
* Underlines a specific string maintaining colors.
*
* @param str The string to underline.
* @return The underlined string.
*/
public static String underline(String str) {
return applyFormat(ChatColor.UNDERLINE, str);
}
/**
* Makes a string bolded, maintaining all colors.
*
* @param str The string to be bolded.
* @return The bolded string.
*/
public static String bold(String str) {
return applyFormat(ChatColor.BOLD, str);
}
/**
* Italicizes a string, maintaining all previous colors.
*
* @param str The string to be italicized.
* @return The intaliciezd string.
*/
public static String italicize(String str) {
return applyFormat(ChatColor.ITALIC, str);
}
/**
* Strikes out a string, keeping all previous colors.
*
* @param str The string to strike out.
* @return The striked-out string.
*/
public static String strikethrough(String str) {
return applyFormat(ChatColor.STRIKETHROUGH, str);
}
/**
* Replaces all instances of one color in "str" from `from` to `to`.
* @param str The base string.
* @param from The base ChatColor.
* @param to The ChatColor to replace to.
*
* @return The replaced String.
*/
public static String replaceColorCodes(String str, ChatColor from, ChatColor to) {
return str.replaceAll(from.toString(), to.toString());
}
public static boolean isFormat(ChatColor color) {
switch(color) {
case BOLD:
case ITALIC:
case UNDERLINE:
case STRIKETHROUGH:
case MAGIC:
return true;
default:
return false;
}
}
/**
* Apply formatting to an entire string that may contain other color codes
*
* @param format The format to apply
* @param text The string to apply the format to
*
* @return New string with the format applied
*/
private static String applyFormat(ChatColor format, String text) {
StringBuilder buffer = new StringBuilder();
boolean escaped = false; // previous char was the escape char
boolean needsFormat = true; // format has been reset and needs to be re-applied
for(int i = 0; i < text.length(); ++i) {
char c = text.charAt(i);
if(escaped) {
escaped = false;
ChatColor color = ChatColor.getByChar(c);
needsFormat = color == null || !isFormat(color);
} else if(c == ChatColor.COLOR_CHAR) {
escaped = true;
} else if(needsFormat) {
needsFormat = false;
buffer.append(format);
}
buffer.append(c);
}
return buffer.toString();
}
/**
* Make text really small (only works on numbers right now)
*/
public static String tiny(String text) {
char[] chars = text.toCharArray();
for(int i = 0; i < chars.length; i++) {
switch(chars[i]) {
case '.': chars[i] = '\u2024'; break;
default: chars[i] = (char) (chars[i] - '0' + '\u2080'); break;
}
}
return String.valueOf(chars);
}
public static net.md_5.bungee.api.ChatColor convert(Enum<?> color) {
return net.md_5.bungee.api.ChatColor.valueOf(color.name());
}
}