diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/Chat.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/Chat.java index 568db4c..623a266 100644 --- a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/Chat.java +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/Chat.java @@ -3,21 +3,13 @@ package fr.pandacube.lib.chat; import java.awt.Color; import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.function.UnaryOperator; import net.kyori.adventure.key.Key; -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.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; @@ -32,424 +24,981 @@ import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; +/** + * A builder for chat components. + *

+ * Use one of the provided static methods to create a new instance. + *

+ * This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in + * Adventure API and its implentation without using the final methods of this builder. + *

+ * The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formating of the + * builded component. The rationale for this design is explained in the documentation of {@link FormatableChat}. + */ public abstract sealed class Chat extends ChatStatic implements HoverEventSource, ComponentLike { - - protected final ComponentBuilder builder; - protected boolean console = false; - - /* package */ Chat(ComponentBuilder b) { - builder = Objects.requireNonNull(b, "Provided component builder must not be null"); - } - - - public Component getAdv() { - return builder.build(); - } - - public BaseComponent get() { - return toBungee(getAdv()); - } - - public BaseComponent[] getAsArray() { - return toBungeeArray(getAdv()); - } - - private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder() - .hexColors() - .useUnusualXRepeatedCharacterHexFormat() - .build(); - public String getLegacyText() { - return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv()); - } - - public String getPlainText() { - return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), ""); - } - - - - - - - - public Chat then(Component comp) { - if (comp instanceof TextComponent txtComp) { - if (!txtComp.hasStyling() && (txtComp.content().isEmpty())) { - // no need to add the provided component to the current component. - // but eventual child component must be added - if (!txtComp.children().isEmpty()) { - for (Component child : txtComp.children()) - then(child); - } - return this; - } - } - builder.append(comp); - return this; - } - public Chat then(BaseComponent subComponent) { - return then(toAdventure(subComponent)); - } - public Chat then(ComponentLike comp) { - return then(comp.asComponent()); - } - public Chat then(BaseComponent[] components) { - return then(toAdventure(components)); - } - - public Chat thenText(Object plainText) { return then(text(plainText)); } - public Chat thenInfo(Object plainText) { return then(infoText(plainText)); } - public Chat thenWarning(Object plainText) { return then(warningText(plainText)); } - public Chat thenSuccess(Object plainText) { return then(successText(plainText)); } - public Chat thenFailure(Object plainText) { return then(failureText(plainText)); } - public Chat thenData(Object plainText) { return then(dataText(plainText)); } - public Chat thenDecoration(Object plainText) { return then(decorationText(plainText)); } - public Chat thenPlayerName(String legacyText) { return then(playerNameText(legacyText)); } - public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); } - public Chat thenNewLine() { return then(Component.newline()); } - public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); } - public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); } - public Chat thenKeyBind(String key) { return then(keybind(key)); } - public Chat thenScore(String name, String objective) { return then(score(name, objective)); } - - public Chat thenURLLink(Chat inner, String url, Chat hover) { return then(clickableURL(inner, url, hover)); } - public Chat thenURLLink(Chat inner, String url) { return then(clickableURL(inner, url)); } - public Chat thenURLLink(String url, Chat hover) { return then(clickableURL(url, hover)); } - public Chat thenURLLink(String url) { return then(clickableURL(url)); } - - public Chat thenCommandLink(Chat inner, String cmdWithSlash, Chat hover) { return then(clickableCommand(inner, cmdWithSlash, hover)); } - public Chat thenCommandLink(Chat inner, String cmdWithSlash) { return then(clickableCommand(inner, cmdWithSlash)); } - public Chat thenCommandLink(String cmdWithSlash, Chat hover) { return then(clickableCommand(cmdWithSlash, hover)); } - public Chat thenCommandLink(String cmdWithSlash) { return then(clickableCommand(cmdWithSlash)); } - - public Chat thenCommandSuggest(Chat inner, String cmdWithSlash, Chat hover) { return then(clickableSuggest(inner, cmdWithSlash, hover)); } - public Chat thenCommandSuggest(Chat inner, String cmdWithSlash) { return then(clickableSuggest(inner, cmdWithSlash)); } - public Chat thenCommandSuggest(String cmdWithSlash, Chat hover) { return then(clickableSuggest(cmdWithSlash, hover)); } - public Chat thenCommandSuggest(String cmdWithSlash) { return then(clickableSuggest(cmdWithSlash)); } - - - - /** - * Draws a full line with the default decoration char, colored with the default decoration color. - * @return this, for method chaining - */ - public Chat thenEmptyCharLine() { - return then(ChatUtil.emptyLine(config.decorationChar, config.decorationColor, console)); - } - - - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided Chat left aligned on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenLeftTextCharLine(Chat leftText) { - return then(ChatUtil.leftText(chat().decorationColor().thenText(" ").then(leftText).thenText(" "), config.decorationChar, - config.decorationColor, config.nbCharMargin, console)); - } - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided component left aligned on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenLeftTextCharLine(BaseComponent leftText) { - return thenLeftTextCharLine(chatComponent(leftText)); - } - - - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided Chat right aligned on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenRightTextCharLine(Chat rightText) { - return then(ChatUtil.rightText(chat().decorationColor().thenText(" ").then(rightText).thenText(" "), config.decorationChar, - config.decorationColor, config.nbCharMargin, console)); - } - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided component right aligned on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenRightTextCharLine(BaseComponent leftText) { - return thenRightTextCharLine(chatComponent(leftText)); - } - - - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided Chat centered on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenCenterTextCharLine(Chat centerText) { - return then(ChatUtil.centerText(chat().decorationColor().thenText(" ").then(centerText).thenText(" "), config.decorationChar, - config.decorationColor, console)); - } - /** - * Draws a full line with the default decoration char, colored with the default decoration color, - * and with the provided component centered on the line, default to the decoration color, and surrounded with 1 space on each side. - * @return this, for method chaining - */ - public Chat thenCenterTextCharLine(BaseComponent leftText) { - return thenCenterTextCharLine(chatComponent(leftText)); - } - - - - - - - - - - - - - - public static final class FormatableChat extends Chat { - /* package */ FormatableChat(ComponentBuilder c) { - super(c); - } - - public FormatableChat console(boolean c) { console = c; return this; } + /* package */ final ComponentBuilder builder; + /* package */ boolean console = false; + /* package */ Integer maxWidth = null; - public FormatableChat color(TextColor c) { builder.color(c); return this; } - public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); } - public FormatableChat color(Color c) { return color(c == null ? null : TextColor.color(c.getRGB())); } - public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); } - - public FormatableChat black() { return color(NamedTextColor.BLACK); } - public FormatableChat darkBlue() { return color(NamedTextColor.DARK_BLUE); } - public FormatableChat darkGreen() { return color(NamedTextColor.DARK_GREEN); } - public FormatableChat darkAqua() { return color(NamedTextColor.DARK_AQUA); } - public FormatableChat darkRed() { return color(NamedTextColor.DARK_RED); } - public FormatableChat darkPurple() { return color(NamedTextColor.DARK_PURPLE); } - public FormatableChat gold() { return color(NamedTextColor.GOLD); } - public FormatableChat gray() { return color(NamedTextColor.GRAY); } - public FormatableChat darkGray() { return color(NamedTextColor.DARK_GRAY); } - public FormatableChat blue() { return color(NamedTextColor.BLUE); } - public FormatableChat green() { return color(NamedTextColor.GREEN); } - public FormatableChat aqua() { return color(NamedTextColor.AQUA); } - public FormatableChat red() { return color(NamedTextColor.RED); } - public FormatableChat lightPurple() { return color(NamedTextColor.LIGHT_PURPLE); } - public FormatableChat yellow() { return color(NamedTextColor.YELLOW); } - public FormatableChat white() { return color(NamedTextColor.WHITE); } - - public FormatableChat successColor() { return color(config.successColor); } - public FormatableChat failureColor() { return color(config.failureColor); } - public FormatableChat infoColor() { return color(config.infoColor); } - public FormatableChat warningColor() { return color(config.warningColor); } - public FormatableChat dataColor() { return color(config.dataColor); } - public FormatableChat decorationColor() { return color(config.decorationColor); } - public FormatableChat urlColor() { return color(config.urlColor); } - public FormatableChat commandColor() { return color(config.commandColor); } - public FormatableChat highlightedCommandColor() { return color(config.highlightedCommandColor); } - public FormatableChat broadcastColor() { return color(config.broadcastColor); } - - private FormatableChat setStyle(Consumer styleOp) { builder.style(styleOp); return this; } - - private FormatableChat setDecoration(TextDecoration deco, Boolean state) { - return setStyle(b -> b.decoration(deco, State.byBoolean(state))); - } - - public FormatableChat bold(Boolean b) { return setDecoration(TextDecoration.BOLD, b); } - public FormatableChat bold() { return bold(true); } - - public FormatableChat italic(Boolean i) { return setDecoration(TextDecoration.ITALIC, i); } - public FormatableChat italic() { return italic(true); } - - public FormatableChat underlined(Boolean u) { return setDecoration(TextDecoration.UNDERLINED, u); } - public FormatableChat underlined() { return underlined(true); } - - public FormatableChat strikethrough(Boolean s) { return setDecoration(TextDecoration.STRIKETHROUGH, s); } - public FormatableChat strikethrough() { return strikethrough(true); } - - public FormatableChat obfuscated(Boolean o) { return setDecoration(TextDecoration.OBFUSCATED, o); } - public FormatableChat obfuscated() { return obfuscated(true); } - - public FormatableChat font(Key f) { return setStyle(s -> s.font(f)); } - - public FormatableChat shiftClickInsertion(String i) { builder.insertion(i); return this; } - - private FormatableChat click(ClickEvent e) { builder.clickEvent(e); return this; } - public FormatableChat clickCommand(String cmdWithSlash) { return click(ClickEvent.runCommand(cmdWithSlash)); } - public FormatableChat clickSuggest(String cmdWithSlash) { return click(ClickEvent.suggestCommand(cmdWithSlash)); } - public FormatableChat clickClipboard(String value) { return click(ClickEvent.copyToClipboard(value)); } - public FormatableChat clickURL(String url) { return click(ClickEvent.openUrl(url)); } - public FormatableChat clickBookPage(int page) { return click(ClickEvent.changePage(page)); } - - public FormatableChat hover(HoverEventSource e) { builder.hoverEvent(e); return this; } - public FormatableChat hover(Chat v) { return hover(v.getAdv()); } - public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); } - public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); } - public FormatableChat hover(String legacyText) { return hover(legacyText(legacyText)); } - - } - - - - - - - @Override - public HoverEvent asHoverEvent(UnaryOperator op) { - return HoverEvent.showText(op.apply(getAdv())); - } - - @Override - public Component asComponent() { - return getAdv(); - } - - - - - - - @Override - public boolean equals(Object obj) { - return obj instanceof Chat c - && getAdv().equals(c.getAdv()); - } - - @Override - public int hashCode() { - return getAdv().hashCode(); - } - - @Override - public String toString() { - return getPlainText(); - } - - - + /* package */ Chat(ComponentBuilder b) { + builder = Objects.requireNonNull(b, "Provided component builder must not be null"); + } - /* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) { - if (values == null) - return null; - ComponentLike[] ret = new ComponentLike[values.length]; - for (int i = 0; i < values.length; i++) { - Object v = values[i]; - if (v instanceof BaseComponent[]) - ret[i] = toAdventure((BaseComponent[]) v); - else if (v instanceof BaseComponent) - ret[i] = toAdventure((BaseComponent) v); - else if (v instanceof ComponentLike) - ret[i] = (ComponentLike) v; - else - ret[i] = Component.text(Objects.toString(v)); - } - return ret; - } - - public static Component toAdventure(BaseComponent[] components) { - return BungeeComponentSerializer.get().deserialize(components); - } - public static Component toAdventure(BaseComponent component) { - return toAdventure(new BaseComponent[] { component }); - } - - public static BaseComponent[] toBungeeArray(Component component) { - return BungeeComponentSerializer.get().serialize(component); - } - public static BaseComponent toBungee(Component component) { - BaseComponent[] arr = toBungeeArray(component); - return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr); - } - - - public 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()); - } - - - public static Chat italicFalseIfNotSet(Chat c) { - c.builder.style(b -> { - if (b.build().decoration(TextDecoration.ITALIC) == State.NOT_SET) { - ((FormatableChat) c).italic(false); - } - }); - return c; - } - - - - - protected static final Config config = new Config(); - - public static Config getConfig() { - return config; - } - - public static class Config { - public TextColor decorationColor = NamedTextColor.YELLOW; - public char decorationChar = '-'; - public int nbCharMargin = 1; - public TextColor successColor = NamedTextColor.GREEN; - public TextColor failureColor = NamedTextColor.RED; - public TextColor infoColor = NamedTextColor.GOLD; - public TextColor warningColor = NamedTextColor.GOLD; - public TextColor dataColor = NamedTextColor.GRAY; - public TextColor urlColor = NamedTextColor.GREEN; - public TextColor commandColor = NamedTextColor.GRAY; - public TextColor highlightedCommandColor = NamedTextColor.WHITE; - public TextColor broadcastColor = NamedTextColor.YELLOW; - public Supplier prefix; - - public int getPrefixWidth(boolean console) { - Chat c = prefix == null ? null : prefix.get(); - return c == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console); - } - } - - - + + + + + + + /* + * Builder terminal operation and serialization + */ + + + /** + * Builds the component into Adventure Component instance. + * @return the {@link Component} builded from this {@link Chat} component. + */ + public Component getAdv() { + return builder.build(); + } + + /** + * Builds the component into BungeeCord {@link BaseComponent} instance. + * @return the {@link BaseComponent} builded from this {@link Chat} component. + */ + public BaseComponent get() { + return toBungee(getAdv()); + } + + /** + * Builds the component into BungeeCord {@link BaseComponent} array. + * @return the {@link BaseComponent} array builded from this {@link Chat} component. + */ + public BaseComponent[] getAsArray() { + return toBungeeArray(getAdv()); + } + + private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder() + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build(); + + /** + * Converts the builded component into legacy text. + * @return the legacy text. RGB colors are in BungeeCord format. + */ + public String getLegacyText() { + return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv()); + } + + /** + * Converts the builded component into plain text. + * @return the plain text of this component. + */ + public String getPlainText() { + return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), ""); + } + + @Override + public HoverEvent asHoverEvent(UnaryOperator op) { + return HoverEvent.showText(op.apply(getAdv())); + } + + /** + * Builds the component into Adventure Component instance. + * @return the {@link Component} builded from this {@link Chat} component. + */ + @Override + public Component asComponent() { + return getAdv(); + } + + + + + + + + + + + + + /* + * Sub-component appending + */ + + + /** + * Appends a component to this component. + * @param comp the component to append. + * @return this. + */ + public Chat then(Component comp) { + if (comp instanceof TextComponent txtComp) { + if (!txtComp.hasStyling() && (txtComp.content().isEmpty())) { + // no need to add the provided component to the current component. + // but eventual child component must be added + if (!txtComp.children().isEmpty()) { + for (Component child : txtComp.children()) + then(child); + } + return this; + } + } + builder.append(comp); + return this; + } + + /** + * Appends a BungeeCord {@link BaseComponent} to this component. + * @param comp the component to append. + * @return this. + */ + public Chat then(BaseComponent comp) { + return then(toAdventure(comp)); + } + + /** + * Appends a component to this component. + * @param comp the component to append. + * @return this. + */ + public Chat then(ComponentLike comp) { + if (comp instanceof ChatFilledLine ac) { + ac.console(console); + if (maxWidth != null) + ac.maxWidth(maxWidth); + } + return then(comp.asComponent()); + } + + /** + * Appends a BungeeCord {@link BaseComponent} array to this component. + * @param comp the components to append. + * @return this. + */ + public Chat then(BaseComponent[] comp) { + return then(toAdventure(comp)); + } + + + + + + + + + /* + * Special sub-components appending + */ + + /** + * Appends a plain text to this component. + * @param plainText the plain text. + * @return this. + */ + public Chat thenText(Object plainText) { return then(text(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#infoColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenInfo(Object plainText) { return then(infoText(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#warningColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenWarning(Object plainText) { return then(warningText(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#successColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenSuccess(Object plainText) { return then(successText(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#failureColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenFailure(Object plainText) { return then(failureText(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#dataColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenData(Object plainText) { return then(dataText(plainText)); } + + /** + * Appends a plain text to this component, colored using {@link ChatConfig#decorationColor}. + * @param plainText the plain text. + * @return this. + */ + public Chat thenDecoration(Object plainText) { return then(decorationText(plainText)); } + + /** + * Appends a component 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. + * @return this. + */ + public Chat thenPlayerName(String legacyText) { return then(playerNameText(legacyText)); } + + /** + * Appends the provided Component, coloring it in white in case there is no color defined. If the provided component + * is an instance of Chat, its content will be duplicated, and the provided one will be untouched. + * @param comp the component. + * @return this. + */ + public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); } + + /** + * Appends a component consisting of a new line. + * @return this. + */ + public Chat thenNewLine() { return then(Component.newline()); } + + /** + * Appends a component with the provided legacy text as its content. + * @param legacyText the legacy text. + * @return this. + */ + public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); } + + /** + * Appends a component with the provided translation key and parameters. + * @param key the translation key. + * @param with the translation parameters. + * @return this. + */ + public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); } + + /** + * Appends a component with the provided keybind. + * @param key the keybind to display. + * @return this. + */ + public Chat thenKeyBind(String key) { return then(keybind(key)); } + + /** + * Appends a component with the provided score name and objective. + * @param name the score name. + * @param objective the score objective. + * @return this. + */ + public Chat thenScore(String name, String objective) { return then(score(name, objective)); } + + + /** + * Appends a component 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 this. + */ + public Chat thenClickableURL(ComponentLike inner, String url, HoverEventSource hover) { return then(clickableURL(inner, url, hover)); } + + /** + * Appends a component that leads to a URL when clicked. + *

+ * When hovered, the component will display the url. To customize the hover content, use + * {@link #thenClickableURL(ComponentLike, String, HoverEventSource)}. + * @param inner the component to make clickable. + * @param url the target url. Must start with {@code "http://"} or {@code "https://"}. + * @return this. + */ + public Chat thenClickableURL(ComponentLike inner, String url) { return then(clickableURL(inner, url)); } + + /** + * Appends a component that leads to a URL when clicked. + *

+ * The text on which to click will be the URL itself. To configure the clicked text, use + * {@link #thenClickableURL(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 this. + */ + public Chat thenClickableURL(String url, HoverEventSource hover) { return then(clickableURL(url, hover)); } + + /** + * Appends a component that leads to a URL when clicked. + *

+ * The text on which to click will be the URL itself. To configure the clicked text, use + * {@link #thenClickableURL(ComponentLike, String)}. + *

+ * When hovered, the component will display the url. To customize the hover content, use + * {@link #thenClickableURL(String, HoverEventSource)}. + * @param url the target url. Must start with {@code "http://"} or {@code "https://"}. + * @return this. + */ + public Chat thenClickableURL(String url) { return then(clickableURL(url)); } + + + /** + * Appends a component that runs a command when clicked. + * @param inner the component to make clickable. + * @param cmdWithSlash the command to run. Must start with {@code "/"}. + * @param hover the content to display when hovering the component. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenClickableCommand(ComponentLike inner, String cmdWithSlash, HoverEventSource hover) { return then(clickableCommand(inner, cmdWithSlash, hover)); } + + /** + * Appends a component that runs a command when clicked. + *

+ * When hovered, the component will display the command itself. To customize the hover content, use + * {@link #thenClickableCommand(ComponentLike, String, HoverEventSource)}. + * @param inner the component to make clickable. + * @param cmdWithSlash the command to run. Must start with {@code "/"}. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenClickableCommand(ComponentLike inner, String cmdWithSlash) { return then(clickableCommand(inner, cmdWithSlash)); } + + /** + * Appends a component that runs a command when clicked. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #thenClickableCommand(ComponentLike, String, HoverEventSource)}. + * @param cmdWithSlash the command to run. Must start with {@code "/"}. + * @param hover the content to display when hovering the component. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenClickableCommand(String cmdWithSlash, HoverEventSource hover) { return then(clickableCommand(cmdWithSlash, hover)); } + + /** + * Appends a component that runs a command when clicked. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #thenClickableCommand(ComponentLike, String)}. + *

+ * When hovered, the component will display the command itself. To customize the hover content, use + * {@link #thenClickableCommand(String, HoverEventSource)}. + * @param cmdWithSlash the command to run. Must start with {@code "/"}. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenClickableCommand(String cmdWithSlash) { return then(clickableCommand(cmdWithSlash)); } + + + /** + * Appends a component that pre-fill the chat box with a command when clicked. + * @param inner the component to make clickable. + * @param cmdWithSlash the command to suggest. Must start with {@code "/"}. + * @param hover the content to display when hovering the component. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenCommandSuggest(ComponentLike inner, String cmdWithSlash, HoverEventSource hover) { return then(clickableSuggest(inner, cmdWithSlash, hover)); } + + /** + * Appends a component that pre-fill the chat box with a command when clicked. + *

+ * When hovered, the component will display the command itself. To customize the hover content, use + * {@link #thenCommandSuggest(ComponentLike, String, HoverEventSource)}. + * @param inner the component to make clickable. + * @param cmdWithSlash the command to suggest. Must start with {@code "/"}. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenCommandSuggest(ComponentLike inner, String cmdWithSlash) { return then(clickableSuggest(inner, cmdWithSlash)); } + + /** + * Appends a component that pre-fill the chat box with a command when clicked. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #thenCommandSuggest(ComponentLike, String, HoverEventSource)}. + * @param cmdWithSlash the command to suggest. Must start with {@code "/"}. + * @param hover the content to display when hovering the component. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenCommandSuggest(String cmdWithSlash, HoverEventSource hover) { return then(clickableSuggest(cmdWithSlash, hover)); } + + /** + * Appends a component that pre-fill the chat box with a command when clicked. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #thenCommandSuggest(ComponentLike, String)}. + *

+ * When hovered, the component will display the command itself. To customize the hover content, use + * {@link #thenCommandSuggest(String, HoverEventSource)}. + * @param cmdWithSlash the command to suggest. Must start with {@code "/"}. + * @return this. + * @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}. + */ + public Chat thenCommandSuggest(String cmdWithSlash) { return then(clickableSuggest(cmdWithSlash)); } + + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a left-aligned text. + * @param leftText the text aligned to the left. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a left-aligned text. + */ + public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a left-aligned text. + * @param leftText the text aligned to the left. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a left-aligned text. + * @deprecated uses Bungeecord chat API. + */ + @Deprecated + public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a right-aligned text. + * @param rightText the text aligned to the right. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a right-aligned text. + */ + public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a right-aligned text. + * @param rightText the text aligned to the right. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a right-aligned text. + * @deprecated uses Bungeecord chat API. + */ + @Deprecated + public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a centered text. + * @param centerText the text aligned to the center. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a centered text. + */ + public Chat thenCenterText(ComponentLike centerText) { + return then(centerText(centerText, console)); + } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and + * color and a centered text. + * @param centerText the text aligned to the center. + * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character + * and color and a centered text. + * @deprecated uses Bungeecord chat API. + */ + @Deprecated + public Chat thenCenterText(BaseComponent centerText) { + return thenCenterText(chatComponent(centerText)); + } + + /** + * Appends a component filling a line of chat (or console) with the configured decoration character and color. + * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. + */ + public Chat thenFilledLine() { return then(filledLine(console)); } + + + + + + + + + + + + /** + * A {@link Chat} that can be formatted. + *

+ * The purpose of subclassing {@link Chat} is to avoid ambiguity with the way the Bungee chat component builder works. + * Here is an example of to use their builder (from + * the Spigot wiki): + *

{@code
+     * BaseComponent[] component = new ComponentBuilder("Hello ").color(ChatColor.RED)
+     *         .append("world").color(ChatColor.DARK_RED).bold(true)
+     *         .append("!").color(ChatColor.RED)
+     *         .create();
+     * }
+ * Here, when you call a formating method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the + * {@code append(String)} method, the formating apply to the last sub-component appended. + *

+ * In our design, we want the formating to apply to the currently builded component, not the last appended one. + * The purpose is to make the component structure clearer and have better control of the formating over the + * component hierarchy. + * Here is the equivalent of the above code, with the {@link Chat} API: + *

{@code
+     * Chat component = Chat.text("Hello ").red()
+     *         .then(Chat.text("world").darkRed().bold())
+     *         .thenText("!"); // short for .then(Chat.text("!"))
+     *         // the red color for "!" is not needed because the parent component is already red.
+     * }
+ * When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, casted + * to {@link Chat}, to prevent future formating (that the programmer would think it formats the previously appended + * sub-component). If the formatting of the currently builded component is needed, since {@link Chat} is a sealed + * class which only subclass is {@link FormatableChat}, you can cast the builder, and use the format methods again. + *
{@code
+     * Chat component = Chat.text("Hello ").red()
+     *         .then(Chat.text("world").darkRed().bold())
+     *         .thenText("!");
+     * // ok now I want to underline everything:
+     * ((FormatableChat)component).underlined(); // this will not format only the last appended text.
+     * }
+ */ + public static final class FormatableChat extends Chat { + /* package */ FormatableChat(ComponentBuilder c) { + super(c); + } + + + /** + * Configure if this component will be rendered on console or not. + * @param c true for console, false for game UI. + * @return this. + */ + public FormatableChat console(boolean c) { console = c; return this; } + /** + * Configure the width of the line. + * @param w the width to consider when rendering the line. In pixel for game UI rendering, n character for + * console rendering. + * @return this. + */ + public FormatableChat maxWidth(int w) { maxWidth = w; return this; } + + /** + * Sets the color of this component. + * @param c the color. + * @return this. + */ + public FormatableChat color(TextColor c) { builder.color(c); return this; } + /** + * Sets the color of this component. + * @param c the color. + * @return this. + */ + public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); } + /** + * Sets the color of this component. + * @param c the color. + * @return this. + */ + public FormatableChat color(Color c) { return color(c == null ? null : TextColor.color(c.getRGB())); } + /** + * Sets the color of this component. + * @param c the color. + * @return this. + */ + public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); } + + + /** + * Sets the color of this component to {@link NamedTextColor#BLACK}. + * @return this. + */ + public FormatableChat black() { return color(NamedTextColor.BLACK); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_BLUE}. + * @return this. + */ + public FormatableChat darkBlue() { return color(NamedTextColor.DARK_BLUE); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_GREEN}. + * @return this. + */ + public FormatableChat darkGreen() { return color(NamedTextColor.DARK_GREEN); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_AQUA}. + * @return this. + */ + public FormatableChat darkAqua() { return color(NamedTextColor.DARK_AQUA); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_RED}. + * @return this. + */ + public FormatableChat darkRed() { return color(NamedTextColor.DARK_RED); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_PURPLE}. + * @return this. + */ + public FormatableChat darkPurple() { return color(NamedTextColor.DARK_PURPLE); } + /** + * Sets the color of this component to {@link NamedTextColor#GOLD}. + * @return this. + */ + public FormatableChat gold() { return color(NamedTextColor.GOLD); } + /** + * Sets the color of this component to {@link NamedTextColor#GRAY}. + * @return this. + */ + public FormatableChat gray() { return color(NamedTextColor.GRAY); } + /** + * Sets the color of this component to {@link NamedTextColor#DARK_GRAY}. + * @return this. + */ + public FormatableChat darkGray() { return color(NamedTextColor.DARK_GRAY); } + /** + * Sets the color of this component to {@link NamedTextColor#BLUE}. + * @return this. + */ + public FormatableChat blue() { return color(NamedTextColor.BLUE); } + /** + * Sets the color of this component to {@link NamedTextColor#GREEN}. + * @return this. + */ + public FormatableChat green() { return color(NamedTextColor.GREEN); } + /** + * Sets the color of this component to {@link NamedTextColor#AQUA}. + * @return this. + */ + public FormatableChat aqua() { return color(NamedTextColor.AQUA); } + /** + * Sets the color of this component to {@link NamedTextColor#RED}. + * @return this. + */ + public FormatableChat red() { return color(NamedTextColor.RED); } + /** + * Sets the color of this component to {@link NamedTextColor#LIGHT_PURPLE}. + * @return this. + */ + public FormatableChat lightPurple() { return color(NamedTextColor.LIGHT_PURPLE); } + /** + * Sets the color of this component to {@link NamedTextColor#YELLOW}. + * @return this. + */ + public FormatableChat yellow() { return color(NamedTextColor.YELLOW); } + /** + * Sets the color of this component to {@link NamedTextColor#WHITE}. + * @return this. + */ + public FormatableChat white() { return color(NamedTextColor.WHITE); } + + + /** + * Sets the color of this component to {@link ChatConfig#successColor}. + * @return this. + */ + public FormatableChat successColor() { return color(ChatConfig.successColor); } + /** + * Sets the color of this component to {@link ChatConfig#failureColor}. + * @return this. + */ + public FormatableChat failureColor() { return color(ChatConfig.failureColor); } + /** + * Sets the color of this component to {@link ChatConfig#infoColor}. + * @return this. + */ + public FormatableChat infoColor() { return color(ChatConfig.infoColor); } + /** + * Sets the color of this component to {@link ChatConfig#warningColor}. + * @return this. + */ + public FormatableChat warningColor() { return color(ChatConfig.warningColor); } + /** + * Sets the color of this component to {@link ChatConfig#dataColor}. + * @return this. + */ + public FormatableChat dataColor() { return color(ChatConfig.dataColor); } + /** + * Sets the color of this component to {@link ChatConfig#decorationColor}. + * @return this. + */ + public FormatableChat decorationColor() { return color(ChatConfig.decorationColor); } + /** + * Sets the color of this component to {@link ChatConfig#urlColor}. + * @return this. + */ + public FormatableChat urlColor() { return color(ChatConfig.urlColor); } + /** + * Sets the color of this component to {@link ChatConfig#commandColor}. + * @return this. + */ + public FormatableChat commandColor() { return color(ChatConfig.commandColor); } + /** + * Sets the color of this component to {@link ChatConfig#highlightedCommandColor}. + * @return this. + */ + public FormatableChat highlightedCommandColor() { return color(ChatConfig.highlightedCommandColor); } + /** + * Sets the color of this component to {@link ChatConfig#broadcastColor}. + * @return this. + */ + public FormatableChat broadcastColor() { return color(ChatConfig.broadcastColor); } + + + private FormatableChat setStyle(Consumer styleOp) { builder.style(styleOp); return this; } + private FormatableChat setDecoration(TextDecoration deco, Boolean state) { + return setStyle(b -> b.decoration(deco, State.byBoolean(state))); + } + + + /** + * Sets the bold status of this component. + * @param b true to enable, false to disable, or null to inherit from parent. + * @return this. + */ + public FormatableChat bold(Boolean b) { return setDecoration(TextDecoration.BOLD, b); } + /** + * Enables the bold status of this component. + * @return this. + */ + public FormatableChat bold() { return bold(true); } + /** + * Sets the italic status of this component. + * @param i true to enable, false to disable, or null to inherit from parent. + * @return this. + */ + public FormatableChat italic(Boolean i) { return setDecoration(TextDecoration.ITALIC, i); } + /** + * Enables the italic status of this component. + * @return this. + */ + public FormatableChat italic() { return italic(true); } + /** + * Sets the underlined status of this component. + * @param u true to enable, false to disable, or null to inherit from parent. + * @return this. + */ + public FormatableChat underlined(Boolean u) { return setDecoration(TextDecoration.UNDERLINED, u); } + /** + * Enables the underlined status of this component. + * @return this. + */ + public FormatableChat underlined() { return underlined(true); } + /** + * Sets the strikethrough status of this component. + * @param s true to enable, false to disable, or null to inherit from parent. + * @return this. + */ + public FormatableChat strikethrough(Boolean s) { return setDecoration(TextDecoration.STRIKETHROUGH, s); } + /** + * Enables the strikethrough status of this component. + * @return this. + */ + public FormatableChat strikethrough() { return strikethrough(true); } + /** + * Sets the obfuscated status of this component. + * @param o true to enable, false to disable, or null to inherit from parent. + * @return this. + */ + public FormatableChat obfuscated(Boolean o) { return setDecoration(TextDecoration.OBFUSCATED, o); } + /** + * Enables the obfuscated status of this component. + * @return this. + */ + public FormatableChat obfuscated() { return obfuscated(true); } + + + /** + * Sets the font of this component. + * @param f the font namespaced key. + * @return this. + */ + public FormatableChat font(Key f) { return setStyle(s -> s.font(f)); } + + + /** + * Configure this component to insert the specified text at the cursor position when clicked. + * @param i the text to insert. + * @return this. + */ + public FormatableChat shiftClickInsertion(String i) { builder.insertion(i); return this; } + + + /** + * Configure this component’s click event. + * @param e the {@link ClickEvent}. + * @return this. + */ + private FormatableChat click(ClickEvent e) { builder.clickEvent(e); return this; } + /** + * Configure this component to execute the specified command when clicked. + * @param cmdWithSlash the command to execute. + * @return this. + */ + public FormatableChat clickCommand(String cmdWithSlash) { return click(ClickEvent.runCommand(cmdWithSlash)); } + /** + * Configure this component to insert in the chat-box the specified command when clicked. + * @param cmdWithSlash the command to suggest. + * @return this. + */ + public FormatableChat clickSuggest(String cmdWithSlash) { return click(ClickEvent.suggestCommand(cmdWithSlash)); } + /** + * Configure this component to copy into clipboard the specified text when clicked. + * @param value the text to copy. + * @return this. + */ + public FormatableChat clickClipboard(String value) { return click(ClickEvent.copyToClipboard(value)); } + /** + * Configure this component to open the specified URL when clicked. + * @param url the URL to open. + * @return this. + */ + public FormatableChat clickURL(String url) { return click(ClickEvent.openUrl(url)); } + /** + * Configure this component to change the page of the opened book when clicked. + * @param page the page to go to. + * @return this. + */ + public FormatableChat clickBookPage(int page) { return click(ClickEvent.changePage(page)); } + + + /** + * Configure this component’s hover event. + * @param e the {@link HoverEventSource}. + * @return this. + */ + public FormatableChat hover(HoverEventSource e) { builder.hoverEvent(e); return this; } + /** + * Configure this component to show the provided component when hovered. + * @param v the component to show. + * @return this. + */ + public FormatableChat hover(Component v) { return hover((HoverEventSource) v); } + /** + * Configure this component to show the provided component when hovered. + * @param v the component to show. + * @return this. + */ + public FormatableChat hover(Chat v) { return hover((HoverEventSource) v); } + /** + * Configure this component to show the provided component when hovered. + * @param v the component to show. + * @return this. + */ + public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); } + /** + * Configure this component to show the provided component when hovered. + * @param v the component to show. + * @return this. + */ + public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); } + /** + * Configure this component to show the provided component when hovered. + * @param v the component to show. + * @return this. + */ + public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); } + /** + * Configure this component to show the provided legacy text when hovered. + * @param legacyText the legacy text to show. + * @return this. + */ + public FormatableChat hover(String legacyText) { return hover(legacyText(legacyText)); } + + } + + + + + + + + + + + + @Override + public boolean equals(Object obj) { + return obj instanceof Chat c + && builder.equals(c.builder); + } + + @Override + public int hashCode() { + return getAdv().hashCode(); + } + + @Override + public String toString() { + return getPlainText(); + } + + + + + + /* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) { + if (values == null) + return null; + ComponentLike[] ret = new ComponentLike[values.length]; + for (int i = 0; i < values.length; i++) { + Object v = values[i]; + if (v instanceof BaseComponent[]) + ret[i] = toAdventure((BaseComponent[]) v); + else if (v instanceof BaseComponent) + ret[i] = toAdventure((BaseComponent) v); + else if (v instanceof ComponentLike) + ret[i] = (ComponentLike) v; + else + ret[i] = Component.text(Objects.toString(v)); + } + return ret; + } + + + /** + * Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}. + * @param components the Bungee {@link BaseComponent} array. + * @return a {@link Component}. + */ + public static Component toAdventure(BaseComponent[] components) { + return BungeeComponentSerializer.get().deserialize(components); + } + /** + * Converts the Bungee {@link BaseComponent} into Adventure {@link Component}. + * @param component the Bungee {@link BaseComponent}. + * @return a {@link Component}. + */ + public static Component toAdventure(BaseComponent component) { + return toAdventure(new BaseComponent[] { component }); + } + + /** + * Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array. + * @param component the Adventure {@link Component}. + * @return a {@link BaseComponent} array. + */ + public static BaseComponent[] toBungeeArray(Component component) { + return BungeeComponentSerializer.get().serialize(component); + } + /** + * Converts the Adventure {@link Component} into Bungee {@link BaseComponent}. + * @param component the Adventure {@link Component}. + * @return a {@link BaseComponent}. + */ + public static BaseComponent toBungee(Component component) { + BaseComponent[] arr = toBungeeArray(component); + return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr); + } + + /** + * Force the italic formating to be set to false if it is not explicitely set in the component. + * This is useful for item lores that defaults to italic in the game UI. + * @param c the {@link Chat} in which to set the italic property if needed. + * @return the provided {@link Chat} instance. + */ + public static Chat italicFalseIfNotSet(Chat c) { + c.builder.style(b -> { + if (b.build().decoration(TextDecoration.ITALIC) == State.NOT_SET) { + ((FormatableChat) c).italic(false); + } + }); + return c; + } + + } diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorGradient.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorGradient.java new file mode 100644 index 0000000..933f74c --- /dev/null +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorGradient.java @@ -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 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); + } +} diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorUtil.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorUtil.java index ab75e2e..3d00a9f 100644 --- a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorUtil.java +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatColorUtil.java @@ -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. - *

- * 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)}). - *

- * 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. + *

+ * 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)}). + *

+ * 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 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); + } + + } \ No newline at end of file diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatConfig.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatConfig.java new file mode 100644 index 0000000..75e3668 --- /dev/null +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatConfig.java @@ -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 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); + } +} diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatFilledLine.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatFilledLine.java new file mode 100644 index 0000000..39527fc --- /dev/null +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatFilledLine.java @@ -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 + } +} diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatStatic.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatStatic.java index f74883e..92b0107 100644 --- a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatStatic.java +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatStatic.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * The text on which to click will be the URL itself. To configure the clicked text, use + * {@link #clickableURL(ComponentLike, String)}. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #clickableCommand(ComponentLike, String)}. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * The text on which to click will be the command itself. To configure the clicked text, use + * {@link #clickableSuggest(ComponentLike, String)}. + *

+ * 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()); + } + } diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatTreeNode.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatTreeNode.java new file mode 100644 index 0000000..54f49cc --- /dev/null +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatTreeNode.java @@ -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 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. + *

+ * 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 render(boolean console) { + List ret = new ArrayList<>(); + + ret.add(ChatStatic.chat() + .then(component)); + + for (int i = 0; i < children.size(); i++) { + List 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; + } +} diff --git a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatUtil.java b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatUtil.java index feb0be8..2a25c19 100644 --- a/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatUtil.java +++ b/pandalib-chat/src/main/java/fr/pandacube/lib/chat/ChatUtil.java @@ -26,757 +26,581 @@ import fr.pandacube.lib.chat.Chat.FormatableChat; */ public class ChatUtil { - /* - * Note : this field is for easy listing of all characters with special sizes. It will all be reported to - * #CHAR_SIZES on class initialization for optimization. - */ - private static final Map SIZE_CHARS_MAPPING = Map.ofEntries( - Map.entry(-6, "§"), - Map.entry(2, "!.,:;i|¡'"), - Map.entry(3, "`lìí’‘"), - Map.entry(4, " I[]tï×"), - Map.entry(5, "\"()*<>fk{}"), - Map.entry(7, "@~®©«»"), - Map.entry(9, "├└") - ); + /* + * Note : this field is for easy listing of all characters with special sizes. It will all be reported to + * #CHAR_SIZES on class initialization for optimization. + */ + private static final Map SIZE_CHARS_MAPPING = Map.ofEntries( + Map.entry(-6, "§"), + Map.entry(2, "!.,:;i|¡'"), + Map.entry(3, "`lìí’‘"), + Map.entry(4, " I[]tï×"), + Map.entry(5, "\"()*<>fk{}"), + Map.entry(7, "@~®©«»"), + Map.entry(9, "├└") + ); - /** - * The default text pixel width for a character in the default Minecraft font. - * If a character has another width, it should be found in {@link #CHAR_SIZES}. - */ - public static final int DEFAULT_CHAR_SIZE = 6; + /** + * The default text pixel width for a character in the default Minecraft font. + * If a character has another width, it should be found in {@link #CHAR_SIZES}. + */ + public static final int DEFAULT_CHAR_SIZE = 6; - /** - * Mapping indicating the text pixel with for specific characters in the default Minecraft font. - * If a character doesn’t have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}. - */ - public static final Map CHAR_SIZES; - static { - Map charSizes = new HashMap<>(); - for (var e : SIZE_CHARS_MAPPING.entrySet()) { - int size = e.getKey(); - for (char c : e.getValue().toCharArray()) { - charSizes.put(c, size); - } - } - CHAR_SIZES = Collections.unmodifiableMap(charSizes); - } + /** + * Mapping indicating the text pixel with for specific characters in the default Minecraft font. + * If a character doesn’t have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}. + */ + public static final Map CHAR_SIZES; + static { + Map charSizes = new HashMap<>(); + for (var e : SIZE_CHARS_MAPPING.entrySet()) { + int size = e.getKey(); + for (char c : e.getValue().toCharArray()) { + charSizes.put(c, size); + } + } + CHAR_SIZES = Collections.unmodifiableMap(charSizes); + } - /** - * The default width of the Minecraft Java Edition chat window, in text pixels. - */ - public static final int DEFAULT_CHAT_WIDTH = 320; + /** + * The default width of the Minecraft Java Edition chat window, in text pixels. + */ + public static final int DEFAULT_CHAT_WIDTH = 320; - /** - * The width of a Minecraft sign, in text pixels. - */ - public static final int SIGN_WIDTH = 90; + /** + * The width of a Minecraft sign, in text pixels. + */ + public static final int SIGN_WIDTH = 90; - /** - * The width of a Minecraft book content, in text pixels. - */ - public static final int BOOK_WIDTH = 116; + /** + * The width of a Minecraft book content, in text pixels. + */ + public static final int BOOK_WIDTH = 116; - /** - * The width of a Minecraft server MOTD message, in text pixels. - */ - public static final int MOTD_WIDTH = 270; + /** + * The width of a Minecraft server MOTD message, in text pixels. + */ + public static final int MOTD_WIDTH = 270; - /** - * The width of a Minecraft Bedrock Edition form button, in text pixels. - */ - public static final int BEDROCK_FORM_WIDE_BUTTON = 178; + /** + * The width of a Minecraft Bedrock Edition form button, in text pixels. + */ + public static final int BEDROCK_FORM_WIDE_BUTTON = 178; - /** - * The default number of character per lines for the console. - */ - public static final int CONSOLE_NB_CHAR_DEFAULT = 50; + /** + * The default number of character per lines for the console. + */ + public static final int CONSOLE_NB_CHAR_DEFAULT = 50; - /** - * Create a {@link Chat} that is a cliquable URL link. - * It is equivalent to the HTML {@code } tag pointing to another page. - * @param text the link text. - * @param url the destination url. must starts with {@code http} or {@code https}. - * @return a {@link Chat} that is a cliquable URL link. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableURL(Chat, String)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createURLLink(String text, String url) { - return Chat.clickableURL(text == null ? null : Chat.legacyText(text), url); - } - - /** - * Create a {@link Chat} that is a cliquable URL link. - * It is equivalent to the HTML {@code } tag pointing to another page. - * @param text the link text. - * @param url the destination url. must starts with {@code http} or {@code https}. - * @param hoverText the text displayed when hovering the link. - * @return a {@link Chat} that is a cliquable URL link. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableURL(Chat, String, Chat)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createURLLink(String text, String url, String hoverText) { - return Chat.clickableURL(text == null ? null : Chat.legacyText(text), url, hoverText == null ? null : Chat.legacyText(hoverText)); - } - - /** - * Create a {@link Chat} that is a cliquable command link. - * When the players clicks on it, they will execute the command. - * @param text the link text. - * @param commandWithSlash the command to execute when clicked. - * @param hoverText the text displayed when hovering the link. - * @return a {@link Chat} that is a cliquable command link. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableCommand(Chat, String, Chat)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createCommandLink(String text, String commandWithSlash, String hoverText) { - return Chat.clickableCommand(text == null ? null : Chat.legacyText(text), commandWithSlash, hoverText == null ? null : Chat.legacyText(hoverText)); - } - - /** - * Create a {@link Chat} that is a cliquable command link. - * When the players clicks on it, they will execute the command. - * @param text the link text. - * @param commandWithSlash the command to execute when clicked. - * @param hoverText the text displayed when hovering the link. - * @return a {@link Chat} that is a cliquable command link. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableCommand(Chat, String, Chat)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createCommandLink(String text, String commandWithSlash, Chat hoverText) { - return Chat.clickableCommand(text == null ? null : Chat.legacyText(text), commandWithSlash, hoverText); - } - - /** - * Create a {@link Chat} that is a cliquable command suggestion. - * When the players clicks on it, they will execute the command. - * @param inner the link text. - * @param commandWithSlash the command to put in the chat box when clicked. - * @param hover the text displayed when hovering the link. - * @return a {@link Chat} that is a cliquable command suggestion. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableSuggest(Chat, String, Chat)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createCommandSuggest(String inner, String commandWithSlash, String hover) { - return Chat.clickableSuggest(inner == null ? null : Chat.legacyText(inner), commandWithSlash, hover == null ? null : Chat.legacyText(hover)); - } - - /** - * Create a {@link Chat} that is a cliquable command suggestion. - * When the players clicks on it, they will execute the command. - * @param inner the link text. - * @param commandWithSlash the command to put in the chat box when clicked. - * @param hover the text displayed when hovering the link. - * @return a {@link Chat} that is a cliquable command suggestion. - * @deprecated it uses String for displayed text. Use {@link Chat#clickableSuggest(Chat, String, Chat)} instead. - */ - @Deprecated(forRemoval = true, since = "2022-07-27") - public static FormatableChat createCommandSuggest(String inner, String commandWithSlash, Chat hover) { - return Chat.clickableSuggest(inner == null ? null : Chat.legacyText(inner), commandWithSlash, hover); - } - - - - - - - /** - * Create a page navigator with clickable page numbers for the chat. - * @param prefix the text to put before the - * @param cmdFormat the command with %d inside to be replaced with the page number (must start with slash) - * @param currentPage the current page number (it is highlighted, and the pages around are displayed, according to - * {@code nbPagesToDisplay}). - * @param nbPages the number of pages. - * @param nbPagesToDisplay the number of pages to display around the first page, the last page and the - * {@code currentPage}. - * @return a {@link Chat} containging the created page navigator. - */ - public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) { - Set pagesToDisplay = new TreeSet<>(); - - for (int i = 0; i < nbPagesToDisplay && i < nbPages && nbPages - i > 0; i++) { - pagesToDisplay.add(i + 1); - pagesToDisplay.add(nbPages - i); - } - - for (int i = currentPage - nbPagesToDisplay + 1; i < currentPage + nbPagesToDisplay; i++) { - if (i > 0 && i <= nbPages) - pagesToDisplay.add(i); - } - - Chat d = ChatStatic.chat().thenLegacyText(prefix); - boolean first = true; - int previous = 0; - - for (int page : pagesToDisplay) { - if (!first) { - if (page == previous + 1) { - d.thenText(" "); - } - else { - if (cmdFormat.endsWith("%d")) { - d.thenText(" "); - d.then(Chat.clickableSuggest(Chat.text("..."), cmdFormat.substring(0, cmdFormat.length() - 2), Chat.text("Choisir la page"))); - d.thenText(" "); - } - else - d.thenText(" ... "); - } - } - else - first = false; - - FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); - if (page == currentPage) { - pDisp.highlightedCommandColor(); - } - d.then(pDisp); - - previous = page; - } - - - return d; - } - - - - - - - - - /** - * @param decorationColor support null values - */ - public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean console) { - return centerText(text, repeatedChar, decorationColor, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH); - } - public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean console, int maxWidth) { - return centerText(text, repeatedChar, decorationColor, false, console, maxWidth); - } - - /** - * @param decorationColor support null values - */ - public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console, int maxWidth) { - - int textWidth = componentWidth(text.getAdv(), console); - - if (textWidth > maxWidth) - return text; - - int repeatedCharWidth = charW(repeatedChar, console, decorationBold); - int sideWidth = (maxWidth - textWidth) / 2; - int sideNbChar = sideWidth / repeatedCharWidth; - - if (sideNbChar == 0) - return text; - - String sideChars = repeatedChar(repeatedChar, sideNbChar); - FormatableChat side = ChatStatic.text(sideChars).color(decorationColor); - if (decorationBold) - side.bold(); - - Chat d = Chat.chat() - .then(side) - .then(text); - if (repeatedChar != ' ') - d.then(side); - - return d; - - } - - public static Chat leftText(Chat text, char repeatedChar, TextColor decorationColor, int nbLeft, boolean console) { - return leftText(text, repeatedChar, decorationColor, nbLeft, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH); - } - - public static Chat leftText(Chat text, char repeatedChar, TextColor decorationColor, int nbLeft, boolean console, int maxWidth) { - - int textWidth = componentWidth(text.getAdv(), console); - int repeatedCharWidth = charW(repeatedChar, console, false); - int leftWidth = nbLeft * repeatedCharWidth; - - if (textWidth + leftWidth > maxWidth) - return text; - - int rightNbChar = (maxWidth - (textWidth + leftWidth)) / repeatedCharWidth; - - Chat d = ChatStatic.chat() - .then(ChatStatic.text(repeatedChar(repeatedChar, nbLeft)).color(decorationColor)) - .then(text); - if (repeatedChar != ' ') { - d.then(ChatStatic.text(repeatedChar(repeatedChar, rightNbChar)).color(decorationColor)); - } - return d; - - } - - public static Chat rightText(Chat text, char repeatedChar, TextColor decorationColor, int nbRight, boolean console) { - return rightText(text, repeatedChar, decorationColor, nbRight, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH); - } - - public static Chat rightText(Chat text, char repeatedChar, TextColor decorationColor, int nbRight, - boolean console, int maxWidth) { - - int textWidth = componentWidth(text.getAdv(), console); - int repeatedCharWidth = charW(repeatedChar, console, false); - int rightWidth = nbRight * repeatedCharWidth; - - if (textWidth + rightWidth > maxWidth) - return text; - - int leftNbChar = (maxWidth - (textWidth + rightWidth)) / repeatedCharWidth; - - Chat d = ChatStatic.chat() - .then(ChatStatic.text(repeatedChar(repeatedChar, leftNbChar)).color(decorationColor)) - .then(text); - if (repeatedChar != ' ') { - d.then(ChatStatic.text(repeatedChar(repeatedChar, nbRight)).color(decorationColor)); - } - return d; - - } - - public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean console) { - return emptyLine(repeatedChar, decorationColor, false, console); - } - - public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console) { - return emptyLine(repeatedChar, decorationColor, decorationBold, console, (console) ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH); - } - - public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console, int maxWidth) { - int count = maxWidth / charW(repeatedChar, console, decorationBold); - FormatableChat line = ChatStatic.text(repeatedChar(repeatedChar, count)).color(decorationColor); - if (decorationBold) - line.bold(); - return line; - } - - private static String repeatedChar(char repeatedChar, int count) { - char[] c = new char[count]; - Arrays.fill(c, repeatedChar); - return new String(c); - } - - - - - - - - public static int componentWidth(Component component, boolean console) { - return componentWidth(component, console, false); - } - - public static int componentWidth(Component component, boolean console, boolean parentBold) { - if (component == null) - return 0; - - int count = 0; - - State currentBold = component.style().decoration(TextDecoration.BOLD); - boolean actuallyBold = childBold(parentBold, currentBold); - - if (component instanceof TextComponent) { - count += strWidth(((TextComponent)component).content(), console, actuallyBold); - } - else if (component instanceof TranslatableComponent) { - for (Component c : ((TranslatableComponent)component).args()) - count += componentWidth(c, console, actuallyBold); - } - for (Component c : component.children()) - count += componentWidth(c, console, actuallyBold); + /** + * Create a page navigator with clickable page numbers for the chat. + * @param prefix the text to put before the + * @param cmdFormat the command with %d inside to be replaced with the page number (must start with slash) + * @param currentPage the current page number (it is highlighted, and the pages around are displayed, according to + * {@code nbPagesToDisplay}). + * @param nbPages the number of pages. + * @param nbPagesToDisplay the number of pages to display around the first page, the last page and the + * {@code currentPage}. + * @return a {@link Chat} containging the created page navigator. + */ + public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) { + Set pagesToDisplay = new TreeSet<>(); - return count; - } - - private static boolean childBold(boolean parent, TextDecoration.State child) { - return (parent && child != State.FALSE) || (!parent && child == State.TRUE); - } + for (int i = 0; i < nbPagesToDisplay && i < nbPages && nbPages - i > 0; i++) { + pagesToDisplay.add(i + 1); + pagesToDisplay.add(nbPages - i); + } - public static int strWidth(String str, boolean console, boolean bold) { - int count = 0; - for (char c : str.toCharArray()) - count += charW(c, console, bold); - return Math.max(count, 0); - } + for (int i = currentPage - nbPagesToDisplay + 1; i < currentPage + nbPagesToDisplay; i++) { + if (i > 0 && i <= nbPages) + pagesToDisplay.add(i); + } - public static int charW(char c, boolean console, boolean bold) { - if (console) - return (c == '§') ? -1 : 1; - return CHAR_SIZES.getOrDefault(c, DEFAULT_CHAR_SIZE) + (bold ? 1 : 0); - } - - - - - - - - - public static List wrapInLimitedPixelsToChat(String legacyText, int pixelWidth) { - return wrapInLimitedPixels(legacyText, pixelWidth).stream() - .map(ChatStatic::legacyText) - .collect(Collectors.toList()); - } - - public static List wrapInLimitedPixels(String legacyText, int pixelWidth) { - List lines = new ArrayList<>(); - - legacyText += "\n"; // workaround to force algorithm to compute last lines; - - String currentLine = ""; - int currentLineSize = 0; - int index = 0; - - StringBuilder currentWord = new StringBuilder(); - int currentWordSize = 0; - boolean bold = false; - boolean firstCharCurrentWordBold = false; - - do { - char c = legacyText.charAt(index); - if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) { - currentWord.append(c); - c = legacyText.charAt(++index); - currentWord.append(c); - - if (c == 'l' || c == 'L') // bold - bold = true; - if ((c >= '0' && c <= '9') // reset bold - || (c >= 'a' && c <= 'f') - || (c >= 'A' && c <= 'F') - || c == 'r' || c == 'R' - || c == 'x' || c == 'X') - bold = false; - - } - else if (c == ' ') { - if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word - lines.add(currentLine); - String lastStyle = ChatColorUtil.getLastColors(currentLine); - if (currentWord.charAt(0) == ' ') { - currentWord = new StringBuilder(currentWord.substring(1)); - currentWordSize -= charW(' ', false, firstCharCurrentWordBold); - } - currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord; - currentLineSize = currentWordSize; - } - else { - currentLine += currentWord; - currentLineSize += currentWordSize; - } - currentWord = new StringBuilder("" + c); - currentWordSize = charW(c, false, bold); - firstCharCurrentWordBold = bold; - } - else if (c == '\n') { - if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word - lines.add(currentLine); - String lastStyle = ChatColorUtil.getLastColors(currentLine); - if (currentWord.charAt(0) == ' ') { - currentWord = new StringBuilder(currentWord.substring(1)); - } - currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord; - } - else { - currentLine += currentWord; - } - // wrap after - lines.add(currentLine); - String lastStyle = ChatColorUtil.getLastColors(currentLine); - - currentLine = lastStyle.equals("§r") ? "" : lastStyle; - currentLineSize = 0; - currentWord = new StringBuilder(); - currentWordSize = 0; - firstCharCurrentWordBold = bold; - } - else { - currentWord.append(c); - currentWordSize += charW(c, false, bold); - } - - } while(++index < legacyText.length()); - - - - - - - return lines; - } - - - - + Chat d = ChatStatic.chat().thenLegacyText(prefix); + boolean first = true; + int previous = 0; - public static List renderTable(List> rows, String space, boolean console) { - List> compRows = new ArrayList<>(rows.size()); - for (List row : rows) { - List compRow = new ArrayList<>(row.size()); - for (Chat c : row) { - compRow.add(c.getAdv()); - } - compRows.add(compRow); - } - return renderTableComp(compRows, space, console); - } - - - public static List renderTableComp(List> rows, String space, boolean console) { - // determine columns width - List nbPixelPerColumn = new ArrayList<>(); - for (List row : rows) { - for (int i = 0; i < row.size(); i++) { - int w = componentWidth(row.get(i), console); - if (nbPixelPerColumn.size() <= i) - nbPixelPerColumn.add(w); - else if (nbPixelPerColumn.get(i) < w) - nbPixelPerColumn.set(i, w); - } - } - - // create the lines with appropriate spacing - List spacedRows = new ArrayList<>(rows.size()); - for (List row : rows) { - Chat spacedRow = Chat.chat(); - for (int i = 0; i < row.size() - 1; i++) { - int w = componentWidth(row.get(i), console); - int padding = nbPixelPerColumn.get(i) - w; - spacedRow.then(row.get(i)); - spacedRow.then(customWidthSpace(padding, console)); - spacedRow.thenText(space); - } - if (!row.isEmpty()) - spacedRow.then(row.get(row.size() - 1)); - spacedRows.add(spacedRow.getAdv()); - } - - return spacedRows; - } - - - - - public static Component customWidthSpace(int width, boolean console) { - if (console) - return Chat.text(" ".repeat(width)).getAdv(); - return switch (width) { - case 0, 1 -> Component.empty(); - case 2 -> Chat.text(".").black().getAdv(); - case 3 -> Chat.text("`").black().getAdv(); - case 6 -> Chat.text(". ").black().getAdv(); - case 7 -> Chat.text("` ").black().getAdv(); - case 11 -> Chat.text("` ").black().getAdv(); - default -> { - int nbSpace = width / 4; - int nbBold = width % 4; - int nbNotBold = nbSpace - nbBold; - if (nbNotBold > 0) { - if (nbBold > 0) { - yield Chat.text(" ".repeat(nbNotBold)).bold(false) - .then(Chat.text(" ".repeat(nbBold)).bold(true)) - .getAdv(); - } - else - yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv(); - } - else if (nbBold > 0) { - yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv(); - } - throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")"); - } - }; - // "." is 2 px - // "`" is 3 px - // " " is 4 px - // 0 "" - // 1 "" - // 2 "." - // 3 "`" - // 4 " " - // 5 "§l " - // 6 ". " - // 7 "` " - // 8 " " - // 9 " §l " - // 10 "§l " - // 11 "` " - // 12 " " - } - - - - - + for (int page : pagesToDisplay) { + if (!first) { + if (page == previous + 1) { + d.thenText(" "); + } + else { + if (cmdFormat.endsWith("%d")) { + d.thenText(" "); + d.thenCommandSuggest(Chat.text("..."), cmdFormat.substring(0, cmdFormat.length() - 2), Chat.text("Choisir la page")); + d.thenText(" "); + } + else + d.thenText(" ... "); + } + } + else + first = false; - private static final String PROGRESS_BAR_START = "["; - private static final String PROGRESS_BAR_END = "]"; - private static final TextColor PROGRESS_BAR_EMPTY_COLOR = NamedTextColor.DARK_GRAY; - private static final char PROGRESS_BAR_EMPTY_CHAR = '.'; - private static final char PROGRESS_BAR_FULL_CHAR = '|'; - - public static Chat progressBar(double[] values, TextColor[] colors, double total, int pixelWidth, boolean console) { - - // 1. Compute char size for each values - int progressPixelWidth = pixelWidth - strWidth(PROGRESS_BAR_START + PROGRESS_BAR_END, console, false); - int charPixelWidth = charW(PROGRESS_BAR_EMPTY_CHAR, console, false); - - assert charPixelWidth == charW(PROGRESS_BAR_FULL_CHAR, console, false) : "PROGRESS_BAR_EMPTY_CHAR and PROGRESS_BAR_FULL_CHAR should have the same pixel width according to #charW(...)"; - - int progressCharWidth = progressPixelWidth / charPixelWidth; - - int[] sizes = new int[values.length]; - double sumValuesBefore = 0; - int sumSizesBefore = 0; + FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); + if (page == currentPage) { + pDisp.highlightedCommandColor(); + } + d.then(pDisp); - for (int i = 0; i < values.length; i++) { - sumValuesBefore += values[i]; - int charPosition = Math.min((int) Math.round(progressCharWidth * sumValuesBefore / total), progressCharWidth); - sizes[i] = charPosition - sumSizesBefore; - sumSizesBefore += sizes[i]; - } - - // 2. Generate rendered text - Chat c = ChatStatic.text(PROGRESS_BAR_START); - - int sumSizes = 0; - for (int i = 0; i < sizes.length; i++) { - sumSizes += sizes[i]; + previous = page; + } - FormatableChat subC = ChatStatic.text(repeatedChar(PROGRESS_BAR_FULL_CHAR, sizes[i])); - if (colors != null && i < colors.length && colors[i] != null) - subC.color(colors[i]); - - c.then(subC); - } - - return c - .then(ChatStatic.text(repeatedChar(PROGRESS_BAR_EMPTY_CHAR, progressCharWidth - sumSizes)) - .color(PROGRESS_BAR_EMPTY_COLOR)) - .thenText(PROGRESS_BAR_END); - } - - public static Chat progressBar(double value, TextColor color, double total, int pixelWidth, boolean console) { - return progressBar(new double[] { value }, new TextColor[] { color }, total, pixelWidth, console); - } - - - - - - - - - + return d; + } - - - - - public static String truncatePrefix(String prefix, int maxLength) { - if (prefix.length() > maxLength) { - String lastColor = ChatColorUtil.getLastColors(prefix); - prefix = truncateAtLengthWithoutReset(prefix, maxLength); - if (!ChatColorUtil.getLastColors(prefix).equals(lastColor)) - prefix = truncateAtLengthWithoutReset(prefix, maxLength - lastColor.length()) + lastColor; - } - return prefix; - } - - - public static String truncateAtLengthWithoutReset(String prefix, int l) { - if (prefix.length() > l) { - prefix = prefix.substring(0, l); - if (prefix.endsWith("§")) - prefix = prefix.substring(0, prefix.length()-1); - } - return prefix; - } - - - - - - - - - - - - - - - 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 - - /** - * Generate a tree view based on the tree structure {@code node}. - * - * Each element in the returned list represent 1 line of the tree view. - * Thus, the caller may send each line separately or at once depending of the quantity of data. - * @return A array of component, each element being a single line. - */ - public static List treeView(DisplayTreeNode node, boolean console) { - List ret = new ArrayList<>(); - - ret.add(ChatStatic.chat() - .then(node.component)); - - for (int i = 0; i < node.children.size(); i++) { - List childComponents = treeView(node.children.get(i), console); - boolean last = i == node.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; - } - - - - - - public static class DisplayTreeNode { - public final Chat component; - public final List children = new ArrayList<>(); - - public DisplayTreeNode(Chat cmp) { - component = cmp; - } - - public DisplayTreeNode addChild(DisplayTreeNode child) { - children.add(child); - return this; - } - } + + + + + + + + /* package */ static String repeatedChar(char repeatedChar, int count) { + char[] c = new char[count]; + Arrays.fill(c, repeatedChar); + return new String(c); + } + + + /** + * Compute the width of the provided component. + * @param component the component to compute the width. + * @param console true to compute the width when displayed on console (so it will count the characters), + * false to compute the width when displayed in game (so it will count the pixels). + * @return the width of the provided component. + */ + public static int componentWidth(Component component, boolean console) { + return componentWidth(component, console, false); + } + + /** + * Compute the width of the provided component, with extra information about the parent component. + * @param component the component to compute the width. + * @param console true to compute the width when displayed on console (so it will count the characters), + * false to compute the width when displayed in game (so it will count the pixels). + * @param parentBold if the component inherits a bold styling from an eventual parent component. + * @return the width of the provided component. + */ + public static int componentWidth(Component component, boolean console, boolean parentBold) { + if (component == null) + return 0; + + int count = 0; + + State currentBold = component.style().decoration(TextDecoration.BOLD); + boolean actuallyBold = childBold(parentBold, currentBold); + + if (component instanceof TextComponent) { + count += strWidth(((TextComponent)component).content(), console, actuallyBold); + } + else if (component instanceof TranslatableComponent) { + for (Component c : ((TranslatableComponent)component).args()) + count += componentWidth(c, console, actuallyBold); + } + + for (Component c : component.children()) + count += componentWidth(c, console, actuallyBold); + + return count; + } + + private static boolean childBold(boolean parent, TextDecoration.State child) { + return (parent && child != State.FALSE) || (!parent && child == State.TRUE); + } + + /** + * Compute the width of the provided text. + * @param str the text to compute the width. + * @param console true to compute the width when displayed on console (so it will count the characters), + * false to compute the width when displayed in game (so it will count the pixels). + * @param bold if the text is bold (may change its width). + * @return the width of the provided text. + */ + public static int strWidth(String str, boolean console, boolean bold) { + int count = 0; + for (char c : str.toCharArray()) + count += charW(c, console, bold); + return Math.max(count, 0); + } + + /** + * Compute the width of the provided character. + *

+ * It uses the mapping in {@link #CHAR_SIZES} for in-game display. For console, every character is size 1. + * The {@code §} character is treated has a negative value, to make legacy codes take 0 width. + * @param c the character to compute the width. + * @param console true to compute the width when displayed on console (so it will count the characters), + * false to compute the width when displayed in game (so it will count the pixels). + * @param bold if the character is bold (may change its width). + * @return the width of the provided character. + */ + public static int charW(char c, boolean console, boolean bold) { + if (console) + return (c == '§') ? -1 : 1; + return CHAR_SIZES.getOrDefault(c, DEFAULT_CHAR_SIZE) + (bold ? 1 : 0); + } + + + /** + * Wraps the provided text in multiple lines, taking into account the legacy formating. + *

+ * This method only takes into account IG text width. Use a regular text-wrapper for console instead. + * @param legacyText the text to wrap. + * @param pixelWidth the width in which the text must fit. + * @return the wrapped text in a {@link List} of {@link Chat} components. + */ + public static List wrapInLimitedPixelsToChat(String legacyText, int pixelWidth) { + return wrapInLimitedPixels(legacyText, pixelWidth).stream() + .map(ChatStatic::legacyText) + .collect(Collectors.toList()); + } + + /** + * Wraps the provided text in multiple lines, taking into account the legacy formating. + *

+ * This method only takes into account IG text width. Use a regular text-wrapper for console instead. + * @param legacyText the text to wrap. + * @param pixelWidth the width in which the text must fit. + * @return the wrapped text in a {@link List} of line. + */ + public static List wrapInLimitedPixels(String legacyText, int pixelWidth) { + List lines = new ArrayList<>(); + + legacyText += "\n"; // workaround to force algorithm to compute last lines; + + String currentLine = ""; + int currentLineSize = 0; + int index = 0; + + StringBuilder currentWord = new StringBuilder(); + int currentWordSize = 0; + boolean bold = false; + boolean firstCharCurrentWordBold = false; + + do { + char c = legacyText.charAt(index); + if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) { + currentWord.append(c); + c = legacyText.charAt(++index); + currentWord.append(c); + + if (c == 'l' || c == 'L') // bold + bold = true; + if ((c >= '0' && c <= '9') // reset bold + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') + || c == 'r' || c == 'R' + || c == 'x' || c == 'X') + bold = false; + + } + else if (c == ' ') { + if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word + lines.add(currentLine); + String lastStyle = ChatColorUtil.getLastColors(currentLine); + if (currentWord.charAt(0) == ' ') { + currentWord = new StringBuilder(currentWord.substring(1)); + currentWordSize -= charW(' ', false, firstCharCurrentWordBold); + } + currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord; + currentLineSize = currentWordSize; + } + else { + currentLine += currentWord; + currentLineSize += currentWordSize; + } + currentWord = new StringBuilder("" + c); + currentWordSize = charW(c, false, bold); + firstCharCurrentWordBold = bold; + } + else if (c == '\n') { + if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word + lines.add(currentLine); + String lastStyle = ChatColorUtil.getLastColors(currentLine); + if (currentWord.charAt(0) == ' ') { + currentWord = new StringBuilder(currentWord.substring(1)); + } + currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord; + } + else { + currentLine += currentWord; + } + // wrap after + lines.add(currentLine); + String lastStyle = ChatColorUtil.getLastColors(currentLine); + + currentLine = lastStyle.equals("§r") ? "" : lastStyle; + currentLineSize = 0; + currentWord = new StringBuilder(); + currentWordSize = 0; + firstCharCurrentWordBold = bold; + } + else { + currentWord.append(c); + currentWordSize += charW(c, false, bold); + } + + } while(++index < legacyText.length()); + + + + + + + return lines; + } + + + /** + * Try to render a matrix of {@link Chat} components into a table in the chat or console. + * @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The englobing list holds + * the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the + * leftText one). The row lengths can be different. + * @param space a spacer to put between columns. + * @param console true to display the table on the console (character alignement), false in game chat (pixel + * alignment, much harder). + * @return a List containing each rendered line of the table. + */ + public static List renderTable(List> data, String space, boolean console) { + List> compRows = new ArrayList<>(data.size()); + for (List row : data) { + List compRow = new ArrayList<>(row.size()); + for (Chat c : row) { + compRow.add(c.getAdv()); + } + compRows.add(compRow); + } + return renderTableComp(compRows, space, console); + } + + + /** + * Try to render a matrix of {@link Component} components into a table in the chat or console. + * @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The englobing list holds + * the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the + * leftText one). The row lengths can be different. + * @param space a spacer to put between columns. + * @param console true to display the table on the console (character alignement), false in game chat (pixel + * alignment, much harder). + * @return a List containing each rendered line of the table. + */ + public static List renderTableComp(List> data, String space, boolean console) { + // determine columns width + List nbPixelPerColumn = new ArrayList<>(); + for (List row : data) { + for (int i = 0; i < row.size(); i++) { + int w = componentWidth(row.get(i), console); + if (nbPixelPerColumn.size() <= i) + nbPixelPerColumn.add(w); + else if (nbPixelPerColumn.get(i) < w) + nbPixelPerColumn.set(i, w); + } + } + + // create the lines with appropriate spacing + List spacedRows = new ArrayList<>(data.size()); + for (List row : data) { + Chat spacedRow = Chat.chat(); + for (int i = 0; i < row.size() - 1; i++) { + int w = componentWidth(row.get(i), console); + int padding = nbPixelPerColumn.get(i) - w; + spacedRow.then(row.get(i)); + spacedRow.then(customWidthSpace(padding, console)); + spacedRow.thenText(space); + } + if (!row.isEmpty()) + spacedRow.then(row.get(row.size() - 1)); + spacedRows.add(spacedRow.getAdv()); + } + + return spacedRows; + } + + + /** + * Provides a component acting as a spacer of a specific width. + *

+ * The returned component contains mostly spaces. If it has visible characters, the component color will be set to + * black to be the least visible as possible. + *

+ * For console, the method returns a {@link Component} with a regular space repeated {@code width} times. + * For IG, the methods returns a {@link Component} with a combination of spaces and some small characters, with part + * of them bold. For some specific width, the returned {@link Component} may not have the intended width. + * @param width the width of the space to produce. + * @param console true if the spacer is intended to be displayed on the console, false if it’s in game chat. + * @return a component acting as a spacer of a specific width. + */ + public static Component customWidthSpace(int width, boolean console) { + if (console) + return Chat.text(" ".repeat(width)).getAdv(); + return switch (width) { + case 0, 1 -> Component.empty(); + case 2 -> Chat.text(".").black().getAdv(); + case 3 -> Chat.text("`").black().getAdv(); + case 6 -> Chat.text(". ").black().getAdv(); + case 7 -> Chat.text("` ").black().getAdv(); + case 11 -> Chat.text("` ").black().getAdv(); + default -> { + int nbSpace = width / 4; + int nbBold = width % 4; + int nbNotBold = nbSpace - nbBold; + if (nbNotBold > 0) { + if (nbBold > 0) { + yield Chat.text(" ".repeat(nbNotBold)).bold(false) + .then(Chat.text(" ".repeat(nbBold)).bold(true)) + .getAdv(); + } + else + yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv(); + } + else if (nbBold > 0) { + yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv(); + } + throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")"); + } + }; + // "." is 2 px + // "`" is 3 px + // " " is 4 px + // 0 "" + // 1 "" + // 2 "." + // 3 "`" + // 4 " " + // 5 "§l " + // 6 ". " + // 7 "` " + // 8 " " + // 9 " §l " + // 10 "§l " + // 11 "` " + // 12 " " + } + + + + + + + private static final String PROGRESS_BAR_START = "["; + private static final String PROGRESS_BAR_END = "]"; + private static final TextColor PROGRESS_BAR_EMPTY_COLOR = NamedTextColor.DARK_GRAY; + private static final char PROGRESS_BAR_EMPTY_CHAR = '.'; + private static final char PROGRESS_BAR_FULL_CHAR = '|'; + + /** + * Generate a (eventually multi-part) progress bar using text. + * @param values the values to render in the progress bar. + * @param colors the colors attributed to each values. + * @param total the total value of the progress bar. + * @param width the width in which the progress bar should fit (in pixel for IG, in character count for console) + * @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat. + * @return a progress bar using text. + */ + public static Chat progressBar(double[] values, TextColor[] colors, double total, int width, boolean console) { + + // 1. Compute char size for each values + int progressPixelWidth = width - strWidth(PROGRESS_BAR_START + PROGRESS_BAR_END, console, false); + int charPixelWidth = charW(PROGRESS_BAR_EMPTY_CHAR, console, false); + + assert charPixelWidth == charW(PROGRESS_BAR_FULL_CHAR, console, false) : "PROGRESS_BAR_EMPTY_CHAR and PROGRESS_BAR_FULL_CHAR should have the same pixel width according to #charW(...)"; + + int progressCharWidth = progressPixelWidth / charPixelWidth; + + int[] sizes = new int[values.length]; + double sumValuesBefore = 0; + int sumSizesBefore = 0; + + for (int i = 0; i < values.length; i++) { + sumValuesBefore += values[i]; + int charPosition = Math.min((int) Math.round(progressCharWidth * sumValuesBefore / total), progressCharWidth); + sizes[i] = charPosition - sumSizesBefore; + sumSizesBefore += sizes[i]; + } + + // 2. Generate rendered text + Chat c = ChatStatic.text(PROGRESS_BAR_START); + + int sumSizes = 0; + for (int i = 0; i < sizes.length; i++) { + sumSizes += sizes[i]; + + FormatableChat subC = ChatStatic.text(repeatedChar(PROGRESS_BAR_FULL_CHAR, sizes[i])); + + if (colors != null && i < colors.length && colors[i] != null) + subC.color(colors[i]); + + c.then(subC); + } + + return c + .then(ChatStatic.text(repeatedChar(PROGRESS_BAR_EMPTY_CHAR, progressCharWidth - sumSizes)) + .color(PROGRESS_BAR_EMPTY_COLOR)) + .thenText(PROGRESS_BAR_END); + } + + /** + * Generate a progress bar using text. + * @param value the value to render in the progress bar. + * @param color the color of the filled part of the bar. + * @param total the total value of the progress bar. + * @param width the width in which the progress bar should fit (in pixel for IG, in character count for console) + * @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat. + * @return a progress bar using text. + */ + public static Chat progressBar(double value, TextColor color, double total, int width, boolean console) { + return progressBar(new double[] { value }, new TextColor[] { color }, total, width, console); + } + + + /** + * Truncate an eventually too long prefix (like team prefix or permission group prefix), keep the last color and + * format. + * @param prefix the prefix that eventually needs truncation. + * @param maxLength the maximum length of the prefix. + * @return a truncated prefix, with the last color kept. + */ + public static String truncatePrefix(String prefix, int maxLength) { + if (prefix.length() > maxLength) { + String lastColor = ChatColorUtil.getLastColors(prefix); + prefix = truncateAtLengthWithoutReset(prefix, maxLength); + if (!ChatColorUtil.getLastColors(prefix).equals(lastColor)) + prefix = truncateAtLengthWithoutReset(prefix, maxLength - lastColor.length()) + lastColor; + } + return prefix; + } + + /** + * Truncate an eventually too long string, also taking care of removing an eventual {@code §} character leftText alone + * at the end. + * @param str the string to eventually truncate. + * @param maxLength the maximum length of the string. + * @return a truncated string. + */ + public static String truncateAtLengthWithoutReset(String str, int maxLength) { + if (str.length() > maxLength) { + str = str.substring(0, maxLength); + if (str.endsWith("§")) + str = str.substring(0, str.length()-1); + } + return str; + } + + } diff --git a/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermEntity.java b/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermEntity.java index 576eaa3..dd67cf3 100644 --- a/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermEntity.java +++ b/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermEntity.java @@ -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); } diff --git a/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermissionsResolver.java b/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermissionsResolver.java index 78b6204..3d7424d 100644 --- a/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermissionsResolver.java +++ b/pandalib-permissions/src/main/java/fr/pandacube/lib/permissions/PermissionsResolver.java @@ -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))); }