Refactor and javadoc pandalib-chat

This commit is contained in:
Marc Baloup 2022-07-30 13:58:16 +02:00
parent 62f5034ca0
commit 4e1a674c49
Signed by: marcbal
GPG Key ID: BBC0FE3ABC30B893
10 changed files with 2818 additions and 1581 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
package fr.pandacube.lib.chat;
import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.format.TextColor;
/**
* A custom gradient with a least 2 colors in it.
*/
public class ChatColorGradient {
private record GradientColor(float location, TextColor color) { }
private final List<GradientColor> colors = new ArrayList<>();
/**
* Put a specific color at a specific location in the gradient.
* @param gradientLocation the location in the gradient.
* @param gradientColor the color to put at this location.
* @return this.
*/
public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) {
colors.add(new GradientColor(gradientLocation, gradientColor));
return this;
}
/**
* Compute a color by interpolating between the 2 colors surrounding the provided location.
* @param gradientLocation the location at which to pick the gradient color.
* @return the computed color.
*/
public synchronized TextColor pickColorAt(float gradientLocation) {
if (colors.isEmpty())
throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance.");
if (colors.size() == 1)
return colors.get(0).color();
colors.sort((p1, p2) -> Float.compare(p1.location(), p2.location()));
if (gradientLocation <= colors.get(0).location())
return colors.get(0).color();
if (gradientLocation >= colors.get(colors.size() - 1).location())
return colors.get(colors.size() - 1).color();
int p1 = 1;
for (; p1 < colors.size(); p1++) {
if (colors.get(p1).location() >= gradientLocation)
break;
}
int p0 = p1 - 1;
float v0 = colors.get(p0).location(), v1 = colors.get(p1).location();
TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color();
return ChatColorUtil.interpolateColor(v0, v1, gradientLocation, cc0, cc1);
}
}

View File

@ -1,61 +1,68 @@
package fr.pandacube.lib.chat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.util.RGBLike;
import net.md_5.bungee.api.ChatColor;
/**
* Provides methods to manipulate legacy colors and {@link ChatColor} class.
*/
public class ChatColorUtil {
public static final String ALL_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoPpRr";
/**
* All characters that represent a colorcode.
*/
public static final String ALL_COLORS = "0123456789AaBbCcDdEeFf";
/**
* All characters that represent a color or format code.
*/
public static final String ALL_CODES = ALL_COLORS + "KkLlMmNnOoPpRr";
private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("§x(?>§[\\da-f]){6}", Pattern.CASE_INSENSITIVE);
private static final Pattern ESS_COLOR_PATTERN = Pattern.compile("§#[\\da-f]{6}", Pattern.CASE_INSENSITIVE);
/**
* Return the legacy format needed to reproduce the format at the end of the provided legacy text.
* Returns the legacy format needed to reproduce the format at the end of the provided legacy text.
* Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format.
* The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value.
* @param legacyText the legacy formated text.
* @return the active format at the end of the provided text.
*/
public static String getLastColors(String legacyText) {
public static String getLastColors(String legacyText) {
StringBuilder result = new StringBuilder();
int length = legacyText.length();
for (int index = length - 2; index >= 0; index--) {
if (legacyText.charAt(index) == ChatColor.COLOR_CHAR) {
// detection of rgb color §x§0§1§2§3§4§5
// detection of rgb color §x§0§1§2§3§4§5
String rgb;
if (index > 11
&& legacyText.charAt(index - 12) == ChatColor.COLOR_CHAR
&& (legacyText.charAt(index - 11) == 'x'
|| legacyText.charAt(index - 11) == 'X')
&& HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) {
result.insert(0, rgb);
break;
}
// detection of rgb color §#012345 (and converting it to bungee chat format)
if (index < length - 7
&& legacyText.charAt(index + 1) == '#'
&& ESS_COLOR_PATTERN.matcher(rgb = legacyText.substring(index, index + 8)).matches()) {
rgb = "§x§" + rgb.charAt(2) + "§" + rgb.charAt(3)
+ "§" + rgb.charAt(4) + "§" + rgb.charAt(5)
+ "§" + rgb.charAt(6) + "§" + rgb.charAt(7);
result.insert(0, rgb);
break;
}
// try detect non-rgb format
if (index > 11
&& legacyText.charAt(index - 12) == ChatColor.COLOR_CHAR
&& (legacyText.charAt(index - 11) == 'x'
|| legacyText.charAt(index - 11) == 'X')
&& HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) {
result.insert(0, rgb);
break;
}
// detection of rgb color §#012345 (and converting it to bungee chat format)
if (index < length - 7
&& legacyText.charAt(index + 1) == '#'
&& ESS_COLOR_PATTERN.matcher(rgb = legacyText.substring(index, index + 8)).matches()) {
rgb = "§x§" + rgb.charAt(2) + "§" + rgb.charAt(3)
+ "§" + rgb.charAt(4) + "§" + rgb.charAt(5)
+ "§" + rgb.charAt(6) + "§" + rgb.charAt(7);
result.insert(0, rgb);
break;
}
// try detect non-rgb format
char colorChar = legacyText.charAt(index + 1);
ChatColor legacyColor = getChatColorByChar(colorChar);
@ -65,8 +72,8 @@ public class ChatColorUtil {
// Once we find a color or reset we can stop searching
char col = legacyColor.toString().charAt(1);
if ((col >= '0' && col <= '9')
|| (col >= 'a' && col <= 'f')
|| col == 'r') {
|| (col >= 'a' && col <= 'f')
|| col == 'r') {
break;
}
}
@ -75,229 +82,215 @@ public class ChatColorUtil {
return result.toString();
}
public static ChatColor getChatColorByChar(char code) {
/**
* Returns the {@link ChatColor} associated with the provided char, case insensitive.
* @param code the case insensitive char code.
* @return the corresponding {@link ChatColor}.
*/
public static ChatColor getChatColorByChar(char code) {
return ChatColor.getByChar(Character.toLowerCase(code));
}
/**
* Translate the color code of the provided string, that uses the the color char, to
* the {@code §} color code format.
* <p>
* This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)},
* because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it).
* Essentials RGB color code are converted to Bungee chat RGB format, so the returned string can be converted
* to component (see {@link Chat#legacyText(Object)}).
* <p>
* This method should be used for user input (no permission check) or string configuration, but not string
* from another API or containing URLs.
*/
/**
* Translate the color code of the provided string, that uses the alt color char, to the {@code §} color code
* format.
* <p>
* This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)},
* because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it).
* Essentials RGB color code are converted to Bungee chat RGB format, so the returned string can be converted
* to component (see {@link Chat#legacyText(Object)}).
* <p>
* This method should be used for user input (no permission check) or string configuration, but not string
* from another API or containing URLs.
* @param altColorChar the alternative character to prefix color codes (usually {@code '&'}).
* @param textToTranslate the text to translate.
* @return the string translated to proper legacy text.
*/
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate)
{
char colorChar = ChatColor.COLOR_CHAR;
StringBuilder acc = new StringBuilder();
char colorChar = ChatColor.COLOR_CHAR;
StringBuilder acc = new StringBuilder();
char[] b = textToTranslate.toCharArray();
for ( int i = 0; i < b.length; i++ )
{
if (i < b.length - 1 // legacy chat format
&& b[i] == altColorChar && ALL_CODES.indexOf(b[i + 1]) > -1)
if (i < b.length - 1 // legacy chat format
&& b[i] == altColorChar && ALL_CODES.indexOf(b[i + 1]) > -1)
{
acc.append(colorChar);
acc.append(lowerCase(b[i + 1]));
i++;
}
else if (i < b.length - 13 // bungee chat RGB format
&& b[i] == altColorChar
&& lowerCase(b[i + 1]) == 'x'
&& b[i + 2] == altColorChar && ALL_COLORS.indexOf(b[i + 3]) > -1
&& b[i + 4] == altColorChar && ALL_COLORS.indexOf(b[i + 5]) > -1
&& b[i + 6] == altColorChar && ALL_COLORS.indexOf(b[i + 7]) > -1
&& b[i + 8] == altColorChar && ALL_COLORS.indexOf(b[i + 9]) > -1
&& b[i + 10] == altColorChar && ALL_COLORS.indexOf(b[i + 11]) > -1
&& b[i + 12] == altColorChar && ALL_COLORS.indexOf(b[i + 13]) > -1) {
acc.append(colorChar).append(lowerCase(b[i + 1]));
acc.append(colorChar).append(lowerCase(b[i + 3]));
acc.append(colorChar).append(lowerCase(b[i + 5]));
acc.append(colorChar).append(lowerCase(b[i + 7]));
acc.append(colorChar).append(lowerCase(b[i + 9]));
acc.append(colorChar).append(lowerCase(b[i + 11]));
acc.append(colorChar).append(lowerCase(b[i + 13]));
i+=13;
}
else if (i < b.length - 7 // Essentials chat RGB format
&& b[i] == altColorChar
&& b[i + 1] == '#'
&& ALL_COLORS.indexOf(b[i + 2]) > -1 && ALL_COLORS.indexOf(b[i + 3]) > -1
&& ALL_COLORS.indexOf(b[i + 4]) > -1 && ALL_COLORS.indexOf(b[i + 5]) > -1
&& ALL_COLORS.indexOf(b[i + 6]) > -1 && ALL_COLORS.indexOf(b[i + 7]) > -1) {
acc.append(colorChar).append('x');
acc.append(colorChar).append(lowerCase(b[i + 2]));
acc.append(colorChar).append(lowerCase(b[i + 3]));
acc.append(colorChar).append(lowerCase(b[i + 4]));
acc.append(colorChar).append(lowerCase(b[i + 5]));
acc.append(colorChar).append(lowerCase(b[i + 6]));
acc.append(colorChar).append(lowerCase(b[i + 7]));
i+=7;
}
else if (i < b.length - 1 && b[i] == altColorChar && b[i + 1] == altColorChar) {
acc.append(altColorChar);
i++;
acc.append(colorChar);
acc.append(lowerCase(b[i + 1]));
i++;
}
else if (i < b.length - 13 // bungee chat RGB format
&& b[i] == altColorChar
&& lowerCase(b[i + 1]) == 'x'
&& b[i + 2] == altColorChar && ALL_COLORS.indexOf(b[i + 3]) > -1
&& b[i + 4] == altColorChar && ALL_COLORS.indexOf(b[i + 5]) > -1
&& b[i + 6] == altColorChar && ALL_COLORS.indexOf(b[i + 7]) > -1
&& b[i + 8] == altColorChar && ALL_COLORS.indexOf(b[i + 9]) > -1
&& b[i + 10] == altColorChar && ALL_COLORS.indexOf(b[i + 11]) > -1
&& b[i + 12] == altColorChar && ALL_COLORS.indexOf(b[i + 13]) > -1) {
acc.append(colorChar).append(lowerCase(b[i + 1]));
acc.append(colorChar).append(lowerCase(b[i + 3]));
acc.append(colorChar).append(lowerCase(b[i + 5]));
acc.append(colorChar).append(lowerCase(b[i + 7]));
acc.append(colorChar).append(lowerCase(b[i + 9]));
acc.append(colorChar).append(lowerCase(b[i + 11]));
acc.append(colorChar).append(lowerCase(b[i + 13]));
i+=13;
}
else if (i < b.length - 7 // Essentials chat RGB format
&& b[i] == altColorChar
&& b[i + 1] == '#'
&& ALL_COLORS.indexOf(b[i + 2]) > -1 && ALL_COLORS.indexOf(b[i + 3]) > -1
&& ALL_COLORS.indexOf(b[i + 4]) > -1 && ALL_COLORS.indexOf(b[i + 5]) > -1
&& ALL_COLORS.indexOf(b[i + 6]) > -1 && ALL_COLORS.indexOf(b[i + 7]) > -1) {
acc.append(colorChar).append('x');
acc.append(colorChar).append(lowerCase(b[i + 2]));
acc.append(colorChar).append(lowerCase(b[i + 3]));
acc.append(colorChar).append(lowerCase(b[i + 4]));
acc.append(colorChar).append(lowerCase(b[i + 5]));
acc.append(colorChar).append(lowerCase(b[i + 6]));
acc.append(colorChar).append(lowerCase(b[i + 7]));
i+=7;
}
else if (i < b.length - 1 && b[i] == altColorChar && b[i + 1] == altColorChar) {
acc.append(altColorChar);
i++;
}
else {
acc.append(b[i]);
acc.append(b[i]);
}
}
return acc.toString();
}
private static char lowerCase(char c) { return Character.toLowerCase(c); }
/**
* Force a text to be italic, while keeping other formatting and colors.
* The text is prefixed with the ITALIC tag, but is not reset at the end.
* @param legacyText the original text
* @return the text fully italic
*/
public static String forceItalic(String legacyText) {
return forceFormat(legacyText, ChatColor.ITALIC);
}
/**
* Force a text to be bold, while keeping other formatting and colors.
* The text is prefixed with the BOLD tag, but is not reset at the end.
* @param legacyText the original text
* @return the text fully bold
*/
public static String forceBold(String legacyText) {
return forceFormat(legacyText, ChatColor.BOLD);
}
/**
* Force a text to be underlined, while keeping other formatting and colors.
* The text is prefixed with the UNDERLINE tag, but is not reset at the end.
* @param legacyText the original text
* @return the text fully underlined
*/
public static String forceUnderline(String legacyText) {
return forceFormat(legacyText, ChatColor.UNDERLINE);
}
/**
* Force a text to be stroked through, while keeping other formatting and colors.
* The text is prefixed with the STRIKETHROUGH tag, but is not reset at the end.
* @param legacyText the original text
* @return the text fully stroked through
*/
public static String forceStrikethrough(String legacyText) {
return forceFormat(legacyText, ChatColor.STRIKETHROUGH);
}
/**
* Force a text to be obfuscated, while keeping other formatting and colors.
* The text is prefixed with the MAGIC tag, but is not reset at the end.
* @param legacyText the original text
* @return the text fully obfuscated
*/
public static String forceObfuscated(String legacyText) {
return forceFormat(legacyText, ChatColor.MAGIC);
}
private static String forceFormat(String legacyText, ChatColor format) {
return format + legacyText
.replace(format.toString(), "") // remove previous tag to make the result cleaner
.replaceAll("§([a-frA-FR\\d])", "§$1" + format);
}
/**
* Replace the RESET tag of the input string to the specified color tag.
* @param legacyText the original text
* @param color the color to used to replace the RESET tag
* (can be a combination of a color tag followed by multiple format tag)
* @return the resulting text
*/
public static String resetToColor(String legacyText, String color) {
return legacyText.replace(ChatColor.RESET.toString(), color);
}
public static TextColor toAdventure(ChatColor bungee) {
if (bungee == null)
return null;
if (bungee.getColor() == null)
throw new IllegalArgumentException("The provided Bungee ChatColor must be an actual color (not format nor reset).");
return TextColor.color(bungee.getColor().getRGB());
}
public static ChatColor toBungee(TextColor col) {
if (col == null)
return null;
if (col instanceof NamedTextColor) {
return ChatColor.of(((NamedTextColor) col).toString());
}
return ChatColor.of(col.asHexString());
}
public static TextColor interpolateColor(float v0, float v1, float v, TextColor cc0, TextColor cc1) {
float normV = (v - v0) / (v1 - v0);
return TextColor.lerp(normV, cc0, cc1);
}
public static class ChatValueGradient {
private record GradientValueColor(float value, TextColor color) { } // Java 16
final List<GradientValueColor> colors = new ArrayList<>();
public synchronized ChatValueGradient add(float v, TextColor col) {
colors.add(new GradientValueColor(v, col));
return this;
}
public synchronized TextColor pickColorAt(float v) {
if (colors.isEmpty())
throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance.");
if (colors.size() == 1)
return colors.get(0).color();
colors.sort((p1, p2) -> Float.compare(p1.value(), p2.value()));
if (v <= colors.get(0).value())
return colors.get(0).color();
if (v >= colors.get(colors.size() - 1).value())
return colors.get(colors.size() - 1).color();
int p1 = 1;
for (; p1 < colors.size(); p1++) {
if (colors.get(p1).value() >= v)
break;
}
int p0 = p1 - 1;
float v0 = colors.get(p0).value(), v1 = colors.get(p1).value();
TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color();
return interpolateColor(v0, v1, v, cc0, cc1);
}
}
private static char lowerCase(char c) { return Character.toLowerCase(c); }
/**
* Force a text to be italic, while keeping other formatting and colors.
* The text is prefixed with the ITALIC tag, but is not reset at the end.
* @param legacyText the original text.
* @return the text fully italic.
*/
public static String forceItalic(String legacyText) {
return forceFormat(legacyText, ChatColor.ITALIC);
}
/**
* Force a text to be bold, while keeping other formatting and colors.
* The text is prefixed with the BOLD tag, but is not reset at the end.
* @param legacyText the original text.
* @return the text fully bold.
*/
public static String forceBold(String legacyText) {
return forceFormat(legacyText, ChatColor.BOLD);
}
/**
* Force a text to be underlined, while keeping other formatting and colors.
* The text is prefixed with the UNDERLINE tag, but is not reset at the end.
* @param legacyText the original text.
* @return the text fully underlined.
*/
public static String forceUnderline(String legacyText) {
return forceFormat(legacyText, ChatColor.UNDERLINE);
}
/**
* Force a text to be stroked through, while keeping other formatting and colors.
* The text is prefixed with the STRIKETHROUGH tag, but is not reset at the end.
* @param legacyText the original text.
* @return the text fully stroked through.
*/
public static String forceStrikethrough(String legacyText) {
return forceFormat(legacyText, ChatColor.STRIKETHROUGH);
}
/**
* Force a text to be obfuscated, while keeping other formatting and colors.
* The text is prefixed with the MAGIC tag, but is not reset at the end.
* @param legacyText the original text.
* @return the text fully obfuscated.
*/
public static String forceObfuscated(String legacyText) {
return forceFormat(legacyText, ChatColor.MAGIC);
}
private static String forceFormat(String legacyText, ChatColor format) {
return format + legacyText
.replace(format.toString(), "") // remove previous tag to make the result cleaner
.replaceAll("§([a-frA-FR\\d])", "§$1" + format);
}
/**
* Replace the RESET tag of the input string to the specified color tag.
* @param legacyText the original text
* @param color the color to used to replace the RESET tag
* (can be a combination of a color tag followed by multiple format tag).
* @return the resulting text.
*/
public static String resetToColor(String legacyText, String color) {
return legacyText.replace(ChatColor.RESET.toString(), color);
}
/**
* Converts the provided {@link ChatColor} to its Adventure counterpart.
* @param bungee a BungeeCord {@link ChatColor} instance.
* @return the {@link TextColor} equivalent to the provided {@link ChatColor}.
*/
public static TextColor toAdventure(ChatColor bungee) {
if (bungee == null)
return null;
if (bungee.getColor() == null)
throw new IllegalArgumentException("The provided Bungee ChatColor must be an actual color (not format nor reset).");
return TextColor.color(bungee.getColor().getRGB());
}
/**
* Converts the provided {@link TextColor} to its BungeeCord counterpart.
* @param col a Adventure {@link TextColor} instance.
* @return the {@link ChatColor} equivalent to the provided {@link TextColor}.
*/
public static ChatColor toBungee(TextColor col) {
if (col == null)
return null;
if (col instanceof NamedTextColor) {
return ChatColor.of(((NamedTextColor) col).toString());
}
return ChatColor.of(col.asHexString());
}
/**
* Create a color, interpolating between 2 colors.
* @param v0 the value corresponding to color {@code cc0}.
* @param v1 the value corresponding to color {@code cc1}.
* @param v the value between {@code v0} and {@code v1} to interpolate.
* @param cc0 the first color.
* @param cc1 the second color.
* @return the interpolated color.
* @see TextColor#lerp(float, RGBLike, RGBLike)
*/
public static TextColor interpolateColor(float v0, float v1, float v, TextColor cc0, TextColor cc1) {
float normV = (v - v0) / (v1 - v0);
return TextColor.lerp(normV, cc0, cc1);
}
}

View File

@ -0,0 +1,90 @@
package fr.pandacube.lib.chat;
import java.util.function.Supplier;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
/**
* Class holding static configuration values for chat component rendering.
*/
public class ChatConfig {
/**
* The color used for decoration.
*/
public static TextColor decorationColor = NamedTextColor.YELLOW;
/**
* The character used as a pattern for decoration.
*/
public static char decorationChar = '-';
/**
* The default margin for left and right aligned text.
*/
public static int nbCharMargin = 1;
/**
* The color used for successful messages.
*/
public static TextColor successColor = NamedTextColor.GREEN;
/**
* The color used for error/failure messages.
*/
public static TextColor failureColor = NamedTextColor.RED;
/**
* the color used for informational messages.
*/
public static TextColor infoColor = NamedTextColor.GOLD;
/**
* The color used for warning messages.
*/
public static TextColor warningColor = NamedTextColor.GOLD;
/**
* The color used to display data in a message.
*/
public static TextColor dataColor = NamedTextColor.GRAY;
/**
* The color used for displayed URLs and clickable URLs.
*/
public static TextColor urlColor = NamedTextColor.GREEN;
/**
* The color used for displayed commands and clickable commands.
*/
public static TextColor commandColor = NamedTextColor.GRAY;
/**
* The color sued to display a command that is highlighted. For example, the current page in a pagination.
*/
public static TextColor highlightedCommandColor = NamedTextColor.WHITE;
/**
* The color used for broadcasted messages.
* It is often used in combination with {@link #prefix}.
*/
public static TextColor broadcastColor = NamedTextColor.YELLOW;
/**
* The prefix used for prefixed messages.
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
* It is often used in combination with {@link #broadcastColor}.
*/
public static Supplier<Chat> prefix;
/**
* Gets the width of the configured {@link #prefix}.
* @param console if the width has to be calculated for the console or not.
* @return the width of the configured {@link #prefix}.
*/
public static int getPrefixWidth(boolean console) {
Chat c;
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
}
}

View File

@ -0,0 +1,208 @@
package fr.pandacube.lib.chat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import fr.pandacube.lib.chat.Chat.FormatableChat;
/**
* Builder for a {@link Chat} component for filling a chat line, with decoration and eventual aligned text.
*/
public class ChatFilledLine implements ComponentLike {
/**
* Builder for a filled line with the provided left-aligned text.
* @param text the text to align ont the left.
* @return a new {@link ChatFilledLine} builder.
*/
public static ChatFilledLine leftText(ComponentLike text) {
return new ChatFilledLine(text, Alignment.LEFT);
}
/**
* Builder for a filled line with the provided right-aligned text.
* @param text the text to align ont the right.
* @return a new {@link ChatFilledLine} builder.
*/
public static ChatFilledLine rightText(ComponentLike text) {
return new ChatFilledLine(text, Alignment.RIGHT);
}
/**
* Builder for a filled line with the provided centered text.
* @param text the text to center.
* @return a new {@link ChatFilledLine} builder.
*/
public static ChatFilledLine centerText(ComponentLike text) {
return new ChatFilledLine(text, Alignment.CENTER);
}
/**
* Builder for a filled line with no text.
* @return a new {@link ChatFilledLine} builder.
*/
public static ChatFilledLine filled() {
return new ChatFilledLine(null, Alignment.NONE);
}
private final ComponentLike text;
private final Alignment alignment;
private char decorationChar = ChatConfig.decorationChar;
private TextColor decorationColor = ChatConfig.decorationColor;
private boolean decorationBold = false;
private int nbSide = ChatConfig.nbCharMargin;
private boolean spacesAroundText = false;
private boolean console = false;
private Integer maxWidth = null;
private ChatFilledLine(ComponentLike text, Alignment alignment) {
this.text = text;
this.alignment = alignment;
}
/**
* Sets the decoration char.
* @param decoChar the character that will fill the line.
* @return this.
*/
public ChatFilledLine decoChar(char decoChar) {
decorationChar = decoChar;
return this;
}
/**
* Sets the decoration color.
* @param decoColor the color of the characters filling the line.
* @return this.
*/
public ChatFilledLine decoColor(TextColor decoColor) {
decorationColor = decoColor;
return this;
}
/**
* Sets the decoration in bold.
* @return this.
*/
public ChatFilledLine decoBold() {
decorationBold = true;
return this;
}
/**
* Sets the number of side character when the text is aligned left or right.
* @param nbSide the number of character that will separate the border from the side of the text.
* @return this.
*/
public ChatFilledLine nbSide(int nbSide) {
this.nbSide = nbSide;
return this;
}
/**
* Adds spaces around the text.
* @return this.
*/
public ChatFilledLine spacesAroundText() {
spacesAroundText = true;
return this;
}
/**
* Configure if the line will be rendered on console or not.
* @param console true for console, false for game UI.
* @return this.
*/
public ChatFilledLine console(boolean console) {
this.console = console;
return this;
}
/**
* Configure the width of the line.
* @param maxWidth the width to consider when rendering the line. In pixel for game UI rendering, n character for
* console rendering.
* @return this.
*/
public ChatFilledLine maxWidth(int maxWidth) {
this.maxWidth = maxWidth;
return this;
}
/**
* Renders this line to a {@link FormatableChat}.
* @return a new {@link FormatableChat} builded by this {@link ChatFilledLine}.
*/
public FormatableChat toChat() {
int maxWidth = (this.maxWidth != null)
? this.maxWidth
: console ? ChatUtil.CONSOLE_NB_CHAR_DEFAULT : ChatUtil.DEFAULT_CHAT_WIDTH;
if (alignment == Alignment.NONE) {
int count = maxWidth / ChatUtil.charW(decorationChar, console, decorationBold);
return Chat.text(ChatUtil.repeatedChar(decorationChar, count)).color(decorationColor).bold(decorationBold);
}
ComponentLike text = spacesAroundText
? Chat.text(" ").then(this.text).thenText(" ")
: this.text;
int textWidth = ChatUtil.componentWidth(text.asComponent(), console);
if (textWidth > maxWidth)
return (FormatableChat) text;
int repeatedCharWidth = ChatUtil.charW(decorationChar, console, decorationBold);
int nbCharLeft = 0, nbCharRight = 0;
switch (alignment) {
case CENTER -> {
nbCharLeft = nbCharRight = (maxWidth - textWidth) / 2 / repeatedCharWidth;
if (nbCharLeft == 0)
return (FormatableChat) text;
}
case LEFT, RIGHT -> {
int remWidth = textWidth + nbSide * repeatedCharWidth;
if (remWidth > maxWidth)
return (FormatableChat) text;
boolean left = alignment == Alignment.LEFT;
int nbOtherSide = (maxWidth - remWidth) / repeatedCharWidth;
nbCharLeft = left ? nbSide : nbOtherSide;
nbCharRight = left ? nbOtherSide : nbSide;
}
}
Chat d = Chat.chat()
.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold))
.then(text);
if (decorationChar != ' ')
d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold));
return (FormatableChat) d;
}
@Override
public @NotNull Component asComponent() {
return toChat().asComponent();
}
private enum Alignment {
LEFT,
CENTER,
RIGHT,
NONE
}
}

View File

@ -2,199 +2,641 @@ package fr.pandacube.lib.chat;
import java.util.Objects;
import net.kyori.adventure.text.BlockNBTComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.EntityNBTComponent;
import net.kyori.adventure.text.KeybindComponent;
import net.kyori.adventure.text.ScoreComponent;
import net.kyori.adventure.text.SelectorComponent;
import net.kyori.adventure.text.StorageNBTComponent;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat.FormatableChat;
/**
* Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component.
*/
public abstract class ChatStatic {
private static FormatableChat chatComponent(Component c) {
return new FormatableChat(Chat.componentToBuilder(c));
}
private static FormatableChat chatComponent(Component c) {
return new FormatableChat(componentToBuilder(c));
}
public static FormatableChat chatComponent(BaseComponent c) {
return new FormatableChat(Chat.componentToBuilder(Chat.toAdventure(c)));
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
* @param c the {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent c) {
return new FormatableChat(componentToBuilder(Chat.toAdventure(c)));
}
public static FormatableChat chatComponent(ComponentLike c) {
return chatComponent(c.asComponent());
}
public static FormatableChat chat() {
return new FormatableChat(Component.text());
}
public static FormatableChat chatComponent(BaseComponent[] c) {
return chatComponent(Chat.toAdventure(c));
}
/**
* Creates a {@link FormatableChat} from the provided {@link ComponentLike}.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
* will be untouched.
* @param c the {@link ComponentLike}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(ComponentLike c) {
return chatComponent(c.asComponent());
}
/**
* Creates a {@link FormatableChat} with an empty main text content.
* @return a new empty {@link FormatableChat}.
*/
public static FormatableChat chat() {
return new FormatableChat(Component.text());
}
/**
* Create a Chat instance with the provided plain text as its main text content.
*
* @param plainText the text to use as he content of the new Chat instance.
* @return a Chat instance with the provided text as its main text content.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat text(Object plainText) {
if (plainText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + plainText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return new FormatableChat(Component.text().content(Objects.toString(plainText)));
}
/**
* Create a Chat instance with the provided legacy text as its main text content.
*
* @param legacyText the text to use as he content of the new Chat instance.
* @return a Chat instance with the provided text as its main text content.
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyText(Object legacyText) {
if (legacyText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
}
public static FormatableChat infoText(Object plainText) {
return text(plainText).infoColor();
}
public static FormatableChat warningText(Object plainText) {
return text(plainText).warningColor();
}
public static FormatableChat dataText(Object plainText) {
return text(plainText).dataColor();
}
public static FormatableChat decorationText(Object plainText) {
return text(plainText).decorationColor();
}
public static FormatableChat successText(Object plainText) {
return text(plainText).successColor();
}
public static FormatableChat failureText(Object plainText) {
return text(plainText).failureColor();
}
public static FormatableChat playerNameText(String legacyText) {
FormatableChat fc = legacyText(legacyText);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
public static FormatableChat playerNameComponent(Component c) {
FormatableChat fc = chatComponent(c);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
public static FormatableChat translation(String key, Object... with) {
return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with)));
}
public static FormatableChat keybind(String key) {
return new FormatableChat(Component.keybind().keybind(key));
}
public static FormatableChat score(String name, String objective) {
return new FormatableChat(Component.score().name(name).objective(objective));
}
public static FormatableChat clickableURL(Chat inner, String url, Chat hover) {
Objects.requireNonNull(url, "url");
if (inner == null)
inner = text(url);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(url, 240));
return (FormatableChat) chat().clickURL(url).urlColor().hover(hover).then(inner);
}
public static FormatableChat clickableURL(Chat inner, String url) {
return clickableURL(inner, url, null);
}
public static FormatableChat clickableURL(String url, Chat hover) {
return clickableURL(null, url, hover);
}
public static FormatableChat clickableURL(String url) {
return clickableURL(null, url, null);
}
public static FormatableChat clickableCommand(Chat inner, String commandWithSlash, Chat hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
if (inner == null)
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickCommand(commandWithSlash).commandColor().hover(hover).then(inner);
}
public static FormatableChat clickableCommand(Chat inner, String commandWithSlash) {
return clickableCommand(inner, commandWithSlash, null);
}
public static FormatableChat clickableCommand(String commandWithSlash, Chat hover) {
return clickableCommand(null, commandWithSlash, hover);
}
public static FormatableChat clickableCommand(String commandWithSlash) {
return clickableCommand(null, commandWithSlash, null);
}
public static FormatableChat clickableSuggest(Chat inner, String commandWithSlash, Chat hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
if (inner == null)
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickSuggest(commandWithSlash).commandColor().hover(hover).then(inner);
}
public static FormatableChat clickableSuggest(Chat inner, String commandWithSlash) {
return clickableSuggest(inner, commandWithSlash, null);
}
public static FormatableChat clickableSuggest(String commandWithSlash, Chat hover) {
return clickableSuggest(null, commandWithSlash, hover);
}
public static FormatableChat clickableSuggest(String commandWithSlash) {
return clickableSuggest(null, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
* @param c the array of {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent[] c) {
return chatComponent(Chat.toAdventure(c));
}
public static Chat prefixedAndColored(ComponentLike message) {
return Chat.chat()
.broadcastColor()
.then(Chat.getConfig().prefix.get())
.then(message);
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat text(Object plainText) {
if (plainText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + plainText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return new FormatableChat(Component.text().content(Objects.toString(plainText)));
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content.
* @param legacyText the legacy text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyText(Object legacyText) {
if (legacyText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#infoColor configured info color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#infoColor()} instead.
*/
public static FormatableChat infoText(Object plainText) {
return text(plainText).infoColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#warningColor configured warning color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#warningColor()} instead.
*/
public static FormatableChat warningText(Object plainText) {
return text(plainText).warningColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#dataColor configured data color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#dataColor()} instead.
*/
public static FormatableChat dataText(Object plainText) {
return text(plainText).dataColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#decorationColor configured decorationColor color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#decorationColor()} instead.
*/
public static FormatableChat decorationText(Object plainText) {
return text(plainText).decorationColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#successColor configured success color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#successColor()} instead.
*/
public static FormatableChat successText(Object plainText) {
return text(plainText).successColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#failureColor configured failure color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#failureColor()} instead.
*/
public static FormatableChat failureText(Object plainText) {
return text(plainText).failureColor();
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its main text content, and colored in white in
* case there is no color on the generated parent component.
* @param legacyText the legacy text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#failureColor()} instead.
*/
public static FormatableChat playerNameText(String legacyText) {
FormatableChat fc = legacyText(legacyText);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
/**
* Creates a {@link FormatableChat} from the provided {@link Component}, coloring in white the generated parent
* component in case there is no color defined.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
* will be untouched.
* @param c the {@link Component}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat playerNameComponent(Component c) {
FormatableChat fc = chatComponent(c);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
/**
* Creates a {@link FormatableChat} with the provided translation key and parameters.
* @param key the translation key.
* @param with the translation parameters.
* @return a new {@link FormatableChat} with the provided translation key and parameters.
*/
public static FormatableChat translation(String key, Object... with) {
return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with)));
}
/**
* Creates a {@link FormatableChat} with the provided keybind.
* @param key the keybind to display.
* @return a new {@link FormatableChat} with the provided keybind.
*/
public static FormatableChat keybind(String key) {
return new FormatableChat(Component.keybind().keybind(key));
}
/**
* Creates a {@link FormatableChat} with the provided score name and objective.
* @param name the score name.
* @param objective the score objective.
* @return a new {@link FormatableChat} with the provided score name and objective.
*/
public static FormatableChat score(String name, String objective) {
return new FormatableChat(Component.score().name(name).objective(objective));
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* @param inner the component to make clickable.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(ComponentLike inner, String url, HoverEventSource<?> hover) {
Objects.requireNonNull(url, "url");
if (inner == null)
inner = text(url);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(url, 240));
return (FormatableChat) chat().clickURL(url).urlColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* <p>
* When hovered, the component will display the url. To customize the hover content, use
* {@link #clickableURL(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(ComponentLike inner, String url) {
return clickableURL(inner, url, null);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* <p>
* The text on which to click will be the URL itself. To configure the clicked text, use
* {@link #clickableURL(ComponentLike, String, HoverEventSource)}.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(String url, HoverEventSource<?> hover) {
return clickableURL(null, url, hover);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* <p>
* The text on which to click will be the URL itself. To configure the clicked text, use
* {@link #clickableURL(ComponentLike, String)}.
* <p>
* When hovered, the component will display the url. To customize the hover content, use
* {@link #clickableURL(String, HoverEventSource)}.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(String url) {
return clickableURL(null, url, null);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* @param inner the component to make clickable.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
if (inner == null)
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickCommand(commandWithSlash).commandColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableCommand(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(ComponentLike inner, String commandWithSlash) {
return clickableCommand(inner, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableCommand(ComponentLike, String, HoverEventSource)}.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(String commandWithSlash, HoverEventSource<?> hover) {
return clickableCommand(null, commandWithSlash, hover);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableCommand(ComponentLike, String)}.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableCommand(String, HoverEventSource)}.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(String commandWithSlash) {
return clickableCommand(null, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @param inner the component to make clickable.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
if (inner == null)
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickSuggest(commandWithSlash).commandColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableSuggest(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(ComponentLike inner, String commandWithSlash) {
return clickableSuggest(inner, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableSuggest(ComponentLike, String, HoverEventSource)}.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(String commandWithSlash, HoverEventSource<?> hover) {
return clickableSuggest(null, commandWithSlash, hover);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableSuggest(ComponentLike, String)}.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableSuggest(String, HoverEventSource)}.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(String commandWithSlash) {
return clickableSuggest(null, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
* @param text the text aligned to the left.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
* @see ChatFilledLine#leftText(ComponentLike)
*/
public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.leftText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* color and a left-aligned text.
* @param text the text aligned to the left.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a left-aligned text.
* @see ChatFilledLine#leftText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat leftText(ComponentLike text, boolean console) {
return ChatFilledLine.leftText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned text.
* @param text the text aligned to the right.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned
* text.
* @see ChatFilledLine#rightText(ComponentLike)
*/
public static FormatableChat rightText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.rightText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* color and a right-aligned text.
* @param text the text aligned to the right.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a right-aligned text.
* @see ChatFilledLine#rightText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat rightText(ComponentLike text, boolean console) {
return ChatFilledLine.rightText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
* @param text the text aligned to the center.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
* @see ChatFilledLine#centerText(ComponentLike)
*/
public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.centerText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* color and a centered text.
* @param text the text aligned to the center.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a centered text.
* @see ChatFilledLine#centerText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat centerText(ComponentLike text, boolean console) {
return ChatFilledLine.centerText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* @param decorationChar the character used for decoration.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* @see ChatFilledLine#filled()
*/
public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.filled().decoChar(decorationChar).decoColor(decorationColor).console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* color.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* @see ChatFilledLine#filled()
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat filledLine(boolean console) {
return ChatFilledLine.filled().console(console).toChat();
}
/**
* Adds the configured prefix and broadcast color to the provided message.
* @param message the message to decorate.
* @return the decorated message.
*/
public static Chat prefixedAndColored(ComponentLike message) {
return Chat.chat()
.broadcastColor()
.then(ChatConfig.prefix.get())
.then(message);
}
private static ComponentBuilder<?, ?> componentToBuilder(Component c) {
ComponentBuilder<?, ?> builder;
if (c instanceof TextComponent) {
builder = Component.text()
.content(((TextComponent) c).content());
}
else if (c instanceof TranslatableComponent) {
builder = Component.translatable()
.key(((TranslatableComponent) c).key())
.args(((TranslatableComponent) c).args());
}
else if (c instanceof SelectorComponent) {
builder = Component.selector()
.pattern(((SelectorComponent) c).pattern());
}
else if (c instanceof ScoreComponent) {
builder = Component.score()
.name(((ScoreComponent) c).name())
.objective(((ScoreComponent) c).objective());
}
else if (c instanceof KeybindComponent) {
builder = Component.keybind()
.keybind(((KeybindComponent) c).keybind());
}
else if (c instanceof BlockNBTComponent) {
builder = Component.blockNBT()
.interpret(((BlockNBTComponent) c).interpret())
.nbtPath(((BlockNBTComponent) c).nbtPath())
.pos(((BlockNBTComponent) c).pos());
}
else if (c instanceof EntityNBTComponent) {
builder = Component.entityNBT()
.interpret(((EntityNBTComponent) c).interpret())
.nbtPath(((EntityNBTComponent) c).nbtPath())
.selector(((EntityNBTComponent) c).selector());
}
else if (c instanceof StorageNBTComponent) {
builder = Component.storageNBT()
.interpret(((StorageNBTComponent) c).interpret())
.nbtPath(((StorageNBTComponent) c).nbtPath())
.storage(((StorageNBTComponent) c).storage());
}
else {
throw new IllegalArgumentException("Unknows component type " + c.getClass());
}
return builder.style(c.style()).append(c.children());
}
}

View File

@ -0,0 +1,77 @@
package fr.pandacube.lib.chat;
import java.util.ArrayList;
import java.util.List;
/**
* A tree structure of {@link Chat} component intended to be rendered using {@link #render(boolean)}.
*/
public class ChatTreeNode {
private static final String TREE_MIDDLE_CONNECTED = "";
private static final String TREE_END_CONNECTED = "";
private static final String TREE_MIDDLE_OPEN = "│§0`§r";
private static final String TREE_END_OPEN = "§0```§r";
private static final String TREE_MIDDLE_OPEN_CONSOLE = "";
private static final String TREE_END_OPEN_CONSOLE = " "; // nbsp
/**
* The component for the current node.
*/
public final Chat component;
/**
* Children nodes.
*/
public final List<ChatTreeNode> children = new ArrayList<>();
/**
* Construct an new {@link ChatTreeNode}.
* @param cmp the component for the current node.
*/
public ChatTreeNode(Chat cmp) {
component = cmp;
}
/**
* Adds a child to the current node.
* @param child the child to add.
* @return this.
*/
public ChatTreeNode addChild(ChatTreeNode child) {
children.add(child);
return this;
}
/**
* Generate a tree view based on this tree structure.
* <p>
* Each element in the returned list represent 1 line of this tree view.
* Thus, the caller may send each line separately or at once depending of the quantity of data.
* @param console true to render for console, false otherwise.
* @return an array of component, each element being a single line.
*/
public List<Chat> render(boolean console) {
List<Chat> ret = new ArrayList<>();
ret.add(ChatStatic.chat()
.then(component));
for (int i = 0; i < children.size(); i++) {
List<Chat> childComponents = children.get(i).render(console);
boolean last = i == children.size() - 1;
for (int j = 0; j < childComponents.size(); j++) {
String prefix = last ? (j == 0 ? TREE_END_CONNECTED : (console ? TREE_END_OPEN_CONSOLE : TREE_END_OPEN))
: (j == 0 ? TREE_MIDDLE_CONNECTED : (console ? TREE_MIDDLE_OPEN_CONSOLE : TREE_MIDDLE_OPEN));
ret.add(ChatStatic.text(prefix)
.then(childComponents.get(j)));
}
}
return ret;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import java.util.OptionalLong;
import java.util.Set;
import java.util.stream.LongStream;
import fr.pandacube.lib.chat.ChatUtil.DisplayTreeNode;
import fr.pandacube.lib.chat.ChatTreeNode;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedEntity;
import fr.pandacube.lib.permissions.SQLPermissions.EntityType;
import fr.pandacube.lib.util.Log;
@ -50,7 +50,7 @@ public abstract class PermEntity {
}
public DisplayTreeNode debugPrefix() {
public ChatTreeNode debugPrefix() {
return Permissions.resolver.debugPrefix(name, type);
}
@ -69,7 +69,7 @@ public abstract class PermEntity {
}
public DisplayTreeNode debugSuffix() {
public ChatTreeNode debugSuffix() {
return Permissions.resolver.debugSuffix(name, type);
}
@ -159,15 +159,15 @@ public abstract class PermEntity {
}
public DisplayTreeNode debugPermission(String permission) {
public ChatTreeNode debugPermission(String permission) {
return debugPermission(permission, null, null);
}
public DisplayTreeNode debugPermission(String permission, String server) {
public ChatTreeNode debugPermission(String permission, String server) {
return debugPermission(permission, server, null);
}
public DisplayTreeNode debugPermission(String permission, String server, String world) {
public ChatTreeNode debugPermission(String permission, String server, String world) {
return Permissions.resolver.debugPermission(name, type, permission, server, world);
}

View File

@ -18,8 +18,7 @@ import com.google.common.cache.CacheBuilder;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.ChatUtil;
import fr.pandacube.lib.chat.ChatUtil.DisplayTreeNode;
import fr.pandacube.lib.chat.ChatTreeNode;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedEntity;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedGroup;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedPlayer;
@ -66,10 +65,10 @@ public class PermissionsResolver {
return getEffectiveData(name, type, DataType.SUFFIX);
}
/* package */ DisplayTreeNode debugPrefix(String name, EntityType type) {
/* package */ ChatTreeNode debugPrefix(String name, EntityType type) {
return debugData(name, type, DataType.PREFIX);
}
/* package */ DisplayTreeNode debugSuffix(String name, EntityType type) {
/* package */ ChatTreeNode debugSuffix(String name, EntityType type) {
return debugData(name, type, DataType.SUFFIX);
}
@ -89,7 +88,7 @@ public class PermissionsResolver {
}
}
private DisplayTreeNode debugData(String name, EntityType type, DataType dataType) {
private ChatTreeNode debugData(String name, EntityType type, DataType dataType) {
CachedEntity entity = (type == EntityType.User)
? backendReader.getCachedPlayer(UUID.fromString(name))
: backendReader.getCachedGroup(name);
@ -104,7 +103,7 @@ public class PermissionsResolver {
if (resolutionResult.conflict) {
Log.warning("For data " + dataType + ":\n"
+ ChatUtil.treeView(resolutionResult.toDisplayTreeNode(), true).stream()
+ resolutionResult.toDisplayTreeNode().render(true).stream()
.map(Chat::getLegacyText)
.collect(Collectors.joining(ChatColor.RESET + "\n")));
}
@ -163,7 +162,7 @@ public class PermissionsResolver {
conflict = c != null;
}
public DisplayTreeNode toDisplayTreeNode() {
public ChatTreeNode toDisplayTreeNode() {
Chat c = Chat.text(entity.name);
if (result == null)
c.then(Chat.text(" (non défini)").gray());
@ -171,11 +170,11 @@ public class PermissionsResolver {
c.thenLegacyText(" \"" + ChatColor.RESET + result + ChatColor.RESET + "\"");
if (conflictMessage != null)
c.thenFailure(" " + conflictMessage);
DisplayTreeNode node = new DisplayTreeNode(c);
ChatTreeNode node = new ChatTreeNode(c);
if (result == null && !conflict && !inheritances.isEmpty()) {
// there is nothing interesting to show on current or subnode
node.children.add(new DisplayTreeNode(Chat.text("(Inheritances hidden for brevety)").darkGray().italic()));
node.children.add(new ChatTreeNode(Chat.text("(Inheritances hidden for brevety)").darkGray().italic()));
return node;
}
@ -290,7 +289,7 @@ public class PermissionsResolver {
}
}
/* package */ DisplayTreeNode debugPermission(String name, EntityType type, String permission, String server, String world) {
/* package */ ChatTreeNode debugPermission(String name, EntityType type, String permission, String server, String world) {
CachedEntity entity = (type == EntityType.User)
? backendReader.getCachedPlayer(UUID.fromString(name))
: backendReader.getCachedGroup(name);
@ -306,7 +305,7 @@ public class PermissionsResolver {
if (resolutionResult.conflict) {
Log.warning("For permission " + permission + ":\n"
+ ChatUtil.treeView(resolutionResult.toDisplayTreeNode(), true).stream()
+ resolutionResult.toDisplayTreeNode().render(true).stream()
.map(Chat::getLegacyText)
.collect(Collectors.joining(ChatColor.RESET + "\n")));
}
@ -484,7 +483,7 @@ public class PermissionsResolver {
conflict = c != null;
}
public DisplayTreeNode toDisplayTreeNode() {
public ChatTreeNode toDisplayTreeNode() {
Chat c = Chat.chat()
.then(result == PermState.UNDEFINED ? Chat.dataText("") : result == PermState.GRANTED ? Chat.successText("") : Chat.failureText(""))
.then(Chat.text(entity instanceof CachedPlayer cp ? Permissions.playerNameGetter.apply(cp.playerId) : entity.name)
@ -496,13 +495,13 @@ public class PermissionsResolver {
c.thenData(" w=" + world);
if (conflictMessage != null)
c.then(Chat.failureText(" " + conflictMessage));
DisplayTreeNode node = new DisplayTreeNode(c);
ChatTreeNode node = new ChatTreeNode(c);
selfPermissions.forEach(p -> node.children.add(p.toDisplayTreeNode()));
if (result == PermState.UNDEFINED && !conflict && !inheritances.isEmpty()) {
// there is nothing interesting to show on current or subnode
node.children.add(new DisplayTreeNode(Chat.text("(Inheritances hidden for brevety)").darkGray().italic()));
node.children.add(new ChatTreeNode(Chat.text("(Inheritances hidden for brevety)").darkGray().italic()));
return node;
}
@ -521,8 +520,8 @@ public class PermissionsResolver {
result = r;
type = t;
}
public DisplayTreeNode toDisplayTreeNode() {
return new DisplayTreeNode(Chat.chat()
public ChatTreeNode toDisplayTreeNode() {
return new ChatTreeNode(Chat.chat()
.then(result ? Chat.successText("") : Chat.failureText(""))
.then(Chat.text(permission).color(type == PermType.WILDCARD ? ChatColor.YELLOW : type == PermType.SPECIAL ? ChatColor.LIGHT_PURPLE : ChatColor.WHITE)));
}