diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 765f676a..26f113ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -18,6 +18,7 @@ import com.eu.habbo.plugin.events.emulator.EmulatorStartShutdownEvent; import com.eu.habbo.plugin.events.emulator.EmulatorStoppedEvent; import com.eu.habbo.threading.ThreadPooling; import com.eu.habbo.util.imager.badges.BadgeImager; +import com.eu.habbo.util.logback.ConsoleStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -368,34 +369,7 @@ public final class Emulator { } static boolean shouldStyleConsole(Map environment, boolean interactiveConsole, String osName, String styleProperty) { - String style = styleProperty == null ? "auto" : styleProperty.trim().toLowerCase(Locale.ROOT); - if (style.equals("ansi") || style.equals("color") || style.equals("colours") || style.equals("colors")) { - return true; - } - if (style.equals("plain") || style.equals("none") || style.equals("false") || style.equals("off")) { - return false; - } - if (!interactiveConsole) { - return false; - } - - Map env = environment == null ? Collections.emptyMap() : environment; - if (env.containsKey("NO_COLOR")) { - return false; - } - if (env.containsKey("WT_SESSION") || env.containsKey("ANSICON") || "ON".equalsIgnoreCase(env.get("ConEmuANSI"))) { - return true; - } - - String term = env.getOrDefault("TERM", ""); - if (term.equalsIgnoreCase("dumb")) { - return false; - } - if (!term.isBlank() && (term.contains("xterm") || term.contains("ansi") || term.contains("screen") || term.contains("tmux"))) { - return true; - } - - return osName == null || !osName.toLowerCase(Locale.ROOT).startsWith("windows"); + return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty); } private static String fit(String value, int width) { diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java new file mode 100644 index 00000000..43fa820d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLevelConverter.java @@ -0,0 +1,11 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +public class ConsoleLevelConverter extends ClassicConverter { + @Override + public String convert(ILoggingEvent event) { + return ConsoleStyle.level(event == null ? null : event.getLevel(), ConsoleStyle.isRuntimeEnabled()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java new file mode 100644 index 00000000..61b997d8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleLoggerConverter.java @@ -0,0 +1,11 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +public class ConsoleLoggerConverter extends ClassicConverter { + @Override + public String convert(ILoggingEvent event) { + return ConsoleStyle.logger(event == null ? "" : event.getLoggerName(), ConsoleStyle.isRuntimeEnabled()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java new file mode 100644 index 00000000..7c59c2aa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/logback/ConsoleStyle.java @@ -0,0 +1,106 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.Level; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +public final class ConsoleStyle { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BOLD = "\u001B[1m"; + private static final String ANSI_DIM = "\u001B[2m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_RED = "\u001B[31m"; + + private static final int LOGGER_WIDTH = 22; + + private ConsoleStyle() { + } + + public static boolean isRuntimeEnabled() { + return isEnabled( + System.getenv(), + System.console() != null, + System.getProperty("os.name", "Unknown"), + System.getProperty("habbo.console.style", "auto")); + } + + public static boolean isEnabled(Map environment, boolean interactiveConsole, String osName, String styleProperty) { + String style = styleProperty == null ? "auto" : styleProperty.trim().toLowerCase(Locale.ROOT); + if (style.equals("ansi") || style.equals("color") || style.equals("colours") || style.equals("colors")) { + return true; + } + if (style.equals("plain") || style.equals("none") || style.equals("false") || style.equals("off")) { + return false; + } + if (!interactiveConsole) { + return false; + } + + Map env = environment == null ? Collections.emptyMap() : environment; + if (env.containsKey("NO_COLOR")) { + return false; + } + if (env.containsKey("WT_SESSION") || env.containsKey("ANSICON") || "ON".equalsIgnoreCase(env.get("ConEmuANSI"))) { + return true; + } + + String term = env.getOrDefault("TERM", ""); + if (term.equalsIgnoreCase("dumb")) { + return false; + } + if (!term.isBlank() && (term.contains("xterm") || term.contains("ansi") || term.contains("screen") || term.contains("tmux"))) { + return true; + } + + return osName == null || !osName.toLowerCase(Locale.ROOT).startsWith("windows"); + } + + public static String level(Level level, boolean styled) { + String name = level == null ? "INFO" : level.toString(); + String plain = String.format("%-5s", name); + + if (!styled) { + return plain; + } + + if (Level.ERROR.equals(level)) { + return ANSI_BOLD + ANSI_RED + "[x] " + plain + ANSI_RESET; + } + if (Level.WARN.equals(level)) { + return ANSI_YELLOW + "[!] " + plain + ANSI_RESET; + } + if (Level.DEBUG.equals(level) || Level.TRACE.equals(level)) { + return ANSI_DIM + "[.] " + plain + ANSI_RESET; + } + + return ANSI_GREEN + "[i] " + plain + ANSI_RESET; + } + + public static String logger(String loggerName, boolean styled) { + String compact = compactLoggerName(loggerName); + String plain = fit(compact, LOGGER_WIDTH); + return styled ? ANSI_CYAN + plain + ANSI_RESET : plain; + } + + private static String compactLoggerName(String loggerName) { + if (loggerName == null || loggerName.isBlank()) { + return ""; + } + + int lastDot = loggerName.lastIndexOf('.'); + return lastDot >= 0 ? loggerName.substring(lastDot + 1) : loggerName; + } + + private static String fit(String value, int width) { + String safe = value == null ? "" : value; + if (safe.length() > width) { + return safe.substring(0, Math.max(0, width - 3)) + "..."; + } + + return String.format("%-" + width + "s", safe); + } +} diff --git a/Emulator/src/main/resources/logback.xml b/Emulator/src/main/resources/logback.xml index 5a972548..5235b60a 100644 --- a/Emulator/src/main/resources/logback.xml +++ b/Emulator/src/main/resources/logback.xml @@ -1,8 +1,11 @@ + + + - %d{HH:mm:ss.SSS} %-5level [%-12thread] %-22logger{0} | %msg%n + %d{HH:mm:ss.SSS} %morningstarLevel [%-12thread] %morningstarLogger | %msg%n diff --git a/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java b/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java index 0f044868..4e0d3724 100644 --- a/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java +++ b/Emulator/src/test/java/com/eu/habbo/ConsoleLogbackLayoutTest.java @@ -13,7 +13,8 @@ class ConsoleLogbackLayoutTest { void consolePatternKeepsStartupMessagesReadable() throws Exception { String logback = Files.readString(Path.of("src/main/resources/logback.xml")); - assertTrue(logback.contains("%-22logger{0}"), "console should show compact class names"); + assertTrue(logback.contains("morningstarLevel"), "console should use the adaptive level formatter"); + assertTrue(logback.contains("morningstarLogger"), "console should use the adaptive logger formatter"); assertTrue(logback.contains("| %msg%n"), "console should leave a clear message column"); assertFalse(logback.contains("%-36logger{36}"), "wide package loggers waste console space"); } diff --git a/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java b/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java new file mode 100644 index 00000000..c62a4e3b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/util/logback/ConsoleStyleTest.java @@ -0,0 +1,49 @@ +package com.eu.habbo.util.logback; + +import ch.qos.logback.classic.Level; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConsoleStyleTest { + @Test + void formatsLevelWithIconAndColorWhenStyled() { + String formatted = ConsoleStyle.level(Level.WARN, true); + + assertTrue(formatted.contains("\u001B[")); + assertTrue(formatted.contains("[!] WARN ")); + assertTrue(formatted.endsWith("\u001B[0m")); + } + + @Test + void formatsLevelAsPlainTextWhenNotStyled() { + assertEquals("WARN ", ConsoleStyle.level(Level.WARN, false)); + } + + @Test + void formatsLoggerWithColorWhenStyled() { + String formatted = ConsoleStyle.logger("com.eu.habbo.networking.Server", true); + + assertTrue(formatted.contains("\u001B[")); + assertTrue(formatted.contains("Server")); + assertTrue(formatted.endsWith("\u001B[0m")); + } + + @Test + void keepsLoggerPlainAndCompactWhenNotStyled() { + assertEquals("Server ", ConsoleStyle.logger("com.eu.habbo.networking.Server", false)); + } + + @Test + void honorsPlainOverrideEvenInWindowsTerminal() { + assertFalse(ConsoleStyle.isEnabled( + Map.of("WT_SESSION", "abc123"), + true, + "Windows 11", + "plain")); + } +}