diff --git a/Spigot-API-Patches/0157-Textual-Chat-Format.patch b/Spigot-API-Patches/0157-Textual-Chat-Format.patch new file mode 100644 index 000000000000..6791d444ce44 --- /dev/null +++ b/Spigot-API-Patches/0157-Textual-Chat-Format.patch @@ -0,0 +1,904 @@ +From 4a6d06cf909573bec873bf16908bc73318c5ab03 Mon Sep 17 00:00:00 2001 +From: MiniDigger +Date: Fri, 28 Sep 2018 08:08:15 +0000 +Subject: [PATCH] Textual Chat Format + + +diff --git a/src/main/java/co/aikar/timings/TimingsReportListener.java b/src/main/java/co/aikar/timings/TimingsReportListener.java +index e7c389c0..a9eb3b17 100644 +--- a/src/main/java/co/aikar/timings/TimingsReportListener.java ++++ b/src/main/java/co/aikar/timings/TimingsReportListener.java +@@ -9,6 +9,7 @@ import org.bukkit.command.MessageCommandSender; + import org.bukkit.command.RemoteConsoleCommandSender; + + import java.util.List; ++import java.util.Map; + + @SuppressWarnings("WeakerAccess") + public class TimingsReportListener implements MessageCommandSender { +@@ -58,6 +59,21 @@ public class TimingsReportListener implements MessageCommandSender { + senders.forEach((sender) -> sender.sendMessage(message)); + } + ++ @Override ++ public void sendRichMessage(String richMessage) { ++ senders.forEach((sender) -> sender.sendRichMessage(richMessage)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ senders.forEach((sender) -> sender.sendRichMessage(richMessage, placeholders)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, Map placeholders) { ++ senders.forEach((sender) -> sender.sendRichMessage(richMessage, placeholders)); ++ } ++ + public void addConsoleIfNeeded() { + boolean hasConsole = false; + for (CommandSender sender : this.senders) { +diff --git a/src/main/java/com/destroystokyo/paper/ComponentParser.java b/src/main/java/com/destroystokyo/paper/ComponentParser.java +new file mode 100755 +index 00000000..d1a93502 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/ComponentParser.java +@@ -0,0 +1,312 @@ ++package com.destroystokyo.paper; ++ ++import net.md_5.bungee.api.ChatColor; ++import net.md_5.bungee.api.chat.BaseComponent; ++import net.md_5.bungee.api.chat.ClickEvent; ++import net.md_5.bungee.api.chat.ComponentBuilder; ++import net.md_5.bungee.api.chat.HoverEvent; ++ ++import java.util.EnumSet; ++import java.util.Map; ++import java.util.Optional; ++import java.util.Stack; ++import java.util.function.Consumer; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++import javax.annotation.Nonnull; ++ ++public class ComponentParser { ++ ++ // https://regex101.com/r/8VZ7uA/5 ++ private static Pattern pattern = Pattern.compile("((?<)(?([^<>]+)|([^<>]+\"(?[^\"]+)\"))(?>))+?"); ++ ++ @Nonnull ++ public static String escapeTokens(@Nonnull String richMessage) { ++ StringBuilder sb = new StringBuilder(); ++ Matcher matcher = pattern.matcher(richMessage); ++ int lastEnd = 0; ++ while (matcher.find()) { ++ int startIndex = matcher.start(); ++ int endIndex = matcher.end(); ++ ++ if (startIndex > lastEnd) { ++ sb.append(richMessage, lastEnd, startIndex); ++ } ++ lastEnd = endIndex; ++ ++ String start = matcher.group("start"); ++ String token = matcher.group("token"); ++ String inner = matcher.group("inner"); ++ String end = matcher.group("end"); ++ ++ // also escape inner ++ if (inner != null) { ++ token = token.replace(inner, escapeTokens(inner)); ++ } ++ ++ sb.append("\\").append(start).append(token).append("\\").append(end); ++ } ++ ++ if (richMessage.length() > lastEnd) { ++ sb.append(richMessage.substring(lastEnd)); ++ } ++ ++ return sb.toString(); ++ } ++ ++ @Nonnull ++ public static String stripTokens(@Nonnull String richMessage) { ++ StringBuilder sb = new StringBuilder(); ++ Matcher matcher = pattern.matcher(richMessage); ++ int lastEnd = 0; ++ while (matcher.find()) { ++ int startIndex = matcher.start(); ++ int endIndex = matcher.end(); ++ ++ if (startIndex > lastEnd) { ++ sb.append(richMessage, lastEnd, startIndex); ++ } ++ lastEnd = endIndex; ++ } ++ ++ if (richMessage.length() > lastEnd) { ++ sb.append(richMessage.substring(lastEnd)); ++ } ++ ++ return sb.toString(); ++ } ++ ++ @Nonnull ++ public static String handlePlaceholders(@Nonnull String richMessage, @Nonnull String... placeholders) { ++ if (placeholders.length % 2 != 0) { ++ throw new RuntimeException( ++ "Invalid number placeholders defined, usage: parseFormat(format, key, value, key, value...)"); ++ } ++ for (int i = 0; i < placeholders.length; i += 2) { ++ richMessage = richMessage.replace("<" + placeholders[i] + ">", placeholders[i + 1]); ++ } ++ return richMessage; ++ } ++ ++ @Nonnull ++ public static String handlePlaceholders(@Nonnull String richMessage, @Nonnull Map placeholders) { ++ for (Map.Entry entry : placeholders.entrySet()) { ++ richMessage = richMessage.replace("<" + entry.getKey() + ">", entry.getValue()); ++ } ++ return richMessage; ++ } ++ ++ @Nonnull ++ public static BaseComponent[] parseFormat(@Nonnull String richMessage, @Nonnull String... placeholders) { ++ return parseFormat(handlePlaceholders(richMessage, placeholders)); ++ } ++ ++ @Nonnull ++ public static BaseComponent[] parseFormat(@Nonnull String richMessage, @Nonnull Map placeholders) { ++ return parseFormat(handlePlaceholders(richMessage, placeholders)); ++ } ++ ++ @Nonnull ++ public static BaseComponent[] parseFormat(@Nonnull String richMessage) { ++ ComponentBuilder builder = null; ++ ++ Stack clickEvents = new Stack<>(); ++ Stack hoverEvents = new Stack<>(); ++ Stack colors = new Stack<>(); ++ EnumSet decorations = EnumSet.noneOf(TextDecoration.class); ++ ++ Matcher matcher = pattern.matcher(richMessage); ++ int lastEnd = 0; ++ while (matcher.find()) { ++ int startIndex = matcher.start(); ++ int endIndex = matcher.end(); ++ ++ String msg = null; ++ if (startIndex > lastEnd) { ++ msg = richMessage.substring(lastEnd, startIndex); ++ } ++ lastEnd = endIndex; ++ ++ // handle message ++ if (msg != null && msg.length() != 0) { ++ // append message ++ if (builder == null) { ++ builder = new ComponentBuilder(msg); ++ } else { ++ builder.append(msg, ComponentBuilder.FormatRetention.NONE); ++ } ++ ++ // set everything that is not closed yet ++ if (clickEvents.size() > 0) { ++ builder.event(clickEvents.peek()); ++ } ++ if (hoverEvents.size() > 0) { ++ builder.event(hoverEvents.peek()); ++ } ++ if (colors.size() > 0) { ++ builder.color(colors.peek()); ++ } ++ if (decorations.size() > 0) { ++ // no lambda because builder isn't effective final :/ ++ for (TextDecoration decor : decorations) { ++ decor.apply(builder); ++ } ++ } ++ } ++ ++// String group = matcher.group(); ++// String start = matcher.group("start"); ++ String token = matcher.group("token"); ++ String inner = matcher.group("inner"); ++// String end = matcher.group("end"); ++ ++ Optional deco; ++ Optional color; ++ ++ // click ++ if (token.startsWith("click:")) { ++ clickEvents.push(handleClick(token, inner)); ++ } else if (token.equals("/click")) { ++ clickEvents.pop(); ++ } ++ // hover ++ else if (token.startsWith("hover:")) { ++ hoverEvents.push(handleHover(token, inner)); ++ } else if (token.equals("/hover")) { ++ hoverEvents.pop(); ++ } ++ // decoration ++ else if ((deco = resolveDecoration(token)).isPresent()) { ++ decorations.add(deco.get()); ++ } else if (token.startsWith("/") && (deco = resolveDecoration(token.replace("/", ""))).isPresent()) { ++ decorations.remove(deco.get()); ++ } ++ // color ++ else if ((color = resolveColor(token)).isPresent()) { ++ colors.push(color.get()); ++ } else if (token.startsWith("/") && resolveColor(token.replace("/", "")).isPresent()) { ++ colors.pop(); ++ } ++ } ++ ++ // handle last message part ++ if (richMessage.length() > lastEnd) { ++ String msg = richMessage.substring(lastEnd, richMessage.length()); ++ // append message ++ if (builder == null) { ++ builder = new ComponentBuilder(msg); ++ } else { ++ builder.append(msg, ComponentBuilder.FormatRetention.NONE); ++ } ++ ++ // set everything that is not closed yet ++ if (clickEvents.size() > 0) { ++ builder.event(clickEvents.peek()); ++ } ++ if (hoverEvents.size() > 0) { ++ builder.event(hoverEvents.peek()); ++ } ++ if (colors.size() > 0) { ++ builder.color(colors.peek()); ++ } ++ if (decorations.size() > 0) { ++ // no lambda because builder isn't effective final :/ ++ for (TextDecoration decor : decorations) { ++ decor.apply(builder); ++ } ++ } ++ } ++ ++ if (builder == null) { ++ // lets just return an empty component ++ builder = new ComponentBuilder(""); ++ } ++ ++ return builder.create(); ++ } ++ ++ @Nonnull ++ private static ClickEvent handleClick(@Nonnull String token, @Nonnull String inner) { ++ String[] args = token.split(":"); ++ ClickEvent clickEvent; ++ if (args.length < 2) { ++ throw new RuntimeException("Can't parse click action (too few args) " + token); ++ } ++ switch (args[1]) { ++ case "run_command": ++ clickEvent = new ClickEvent(ClickEvent.Action.RUN_COMMAND, token.replace("click:run_command:", "")); ++ break; ++ case "suggest_command": ++ clickEvent = new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, token.replace("click:suggest_command:", "")); ++ break; ++ case "open_url": ++ clickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL, token.replace("click:open_url:", "")); ++ break; ++ case "change_page": ++ clickEvent = new ClickEvent(ClickEvent.Action.CHANGE_PAGE, token.replace("click:change_page:", "")); ++ break; ++ default: ++ throw new RuntimeException("Can't parse click action (invalid type " + args[1] + ") " + token); ++ } ++ return clickEvent; ++ } ++ ++ @Nonnull ++ private static HoverEvent handleHover(@Nonnull String token, @Nonnull String inner) { ++ String[] args = token.split(":"); ++ HoverEvent hoverEvent; ++ if (args.length < 2) { ++ throw new RuntimeException("Can't parse hover action (too few args) " + token); ++ } ++ switch (args[1]) { ++ case "show_text": ++ hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, parseFormat(inner)); ++ break; ++ case "show_item": ++ hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ITEM, parseFormat(inner)); ++ break; ++ case "show_entity": ++ hoverEvent = new HoverEvent(HoverEvent.Action.SHOW_ENTITY, parseFormat(inner)); ++ break; ++ default: ++ throw new RuntimeException("Can't parse hover action (invalid type " + args[1] + ") " + token); ++ } ++ return hoverEvent; ++ } ++ ++ @Nonnull ++ private static Optional resolveColor(@Nonnull String token) { ++ try { ++ return Optional.of(ChatColor.valueOf(token.toUpperCase())); ++ } catch (IllegalArgumentException ex) { ++ return Optional.empty(); ++ } ++ } ++ ++ @Nonnull ++ private static Optional resolveDecoration(@Nonnull String token) { ++ try { ++ return Optional.of(TextDecoration.valueOf(token.toUpperCase())); ++ } catch (IllegalArgumentException ex) { ++ return Optional.empty(); ++ } ++ } ++ ++ enum TextDecoration { ++ BOLD(builder -> builder.bold(true)), ++ ITALIC(builder -> builder.italic(true)), ++ UNDERLINED(builder -> builder.underlined(true)), ++ STRIKETHROUGH(builder -> builder.strikethrough(true)), ++ OBFUSCATED(builder -> builder.obfuscated(true)); ++ ++ private Consumer builder; ++ ++ TextDecoration(Consumer builder) { ++ this.builder = builder; ++ } ++ ++ public void apply(ComponentBuilder comp) { ++ builder.accept(comp); ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/ComponentSerializer.java b/src/main/java/com/destroystokyo/paper/ComponentSerializer.java +new file mode 100755 +index 00000000..60fe6523 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/ComponentSerializer.java +@@ -0,0 +1,151 @@ ++package com.destroystokyo.paper; ++ ++import net.md_5.bungee.api.ChatColor; ++import net.md_5.bungee.api.chat.BaseComponent; ++import net.md_5.bungee.api.chat.ClickEvent; ++import net.md_5.bungee.api.chat.HoverEvent; ++ ++import javax.annotation.Nonnull; ++ ++public class ComponentSerializer { ++ ++ @Nonnull ++ public static String serialize(@Nonnull BaseComponent... components) { ++ StringBuilder sb = new StringBuilder(); ++ ++ for (int i = 0; i < components.length; i++) { ++ BaseComponent comp = components[i]; ++ ++ // # start tags ++ ++ // ## get prev comp ++ BaseComponent prevComp = null; ++ if (i > 0) { ++ prevComp = components[i - 1]; ++ } ++ ++ // ## color ++ // ### white is not important ++ if (!ChatColor.WHITE.equals(comp.getColor())) { ++ sb.append(startColor(comp.getColor())); ++ } ++ ++ // ## decoration ++ // ### only start if prevComp didn't start ++ if (comp.isBold() && (prevComp == null || !prevComp.isBold())) { ++ sb.append(startTag("bold")); ++ } ++ if (comp.isItalic() && (prevComp == null || !prevComp.isItalic())) { ++ sb.append(startTag("italic")); ++ } ++ if (comp.isObfuscated() && (prevComp == null || !prevComp.isObfuscated())) { ++ sb.append(startTag("obfuscated")); ++ } ++ if (comp.isStrikethrough() && (prevComp == null || !prevComp.isStrikethrough())) { ++ sb.append(startTag("strikethrough")); ++ } ++ if (comp.isUnderlined() && (prevComp == null || !prevComp.isUnderlined())) { ++ sb.append(startTag("underlined")); ++ } ++ ++ // ## hover ++ // ### only start if prevComp didn't start the same one ++ HoverEvent hov = comp.getHoverEvent(); ++ if (hov != null && (prevComp == null || !equals(hov, prevComp.getHoverEvent()))) { ++ sb.append(startTag( ++ "hover:" + hov.getAction().name().toLowerCase() + ":\"" + serialize(hov.getValue()) + "\"")); ++ } ++ ++ // ## click ++ // ### only start if prevComp didn't start the same one ++ ClickEvent click = comp.getClickEvent(); ++ if (click != null && (prevComp == null || !equals(click, prevComp.getClickEvent()))) { ++ sb.append( ++ startTag("click:" + click.getAction().name().toLowerCase() + ":\"" + click.getValue() + "\"")); ++ } ++ ++ // # append text ++ sb.append(comp.toPlainText()); ++ ++ // # end tags ++ ++ // ## get next comp ++ BaseComponent nextComp = null; ++ if (i + 1 < components.length) { ++ nextComp = components[i + 1]; ++ } ++ ++ // ## color ++ // ### only end color if next comp is white and curren't isn't ++ if (nextComp != null && comp.getColor() != ChatColor.WHITE) { ++ if (nextComp.getColor() == ChatColor.WHITE || nextComp.getColor() == null) { ++ sb.append(endColor(comp.getColor())); ++ } ++ } ++ ++ // ## decoration ++ // ### only end decoration if next tag is different ++ if (nextComp != null) { ++ if (comp.isBold() && !nextComp.isBold()) { ++ sb.append(endTag("bold")); ++ } ++ if (comp.isItalic() && !nextComp.isItalic()) { ++ sb.append(endTag("italic")); ++ } ++ if (comp.isObfuscated() && !nextComp.isObfuscated()) { ++ sb.append(endTag("obfuscated")); ++ } ++ if (comp.isStrikethrough() && !nextComp.isStrikethrough()) { ++ sb.append(endTag("strikethrough")); ++ } ++ if (comp.isUnderlined() && !nextComp.isUnderlined()) { ++ sb.append(endTag("underlined")); ++ } ++ } ++ ++ // ## hover ++ // ### only end hover if next tag is different ++ if (nextComp != null && comp.getHoverEvent() != null) { ++ if (!equals(comp.getHoverEvent(), nextComp.getHoverEvent())) { ++ sb.append(endTag("hover")); ++ } ++ } ++ ++ // ## click ++ // ### only end click if next tag is different ++ if (nextComp != null && comp.getClickEvent() != null) { ++ if (!equals(comp.getClickEvent(), nextComp.getClickEvent())) { ++ sb.append(endTag("click")); ++ } ++ } ++ } ++ ++ return sb.toString(); ++ } ++ ++ private static boolean equals(ClickEvent c1, ClickEvent c2) { ++ if (c2 == null && c1 != null) return false; ++ return c1.equals(c2) || (c1.getAction().equals(c2.getAction()) && c1.getValue().equals(c2.getValue())); ++ } ++ ++ private static boolean equals(HoverEvent h1, HoverEvent h2) { ++ if (h2 == null && h1 != null) return false; ++ return h1.equals(h2) || (h1.getAction().equals(h2.getAction()));// TODO also compare value ++ } ++ ++ private static String startColor(ChatColor color) { ++ return startTag(color.name().toLowerCase()); ++ } ++ ++ private static String endColor(ChatColor color) { ++ return endTag(color.name().toLowerCase()); ++ } ++ ++ private static String startTag(String content) { ++ return String.format("<%s>", content); ++ } ++ ++ private static String endTag(String content) { ++ return String.format("", content); ++ } ++} +diff --git a/src/main/java/org/bukkit/command/BufferedCommandSender.java b/src/main/java/org/bukkit/command/BufferedCommandSender.java +index fd452bce..d1832515 100644 +--- a/src/main/java/org/bukkit/command/BufferedCommandSender.java ++++ b/src/main/java/org/bukkit/command/BufferedCommandSender.java +@@ -8,6 +8,26 @@ public class BufferedCommandSender implements MessageCommandSender { + buffer.append("\n"); + } + ++ // Paper start: textual chat format ++ @Override ++ public void sendRichMessage(String richMessage) { ++ buffer.append(richMessage);//TODO check that this actually works ++ buffer.append("\n"); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ buffer.append(richMessage);//TODO fixme ++ buffer.append("\n"); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ buffer.append(richMessage);//TODO fixme ++ buffer.append("\n"); ++ } ++ // Paper end ++ + public String getBuffer() { + return buffer.toString(); + } +diff --git a/src/main/java/org/bukkit/command/CommandSender.java b/src/main/java/org/bukkit/command/CommandSender.java +old mode 100644 +new mode 100755 +index fcb03b83..0df06229 +--- a/src/main/java/org/bukkit/command/CommandSender.java ++++ b/src/main/java/org/bukkit/command/CommandSender.java +@@ -1,5 +1,7 @@ + package org.bukkit.command; + ++import com.destroystokyo.paper.ComponentParser; ++ + import org.bukkit.Server; + import org.bukkit.permissions.Permissible; + +@@ -19,6 +21,42 @@ public interface CommandSender extends Permissible { + */ + public void sendMessage(String[] messages); + ++ // Paper start: textual chat format ++ /** ++ * Sends this sender a rich message
++ * For details on the rich/textual string format, see {@link ComponentParser} ++ * ++ * @param richMessage rich message to be displayed ++ */ ++ public void sendRichMessage(String richMessage); ++ ++ /** ++ * Sends this sender a rich message with placeholders
++ * For details on the rich/textual string format, see {@link ComponentParser} ++ * ++ * @param richMessage rich message to be displayed ++ * @param placeholders key and value pairs for placeholders in the message ++ */ ++ public void sendRichMessage(String richMessage, String... placeholders); ++ ++ /** ++ * Sends this sender a rich message with placeholders
++ * For details on the rich/textual string format, see {@link ComponentParser} ++ * ++ * @param richMessage rich message to be displayed ++ * @param placeholders key and value pairs for placeholders in the message ++ */ ++ public void sendRichMessage(String richMessage, java.util.Map placeholders); ++ ++ /** ++ * Sends this sender rich enhanced messages
++ * For details on the rich/textual string format, see {@link ComponentParser} ++ * ++ * @param richMessages An array of rich messages to be displayed ++ */ ++ public void sendRichMessage(String[] richMessages); ++ // Paper end ++ + /** + * Returns the server instance that this command is running on + * +diff --git a/src/main/java/org/bukkit/command/MessageCommandSender.java b/src/main/java/org/bukkit/command/MessageCommandSender.java +index 5527e7c8..9f5cb5b5 100644 +--- a/src/main/java/org/bukkit/command/MessageCommandSender.java ++++ b/src/main/java/org/bukkit/command/MessageCommandSender.java +@@ -22,6 +22,13 @@ public interface MessageCommandSender extends CommandSender { + } + } + ++ @Override ++ default void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ + @Override + default Server getServer() { + return Bukkit.getServer(); +diff --git a/src/test/java/com/destroystokyo/paper/ComponentParserTest.java b/src/test/java/com/destroystokyo/paper/ComponentParserTest.java +new file mode 100755 +index 00000000..edb711c9 +--- /dev/null ++++ b/src/test/java/com/destroystokyo/paper/ComponentParserTest.java +@@ -0,0 +1,146 @@ ++package com.destroystokyo.paper; ++ ++import net.md_5.bungee.api.chat.BaseComponent; ++import net.md_5.bungee.chat.ComponentSerializer; ++ ++import org.junit.Test; ++ ++import static org.junit.Assert.assertEquals; ++ ++public class ComponentParserTest { ++ ++ @Test ++ public void test() { ++ String input1 = "TEST nestedTest"; ++ String input2 = "TEST nestedTest"; ++ String out1 = ComponentSerializer.toString(ComponentParser.parseFormat(input1)); ++ String out2 = ComponentSerializer.toString(ComponentParser.parseFormat(input2)); ++ System.out.println(out1); ++ System.out.println(out2); ++ } ++ ++ @Test ++ public void testStripSimple() { ++ String input = "TEST nestedTest"; ++ String expected = "TEST nestedTest"; ++ assertEquals(expected, ComponentParser.stripTokens(input)); ++ } ++ ++ @Test ++ public void testStripComplex() { ++ String input = " random strangerclick here to FEEL it"; ++ String expected = " random strangerclick here to FEEL it"; ++ assertEquals(expected, ComponentParser.stripTokens(input)); ++ } ++ ++ @Test ++ public void testStripInner() { ++ String input = "test:TEST\">TEST"; ++ String expected = "TEST"; ++ assertEquals(expected, ComponentParser.stripTokens(input)); ++ } ++ ++ @Test ++ public void testEscapeSimple() { ++ String input = "TEST nestedTest"; ++ String expected = "\\TEST\\ nested\\Test"; ++ assertEquals(expected, ComponentParser.escapeTokens(input)); ++ } ++ ++ @Test ++ public void testEscapeComplex() { ++ String input = " random strangerclick here to FEEL it"; ++ String expected = "\\\\ random \\stranger\\\\\\\\click here\\\\ to \\FEEL\\ it"; ++ assertEquals(expected, ComponentParser.escapeTokens(input)); ++ } ++ ++ @Test ++ public void testEscapeInner() { ++ String input = "test:TEST\">TEST"; ++ String expected = "\\test:TEST\"\\>TEST"; ++ assertEquals(expected, ComponentParser.escapeTokens(input)); ++ } ++ ++ ++ @Test ++ public void checkPlaceholder() { ++ String input = ""; ++ String expected = "{\"text\":\"Hello!\"}"; ++ BaseComponent[] comp = ComponentParser.parseFormat(input, "test", "Hello!"); ++ ++ test(comp, expected); ++ } ++ ++ @Test ++ public void testNiceMix() { ++ String input = " random strangerclick here to FEEL it"; ++ String expected = "{\"extra\":[{\"color\":\"yellow\",\"text\":\"Hello! random \"},{\"color\":\"yellow\",\"bold\":true,\"text\":\"stranger\"},{\"color\":\"red\",\"underlined\":true,\"clickEvent\":{\"action\":\"run_command\",\"value\":\"test command\"},\"text\":\"click here\"},{\"color\":\"blue\",\"underlined\":true,\"text\":\" to \"},{\"color\":\"blue\",\"bold\":true,\"underlined\":true,\"text\":\"FEEL\"},{\"color\":\"blue\",\"bold\":true,\"text\":\" it\"}],\"text\":\"\"}"; ++ BaseComponent[] comp = ComponentParser.parseFormat(input, "test", "Hello!"); ++ ++ test(comp, expected); ++ } ++ ++ @Test ++ public void testColorSimple() { ++ String input = "TEST"; ++ String expected = "{\"color\":\"yellow\",\"text\":\"TEST\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testColorNested() { ++ String input = "TESTnestedTest"; ++ String expected = "{\"extra\":[{\"color\":\"yellow\",\"text\":\"TEST\"},{\"color\":\"green\",\"text\":\"nested\"},{\"color\":\"yellow\",\"text\":\"Test\"}],\"text\":\"\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testColorNotNested() { ++ String input = "TESTnestedTest"; ++ String expected = "{\"extra\":[{\"color\":\"yellow\",\"text\":\"TEST\"},{\"color\":\"green\",\"text\":\"nested\"},{\"text\":\"Test\"}],\"text\":\"\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testHover() { ++ String input = "test\">TEST"; ++ String expected = "{\"hoverEvent\":{\"action\":\"show_text\",\"value\":[{\"color\":\"red\",\"text\":\"test\"}]},\"text\":\"TEST\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testHoverWithColon() { ++ String input = "test:TEST\">TEST"; ++ String expected = "{\"hoverEvent\":{\"action\":\"show_text\",\"value\":[{\"color\":\"red\",\"text\":\"test:TEST\"}]},\"text\":\"TEST\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testClick() { ++ String input = "TEST"; ++ String expected = "{\"clickEvent\":{\"action\":\"run_command\",\"value\":\"test\"},\"text\":\"TEST\"}"; ++ ++ test(input, expected); ++ } ++ ++ @Test ++ public void testClickExtendedCommand() { ++ String input = "TEST"; ++ String expected = "{\"clickEvent\":{\"action\":\"run_command\",\"value\":\"test command\"},\"text\":\"TEST\"}"; ++ ++ test(input, expected); ++ } ++ ++ private void test(String input, String expected) { ++ test(ComponentParser.parseFormat(input), expected); ++ } ++ ++ private void test(BaseComponent[] comp, String expected) { ++ assertEquals(expected, ComponentSerializer.toString(comp)); ++ } ++} +diff --git a/src/test/java/com/destroystokyo/paper/ComponentSerializerTest.java b/src/test/java/com/destroystokyo/paper/ComponentSerializerTest.java +new file mode 100755 +index 00000000..7203d0d4 +--- /dev/null ++++ b/src/test/java/com/destroystokyo/paper/ComponentSerializerTest.java +@@ -0,0 +1,122 @@ ++package com.destroystokyo.paper; ++ ++import net.md_5.bungee.api.ChatColor; ++import net.md_5.bungee.api.chat.ClickEvent; ++import net.md_5.bungee.api.chat.ComponentBuilder; ++import net.md_5.bungee.api.chat.ComponentBuilder.FormatRetention; ++import net.md_5.bungee.api.chat.HoverEvent; ++import net.md_5.bungee.api.chat.HoverEvent.Action; ++ ++import org.junit.Test; ++ ++import static org.junit.Assert.assertEquals; ++ ++public class ComponentSerializerTest { ++ ++ @Test ++ public void testColor() { ++ String expected = "This is a test"; ++ ++ ComponentBuilder builder = new ComponentBuilder("This is a test"); ++ builder.color(ChatColor.RED); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testColorClosing() { ++ String expected = "This is a test"; ++ ++ ComponentBuilder builder = new ComponentBuilder("This is a "); ++ builder.color(ChatColor.RED); ++ builder.append("test", FormatRetention.NONE); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testNestedColor() { ++ String expected = "This is a blue test"; ++ ++ ComponentBuilder builder = new ComponentBuilder("This is a ").color(ChatColor.RED)// ++ .append("blue ", FormatRetention.NONE).color(ChatColor.BLUE)// ++ .append("test", FormatRetention.NONE).color(ChatColor.RED); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testDecoration() { ++ String expected = "This is underlined, this isn't"; ++ ++ ComponentBuilder builder = new ComponentBuilder("This is ").underlined(true)// ++ .append("underlined", FormatRetention.NONE).bold(true).underlined(true)// ++ .append(", this", FormatRetention.NONE).bold(true)// ++ .append(" isn't", FormatRetention.NONE); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testHover() { ++ String expected = "Some hover that ends here"; ++ ++ ComponentBuilder builder = new ComponentBuilder("Some hover") ++ .event(new HoverEvent(Action.SHOW_TEXT, new ComponentBuilder("---").create()))// ++ .append(" that ends here", FormatRetention.NONE); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testHoverWithNested() { ++ String expected = "----\">Some hover that ends here"; ++ ++ ComponentBuilder builder = new ComponentBuilder("Some hover").event(new HoverEvent(Action.SHOW_TEXT, // ++ new ComponentBuilder("---").color(ChatColor.RED)// ++ .append("-").color(ChatColor.BLUE).bold(true).create()))// ++ .append(" that ends here", FormatRetention.NONE); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testClick() { ++ String expected = "Some click that ends here"; ++ ++ ComponentBuilder builder = new ComponentBuilder("Some click") ++ .event(new ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "test"))// ++ .append(" that ends here", FormatRetention.NONE); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testContinuedClick() { ++ String expected = "Some click that doesn't ends here"; ++ ++ ComponentBuilder builder = new ComponentBuilder("Some click") ++ .event(new ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "test"))// ++ .append(" that doesn't ends here", FormatRetention.EVENTS).color(ChatColor.RED); ++ ++ test(builder, expected); ++ } ++ ++ @Test ++ public void testContinuedClick2() { ++ String expected = "Some click that doesn't ends here"; ++ ++ ComponentBuilder builder = new ComponentBuilder("Some click") ++ .event(new ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "test"))// ++ .append(" that doesn't ends here", FormatRetention.NONE) ++ .event(new ClickEvent(net.md_5.bungee.api.chat.ClickEvent.Action.RUN_COMMAND, "test")) ++ .color(ChatColor.RED); ++ ++ test(builder, expected); ++ } ++ ++ private void test(ComponentBuilder builder, String expected) { ++ String string = ComponentSerializer.serialize(builder.create()); ++ assertEquals(expected, string); ++ } ++} +-- +2.17.1 + diff --git a/Spigot-Server-Patches/0310-Prevent-Saving-Bad-entities-to-chunks.patch b/Spigot-Server-Patches/0310-Prevent-Saving-Bad-entities-to-chunks.patch index db34e0ca8e37..0d0c2745dee4 100644 --- a/Spigot-Server-Patches/0310-Prevent-Saving-Bad-entities-to-chunks.patch +++ b/Spigot-Server-Patches/0310-Prevent-Saving-Bad-entities-to-chunks.patch @@ -1,4 +1,4 @@ -From fb88b169444615df90c320481befdc3309aea0cd Mon Sep 17 00:00:00 2001 +From da641b69b998c02db2280fc7022b6ff211eca042 Mon Sep 17 00:00:00 2001 From: Aikar Date: Thu, 26 Jul 2018 00:11:12 -0400 Subject: [PATCH] Prevent Saving Bad entities to chunks @@ -18,7 +18,7 @@ an invalid entity. This should reduce log occurrences of dupe uuid messages. diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java -index 3d8f0b5786..db5c6e0f74 100644 +index 3d8f0b57..db5c6e0f 100644 --- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java +++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java @@ -563,11 +563,22 @@ public class ChunkRegionLoader implements IChunkLoader, IAsyncChunkSaver { @@ -57,5 +57,5 @@ index 3d8f0b5786..db5c6e0f74 100644 nbttagcompound.set("Entities", nbttaglist1); NBTTagList nbttaglist2 = new NBTTagList(); -- -2.19.0 +2.17.1 diff --git a/Spigot-Server-Patches/0381-Textual-Chat-Format.patch b/Spigot-Server-Patches/0381-Textual-Chat-Format.patch new file mode 100644 index 000000000000..0e7b03d0b33a --- /dev/null +++ b/Spigot-Server-Patches/0381-Textual-Chat-Format.patch @@ -0,0 +1,581 @@ +From 5576a212b26ed26a81c9a8f90a61de539c03f00c Mon Sep 17 00:00:00 2001 +From: MiniDigger +Date: Fri, 28 Sep 2018 09:15:15 +0000 +Subject: [PATCH] Textual Chat Format + + +diff --git a/src/main/java/net/minecraft/server/TileEntitySign.java b/src/main/java/net/minecraft/server/TileEntitySign.java +old mode 100644 +new mode 100755 +index 20dc3f27..6f4a561d +--- a/src/main/java/net/minecraft/server/TileEntitySign.java ++++ b/src/main/java/net/minecraft/server/TileEntitySign.java +@@ -1,157 +1,157 @@ +-package net.minecraft.server; +- +-import com.mojang.brigadier.exceptions.CommandSyntaxException; +-import javax.annotation.Nullable; +- +-public class TileEntitySign extends TileEntity implements ICommandListener { +- +- public final IChatBaseComponent[] lines = new IChatBaseComponent[] { new ChatComponentText(""), new ChatComponentText(""), new ChatComponentText(""), new ChatComponentText("")}; +- public int e = -1; +- public boolean isEditable = true; +- private EntityHuman g; +- private final String[] h = new String[4]; +- +- public TileEntitySign() { +- super(TileEntityTypes.SIGN); +- } +- +- public NBTTagCompound save(NBTTagCompound nbttagcompound) { +- super.save(nbttagcompound); +- +- for (int i = 0; i < 4; ++i) { +- String s = IChatBaseComponent.ChatSerializer.a(this.lines[i]); +- +- nbttagcompound.setString("Text" + (i + 1), s); +- } +- +- // CraftBukkit start +- if (Boolean.getBoolean("convertLegacySigns")) { +- nbttagcompound.setBoolean("Bukkit.isConverted", true); +- } +- // CraftBukkit end +- +- return nbttagcompound; +- } +- +- public void load(NBTTagCompound nbttagcompound) { +- this.isEditable = false; +- super.load(nbttagcompound); +- +- // CraftBukkit start - Add an option to convert signs correctly +- // This is done with a flag instead of all the time because +- // we have no way to tell whether a sign is from 1.7.10 or 1.8 +- +- boolean oldSign = Boolean.getBoolean("convertLegacySigns") && !nbttagcompound.getBoolean("Bukkit.isConverted"); +- +- for (int i = 0; i < 4; ++i) { +- String s = nbttagcompound.getString("Text" + (i + 1)); +- if (s != null && s.length() > 2048) { +- s = "\"\""; +- } +- +- try { +- //IChatBaseComponent ichatbasecomponent = IChatBaseComponent.ChatSerializer.a(s); // Paper - move down - the old format might throw a json error +- +- if (oldSign && !isLoadingStructure) { // Paper - saved structures will be in the new format, but will not have isConverted +- lines[i] = org.bukkit.craftbukkit.util.CraftChatMessage.fromString(s)[0]; +- continue; +- } +- // CraftBukkit end +- IChatBaseComponent ichatbasecomponent = IChatBaseComponent.ChatSerializer.a(s); // Paper - after old sign +- +- if (this.world instanceof WorldServer) { +- try { +- this.lines[i] = ChatComponentUtils.filterForDisplay(this.a((EntityPlayer) null), ichatbasecomponent, (Entity) null); +- } catch (CommandSyntaxException commandsyntaxexception) { +- this.lines[i] = ichatbasecomponent; +- } +- } else { +- this.lines[i] = ichatbasecomponent; +- } +- } catch (com.google.gson.JsonParseException jsonparseexception) { +- this.lines[i] = new ChatComponentText(s); +- } +- +- this.h[i] = null; +- } +- +- } +- +- public void a(int i, IChatBaseComponent ichatbasecomponent) { +- this.lines[i] = ichatbasecomponent; +- this.h[i] = null; +- } +- +- @Nullable +- public PacketPlayOutTileEntityData getUpdatePacket() { +- return new PacketPlayOutTileEntityData(this.position, 9, this.aa_()); +- } +- +- public NBTTagCompound aa_() { +- return this.save(new NBTTagCompound()); +- } +- +- public boolean isFilteredNBT() { +- return true; +- } +- +- public boolean d() { +- return this.isEditable; +- } +- +- public void a(EntityHuman entityhuman) { +- this.g = entityhuman; +- } +- +- public EntityHuman e() { +- return this.g; +- } +- +- public boolean b(EntityHuman entityhuman) { +- IChatBaseComponent[] aichatbasecomponent = this.lines; +- int i = aichatbasecomponent.length; +- +- for (int j = 0; j < i; ++j) { +- IChatBaseComponent ichatbasecomponent = aichatbasecomponent[j]; +- ChatModifier chatmodifier = ichatbasecomponent == null ? null : ichatbasecomponent.getChatModifier(); +- +- if (chatmodifier != null && chatmodifier.h() != null) { +- ChatClickable chatclickable = chatmodifier.h(); +- +- if (chatclickable.a() == ChatClickable.EnumClickAction.RUN_COMMAND) { +- entityhuman.bK().getCommandDispatcher().a(this.a((EntityPlayer) entityhuman), chatclickable.b()); +- } +- } +- } +- +- return true; +- } +- +- public void sendMessage(IChatBaseComponent ichatbasecomponent) {} +- +- // CraftBukkit start +- @Override +- public org.bukkit.command.CommandSender getBukkitSender(CommandListenerWrapper wrapper) { +- return wrapper.f() != null ? wrapper.f().getBukkitSender(wrapper) : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, this); +- } +- // CraftBukkit end +- +- public CommandListenerWrapper a(@Nullable EntityPlayer entityplayer) { +- String s = entityplayer == null ? "Sign" : entityplayer.getDisplayName().getString(); +- Object object = entityplayer == null ? new ChatComponentText("Sign") : entityplayer.getScoreboardDisplayName(); +- +- return new CommandListenerWrapper(this, new Vec3D((double) this.position.getX() + 0.5D, (double) this.position.getY() + 0.5D, (double) this.position.getZ() + 0.5D), Vec2F.a, (WorldServer) this.world, 2, s, (IChatBaseComponent) object, this.world.getMinecraftServer(), entityplayer); +- } +- +- public boolean a() { +- return false; +- } +- +- public boolean b() { +- return false; +- } +- +- public boolean B_() { +- return false; +- } +-} ++package net.minecraft.server; ++ ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import javax.annotation.Nullable; ++ ++public class TileEntitySign extends TileEntity implements ICommandListener { ++ ++ public final IChatBaseComponent[] lines = new IChatBaseComponent[] { new ChatComponentText(""), new ChatComponentText(""), new ChatComponentText(""), new ChatComponentText("")}; ++ public int e = -1; ++ public boolean isEditable = true; ++ private EntityHuman g; ++ private final String[] h = new String[4]; ++ ++ public TileEntitySign() { ++ super(TileEntityTypes.SIGN); ++ } ++ ++ public NBTTagCompound save(NBTTagCompound nbttagcompound) { ++ super.save(nbttagcompound); ++ ++ for (int i = 0; i < 4; ++i) { ++ String s = IChatBaseComponent.ChatSerializer.a(this.lines[i]); ++ ++ nbttagcompound.setString("Text" + (i + 1), s); ++ } ++ ++ // CraftBukkit start ++ if (Boolean.getBoolean("convertLegacySigns")) { ++ nbttagcompound.setBoolean("Bukkit.isConverted", true); ++ } ++ // CraftBukkit end ++ ++ return nbttagcompound; ++ } ++ ++ public void load(NBTTagCompound nbttagcompound) { ++ this.isEditable = false; ++ super.load(nbttagcompound); ++ ++ // CraftBukkit start - Add an option to convert signs correctly ++ // This is done with a flag instead of all the time because ++ // we have no way to tell whether a sign is from 1.7.10 or 1.8 ++ ++ boolean oldSign = Boolean.getBoolean("convertLegacySigns") && !nbttagcompound.getBoolean("Bukkit.isConverted"); ++ ++ for (int i = 0; i < 4; ++i) { ++ String s = nbttagcompound.getString("Text" + (i + 1)); ++ if (s != null && s.length() > 2048) { ++ s = "\"\""; ++ } ++ ++ try { ++ //IChatBaseComponent ichatbasecomponent = IChatBaseComponent.ChatSerializer.a(s); // Paper - move down - the old format might throw a json error ++ ++ if (oldSign && !isLoadingStructure) { // Paper - saved structures will be in the new format, but will not have isConverted ++ lines[i] = org.bukkit.craftbukkit.util.CraftChatMessage.fromString(s)[0]; ++ continue; ++ } ++ // CraftBukkit end ++ IChatBaseComponent ichatbasecomponent = IChatBaseComponent.ChatSerializer.a(s); // Paper - after old sign ++ ++ if (this.world instanceof WorldServer) { ++ try { ++ this.lines[i] = ChatComponentUtils.filterForDisplay(this.a((EntityPlayer) null), ichatbasecomponent, (Entity) null); ++ } catch (CommandSyntaxException commandsyntaxexception) { ++ this.lines[i] = ichatbasecomponent; ++ } ++ } else { ++ this.lines[i] = ichatbasecomponent; ++ } ++ } catch (com.google.gson.JsonParseException jsonparseexception) { ++ this.lines[i] = new ChatComponentText(s); ++ } ++ ++ this.h[i] = null; ++ } ++ ++ } ++ ++ public void a(int i, IChatBaseComponent ichatbasecomponent) { ++ this.lines[i] = ichatbasecomponent; ++ this.h[i] = null; ++ } ++ ++ @Nullable ++ public PacketPlayOutTileEntityData getUpdatePacket() { ++ return new PacketPlayOutTileEntityData(this.position, 9, this.aa_()); ++ } ++ ++ public NBTTagCompound aa_() { ++ return this.save(new NBTTagCompound()); ++ } ++ ++ public boolean isFilteredNBT() { ++ return true; ++ } ++ ++ public boolean d() { ++ return this.isEditable; ++ } ++ ++ public void a(EntityHuman entityhuman) { ++ this.g = entityhuman; ++ } ++ ++ public EntityHuman e() { ++ return this.g; ++ } ++ ++ public boolean b(EntityHuman entityhuman) { ++ IChatBaseComponent[] aichatbasecomponent = this.lines; ++ int i = aichatbasecomponent.length; ++ ++ for (int j = 0; j < i; ++j) { ++ IChatBaseComponent ichatbasecomponent = aichatbasecomponent[j]; ++ ChatModifier chatmodifier = ichatbasecomponent == null ? null : ichatbasecomponent.getChatModifier(); ++ ++ if (chatmodifier != null && chatmodifier.h() != null) { ++ ChatClickable chatclickable = chatmodifier.h(); ++ ++ if (chatclickable.a() == ChatClickable.EnumClickAction.RUN_COMMAND) { ++ entityhuman.bK().getCommandDispatcher().a(this.a((EntityPlayer) entityhuman), chatclickable.b()); ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public void sendMessage(IChatBaseComponent ichatbasecomponent) {} ++ ++ // CraftBukkit start ++ @Override ++ public org.bukkit.command.CommandSender getBukkitSender(CommandListenerWrapper wrapper) { ++ return wrapper.f() != null ? wrapper.f().getBukkitSender(wrapper) : new org.bukkit.craftbukkit.command.CraftBlockCommandSender(wrapper, this); ++ } ++ // CraftBukkit end ++ ++ public CommandListenerWrapper a(@Nullable EntityPlayer entityplayer) { ++ String s = entityplayer == null ? "Sign" : entityplayer.getDisplayName().getString(); ++ Object object = entityplayer == null ? new ChatComponentText("Sign") : entityplayer.getScoreboardDisplayName(); ++ ++ return new CommandListenerWrapper(this, new Vec3D((double) this.position.getX() + 0.5D, (double) this.position.getY() + 0.5D, (double) this.position.getZ() + 0.5D), Vec2F.a, (WorldServer) this.world, 2, s, (IChatBaseComponent) object, this.world.getMinecraftServer(), entityplayer); ++ } ++ ++ public boolean a() { ++ return false; ++ } ++ ++ public boolean b() { ++ return false; ++ } ++ ++ public boolean B_() { ++ return false; ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java +old mode 100644 +new mode 100755 +index 701a57e0..5951c7d8 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftBlockCommandSender.java +@@ -1,5 +1,7 @@ + package org.bukkit.craftbukkit.command; + ++import com.destroystokyo.paper.ComponentParser; // Paper ++ + import net.minecraft.server.CommandListenerWrapper; + import net.minecraft.server.IChatBaseComponent; + import net.minecraft.server.TileEntity; +@@ -38,6 +40,31 @@ public class CraftBlockCommandSender extends ServerCommandSender implements Bloc + } + } + ++ // Paper start: textual chat format ++ // TODO fixme, allow to send actual chat ++ @Override ++ public void sendRichMessage(String richMessage) { ++ sendMessage(ComponentParser.stripTokens(richMessage)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ // Paper end ++ + public String getName() { + return block.getName(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +old mode 100644 +new mode 100755 +index 9abcf92d..a54f70c2 +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftConsoleCommandSender.java +@@ -1,5 +1,7 @@ + package org.bukkit.craftbukkit.command; + ++import com.destroystokyo.paper.ComponentParser; // Paper ++ + import org.bukkit.ChatColor; + import org.bukkit.command.ConsoleCommandSender; + import org.bukkit.conversations.Conversation; +@@ -32,6 +34,30 @@ public class CraftConsoleCommandSender extends ServerCommandSender implements Co + } + } + ++ // Paper start: textual chat format ++ @Override ++ public void sendRichMessage(String richMessage) { ++ sendMessage(ComponentParser.stripTokens(richMessage)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ // Paper end ++ + public String getName() { + return "CONSOLE"; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java +old mode 100644 +new mode 100755 +index 228e88a6..8b5bb61c +--- a/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/CraftRemoteConsoleCommandSender.java +@@ -1,5 +1,7 @@ + package org.bukkit.craftbukkit.command; + ++import com.destroystokyo.paper.ComponentParser; // Paper ++ + import net.minecraft.server.ChatComponentText; + import net.minecraft.server.RemoteControlCommandListener; + import org.bukkit.command.RemoteConsoleCommandSender; +@@ -24,6 +26,31 @@ public class CraftRemoteConsoleCommandSender extends ServerCommandSender impleme + } + } + ++ // Paper start: textual chat format ++ // TODO fixme, allow to send actual chat ++ @Override ++ public void sendRichMessage(String richMessage) { ++ sendMessage(ComponentParser.stripTokens(richMessage)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ sendMessage(ComponentParser.stripTokens(ComponentParser.handlePlaceholders(richMessage, placeholders))); ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ // Paper end ++ + @Override + public String getName() { + return "Rcon"; +diff --git a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java +old mode 100644 +new mode 100755 +index 7609e861..ff50882d +--- a/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java ++++ b/src/main/java/org/bukkit/craftbukkit/command/ProxiedNativeCommandSender.java +@@ -48,6 +48,30 @@ public class ProxiedNativeCommandSender implements ProxiedCommandSender { + getCaller().sendMessage(messages); + } + ++ // Paper start: textual chat format ++ @Override ++ public void sendRichMessage(String richMessage) { ++ getCaller().sendRichMessage(richMessage); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ getCaller().sendRichMessage(richMessage, placeholders); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ getCaller().sendRichMessage(richMessage, placeholders); ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ // Paper end ++ + @Override + public Server getServer() { + return getCallee().getServer(); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index f4af6ea0..1c09e8f9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -626,6 +626,28 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + + } + ++ // Paper start: textual chat format ++ @Override ++ public void sendRichMessage(String richMessage) { ++ ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, java.util.Map placeholders) { ++ ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ ++ } ++ // Paper end ++ + @Override + public String getName() { + return CraftChatMessage.fromComponent(getHandle().getDisplayName(), EnumChatFormat.WHITE); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +old mode 100644 +new mode 100755 +index bfb2f1a1..45264871 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -1,5 +1,6 @@ + package org.bukkit.craftbukkit.entity; + ++import com.destroystokyo.paper.ComponentParser; + import com.destroystokyo.paper.Title; + import com.destroystokyo.paper.profile.CraftPlayerProfile; + import com.destroystokyo.paper.profile.PlayerProfile; +@@ -225,6 +226,30 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + } + ++ // Paper start: textual chat format ++ @Override ++ public void sendRichMessage(String richMessage) { ++ spigot().sendMessage(ComponentParser.parseFormat(richMessage)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, String... placeholders) { ++ spigot().sendMessage(ComponentParser.parseFormat(richMessage, placeholders)); ++ } ++ ++ @Override ++ public void sendRichMessage(String richMessage, Map placeholders) { ++ spigot().sendMessage(ComponentParser.parseFormat(richMessage, placeholders)); ++ } ++ ++ @Override ++ public void sendRichMessage(String[] richMessages) { ++ for (String message : richMessages) { ++ sendRichMessage(message); ++ } ++ } ++ // Paper end ++ + // Paper start + @Override + public void sendActionBar(String message) { +-- +2.17.1 +