diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28b30556e..b3713b588 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,7 @@ adventure-api = { group = "net.kyori", name = "adventure-api", version.ref = "ad adventure-serializer-gson = { group = "net.kyori", name = "adventure-text-serializer-gson", version.ref = "adventure" } adventure-serializer-legacy = { group = "net.kyori", name = "adventure-text-serializer-legacy", version.ref = "adventure" } adventure-serializer-plain = { group = "net.kyori", name = "adventure-text-serializer-plain", version.ref = "adventure" } +adventure-text-logger-slf4j = { group = "net.kyori", name = "adventure-text-logger-slf4j", version.ref = "adventure" } # Kotlin kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } @@ -100,6 +101,6 @@ jcstress-core = { group = "org.openjdk.jcstress", name = "jcstress-core", versio kotlin = ["kotlin-stdlib-jdk8", "kotlin-reflect"] flare = ["flare", "flare-fastutil"] -adventure = ["adventure-api", "adventure-serializer-gson", "adventure-serializer-legacy", "adventure-serializer-plain"] +adventure = ["adventure-api", "adventure-serializer-gson", "adventure-serializer-legacy", "adventure-serializer-plain", "adventure-text-logger-slf4j"] logging = ["tinylog-api", "tinylog-impl", "tinylog-slf4j"] terminal = ["jline", "jline-jansi"] diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 7b43a30b0..077c6f728 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -1,5 +1,6 @@ package net.minestom.server; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minestom.server.advancements.AdvancementManager; import net.minestom.server.adventure.bossbar.BossBarManager; import net.minestom.server.command.CommandManager; @@ -29,8 +30,6 @@ import net.minestom.server.world.biomes.BiomeManager; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnknownNullability; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; @@ -44,7 +43,7 @@ import java.net.SocketAddress; */ public final class MinecraftServer { - public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class); + public static final ComponentLogger LOGGER = ComponentLogger.logger(MinecraftServer.class); public static final String VERSION_NAME = "1.19.2"; public static final int PROTOCOL_VERSION = 760; diff --git a/src/main/java/net/minestom/server/adventure/MinestomAdventure.java b/src/main/java/net/minestom/server/adventure/MinestomAdventure.java index 503707ff8..27a51f5c9 100644 --- a/src/main/java/net/minestom/server/adventure/MinestomAdventure.java +++ b/src/main/java/net/minestom/server/adventure/MinestomAdventure.java @@ -21,7 +21,7 @@ public final class MinestomAdventure { * A codec to convert between strings and NBT. */ public static final Codec NBT_CODEC - = Codec.of(encoded -> new SNBTParser(new StringReader(encoded)).parse(), NBT::toSNBT); + = Codec.codec(encoded -> new SNBTParser(new StringReader(encoded)).parse(), NBT::toSNBT); /** * If components should be automatically translated in outgoing packets. diff --git a/src/main/java/net/minestom/server/adventure/provider/MinestomComponentLoggerProvider.java b/src/main/java/net/minestom/server/adventure/provider/MinestomComponentLoggerProvider.java new file mode 100644 index 000000000..8e28dcbe1 --- /dev/null +++ b/src/main/java/net/minestom/server/adventure/provider/MinestomComponentLoggerProvider.java @@ -0,0 +1,21 @@ +package net.minestom.server.adventure.provider; + +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; + +@SuppressWarnings("UnstableApiUsage") // we are permitted to provide this +public class MinestomComponentLoggerProvider implements ComponentLoggerProvider { + private static final LegacyComponentSerializer SERIALIZER = LegacyComponentSerializer.builder() + .character(LegacyComponentSerializer.SECTION_CHAR) + .flattener(MinestomFlattenerProvider.INSTANCE) + .hexColors() + .build(); + + @Override + public @NotNull ComponentLogger logger(@NotNull LoggerHelper helper, @NotNull String name) { + return helper.delegating(LoggerFactory.getLogger(name), SERIALIZER::serialize); + } +} diff --git a/src/main/java/net/minestom/server/command/ConsoleSender.java b/src/main/java/net/minestom/server/command/ConsoleSender.java index 4c115003b..3b31ea724 100644 --- a/src/main/java/net/minestom/server/command/ConsoleSender.java +++ b/src/main/java/net/minestom/server/command/ConsoleSender.java @@ -3,12 +3,10 @@ package net.minestom.server.command; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minestom.server.permission.Permission; import net.minestom.server.tag.TagHandler; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -17,8 +15,7 @@ import java.util.concurrent.CopyOnWriteArraySet; * Represents the console when sending a command to the server. */ public class ConsoleSender implements CommandSender { - private static final PlainTextComponentSerializer PLAIN_SERIALIZER = PlainTextComponentSerializer.plainText(); - private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleSender.class); + private static final ComponentLogger LOGGER = ComponentLogger.logger(ConsoleSender.class); private final Set permissions = new CopyOnWriteArraySet<>(); private final TagHandler tagHandler = TagHandler.newHandler(); @@ -30,8 +27,7 @@ public class ConsoleSender implements CommandSender { @Override public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { - // we don't use the serializer here as we just need the plain text of the message - this.sendMessage(PLAIN_SERIALIZER.serialize(message)); + LOGGER.info(message); } @NotNull diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java index 2ecc9e8fc..020f229cb 100644 --- a/src/main/java/net/minestom/server/extensions/Extension.java +++ b/src/main/java/net/minestom/server/extensions/Extension.java @@ -1,10 +1,10 @@ package net.minestom.server.extensions; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; import java.io.IOException; import java.io.InputStream; @@ -64,7 +64,7 @@ public abstract class Extension { * @return The logger for the extension */ @NotNull - public Logger getLogger() { + public ComponentLogger getLogger() { return getExtensionClassLoader().getLogger(); } diff --git a/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java b/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java index 1c212bd9a..dd96f46af 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionClassLoader.java @@ -1,11 +1,10 @@ package net.minestom.server.extensions; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minestom.server.MinecraftServer; import net.minestom.server.event.Event; import net.minestom.server.event.EventNode; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.InputStream; import java.net.URL; @@ -17,7 +16,7 @@ public final class ExtensionClassLoader extends URLClassLoader { private final List children = new ArrayList<>(); private final DiscoveredExtension discoveredExtension; private EventNode eventNode; - private Logger logger; + private ComponentLogger logger; public ExtensionClassLoader(String name, URL[] urls, DiscoveredExtension discoveredExtension) { super("Ext_" + name, urls, MinecraftServer.class.getClassLoader()); @@ -77,9 +76,9 @@ public final class ExtensionClassLoader extends URLClassLoader { return eventNode; } - public Logger getLogger() { + public ComponentLogger getLogger() { if (logger == null) { - logger = LoggerFactory.getLogger(discoveredExtension.getName()); + logger = ComponentLogger.logger(discoveredExtension.getName()); } return logger; } diff --git a/src/main/java/net/minestom/server/terminal/MinestomConsoleWriter.java b/src/main/java/net/minestom/server/terminal/MinestomConsoleWriter.java index 10ad3f3d9..0c19faf60 100644 --- a/src/main/java/net/minestom/server/terminal/MinestomConsoleWriter.java +++ b/src/main/java/net/minestom/server/terminal/MinestomConsoleWriter.java @@ -1,5 +1,6 @@ package net.minestom.server.terminal; +import org.fusesource.jansi.AnsiConsole; import org.tinylog.core.LogEntry; import org.tinylog.writers.AbstractFormatPatternWriter; @@ -14,10 +15,12 @@ public final class MinestomConsoleWriter extends AbstractFormatPatternWriter { @Override public void write(LogEntry logEntry) throws Exception { + String rendered = render(logEntry); + String formatted = TerminalColorConverter.format(rendered); if (reader != null) { - reader.printAbove(render(logEntry)); + reader.printAbove(formatted); } else { - System.out.print(render(logEntry)); + AnsiConsole.out().print(formatted); } } diff --git a/src/main/java/net/minestom/server/terminal/TerminalColorConverter.java b/src/main/java/net/minestom/server/terminal/TerminalColorConverter.java new file mode 100644 index 000000000..fea9241e5 --- /dev/null +++ b/src/main/java/net/minestom/server/terminal/TerminalColorConverter.java @@ -0,0 +1,102 @@ +package net.minestom.server.terminal; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.minestom.server.utils.PropertyUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A string converter to convert a string to an ansi-colored one. + * + * @see TerminalConsoleAppender + * @see Paper + */ +final class TerminalColorConverter { + private static final boolean SUPPORT_HEX_COLOR = PropertyUtils.getBoolean("minestom.terminal.support-hex-color", true); + private static final boolean SUPPORT_COLOR = PropertyUtils.getBoolean("minestom.terminal.support-color", true); + + private static final String RGB_ANSI = "\u001B[38;2;%d;%d;%dm"; + private static final String ANSI_RESET = "\u001B[m"; + private static final String LOOKUP = "0123456789abcdefklmnor"; + private static final String[] ANSI_CODES = new String[]{ + getAnsiColor(NamedTextColor.BLACK, "\u001B[0;30m"), // Black §0 + getAnsiColor(NamedTextColor.DARK_BLUE, "\u001B[0;34m"), // Dark Blue §1 + getAnsiColor(NamedTextColor.DARK_GREEN, "\u001B[0;32m"), // Dark Green §2 + getAnsiColor(NamedTextColor.DARK_AQUA, "\u001B[0;36m"), // Dark Aqua §3 + getAnsiColor(NamedTextColor.DARK_RED, "\u001B[0;31m"), // Dark Red §4 + getAnsiColor(NamedTextColor.DARK_PURPLE, "\u001B[0;35m"), // Dark Purple §5 + getAnsiColor(NamedTextColor.GOLD, "\u001B[0;33m"), // Gold §6 + getAnsiColor(NamedTextColor.GRAY, "\u001B[0;37m"), // Gray §7 + getAnsiColor(NamedTextColor.DARK_GRAY, "\u001B[0;30;1m"), // Dark Gray §8 + getAnsiColor(NamedTextColor.BLUE, "\u001B[0;34;1m"), // Blue §9 + getAnsiColor(NamedTextColor.GREEN, "\u001B[0;32;1m"), // Green §a + getAnsiColor(NamedTextColor.AQUA, "\u001B[0;36;1m"), // Aqua §b + getAnsiColor(NamedTextColor.RED, "\u001B[0;31;1m"), // Red §c + getAnsiColor(NamedTextColor.LIGHT_PURPLE, "\u001B[0;35;1m"), // Light Purple §d + getAnsiColor(NamedTextColor.YELLOW, "\u001B[0;33;1m"), // Yellow §e + getAnsiColor(NamedTextColor.WHITE, "\u001B[0;37;1m"), // White §f + "\u001B[5m", // Obfuscated §k + "\u001B[1m", // Bold §l + "\u001B[9m", // Strikethrough §m + "\u001B[4m", // Underline §n + "\u001B[3m", // Italic §o + ANSI_RESET, // Reset §r + }; + private static final Pattern RGB_PATTERN = Pattern.compile(LegacyComponentSerializer.SECTION_CHAR + "#([\\da-fA-F]{6})"); + private static final Pattern NAMED_PATTERN = Pattern.compile(LegacyComponentSerializer.SECTION_CHAR + "([\\da-fk-orA-FK-OR])"); + + private TerminalColorConverter() { + } + + private static String getAnsiColor(NamedTextColor color, String fallback) { + return getAnsiColorFromHexColor(color.value(), fallback); + } + + private static String getAnsiColorFromHexColor(int color, String fallback) { + return SUPPORT_HEX_COLOR ? String.format(RGB_ANSI, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) : fallback; + } + + private static String getAnsiColorFromHexColor(int color) { + return getAnsiColorFromHexColor(color, ""); + } + + /** + * Format the colored string to an ansi-colored one. + * + * @param string the string to format + * @return the formatted string + */ + public static String format(String string) { + if (string.indexOf(LegacyComponentSerializer.SECTION_CHAR) == -1) { + return string; + } + + string = RGB_PATTERN.matcher(string).replaceAll(match -> { + if (SUPPORT_COLOR) { + String hex = match.group(1); + return getAnsiColorFromHexColor(Integer.parseInt(hex, 16)); + } else { + return ""; + } + }); + + Matcher matcher = NAMED_PATTERN.matcher(string); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + int format = LOOKUP.indexOf(Character.toLowerCase(matcher.group().charAt(1))); + if (format != -1) { + matcher.appendReplacement(builder, SUPPORT_COLOR ? ANSI_CODES[format] : ""); + } else { + matcher.appendReplacement(builder, matcher.group()); + } + } + matcher.appendTail(builder); + + if (SUPPORT_COLOR) { + builder.append(ANSI_RESET); + } + return builder.toString(); + } +} diff --git a/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider new file mode 100644 index 000000000..7521bc161 --- /dev/null +++ b/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider @@ -0,0 +1 @@ +net.minestom.server.adventure.provider.MinestomComponentLoggerProvider \ No newline at end of file diff --git a/src/test/java/net/minestom/server/terminal/TerminalColorConverterTest.java b/src/test/java/net/minestom/server/terminal/TerminalColorConverterTest.java new file mode 100644 index 000000000..a4e527d15 --- /dev/null +++ b/src/test/java/net/minestom/server/terminal/TerminalColorConverterTest.java @@ -0,0 +1,27 @@ +package net.minestom.server.terminal; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TerminalColorConverterTest { + @Test + void testFormat() { + String input = "§c§lHello §r§b§lWorld"; + String expected = "\u001B[38;2;255;85;85m\u001B[1mHello \u001B[m\u001B[38;2;85;255;255m\u001B[1mWorld\u001B[m"; + String actual = TerminalColorConverter.format(input); + assertEquals(expected, actual); + } + + @Test + void testComponentFormat() { + Component input = Component.text("Hello World").color(NamedTextColor.RED).decorate(TextDecoration.BOLD); + String expected = "\u001B[38;2;255;85;85m\u001B[1mHello World\u001B[m"; + String actual = TerminalColorConverter.format(LegacyComponentSerializer.legacySection().serialize(input)); + assertEquals(expected, actual); + } +} \ No newline at end of file