Merge pull request #177 from simoleo89/style/startup-console

style(startup): console banner/splash/colors
This commit is contained in:
DuckieTM
2026-06-15 07:20:23 +02:00
committed by GitHub
11 changed files with 430 additions and 24 deletions
+110 -18
View File
@@ -18,6 +18,8 @@ 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.fusesource.jansi.AnsiConsole;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +40,12 @@ public final class Emulator {
private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class);
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
private static final String ANSI_RESET = "\u001B[0m";
private static final String ANSI_BOLD = "\u001B[1m";
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_DIM = "\u001B[2m";
// Fallback version, only used when running outside a packaged jar (e.g. from
// the IDE). In production the version comes from the jar manifest below.
@@ -65,7 +73,6 @@ public final class Emulator {
"██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝███████║ ██║ ██║ ██║██║ ██║\n" +
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\n" +
"Still Rocking in 2026.\n";
public static String build = "";
public static long buildTimestamp = -1L;
@@ -104,14 +111,12 @@ public final class Emulator {
public static void main(String[] args) throws Exception {
try {
if (OS_NAME.startsWith("Windows") && !CLASS_PATH.contains("idea_rt.jar")) {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
appender.stop();
appender.setWithJansi(true);
appender.start();
}
boolean styledConsole = shouldStyleConsole(
System.getenv(),
System.console() != null,
OS_NAME,
System.getProperty("habbo.console.style", "auto"));
configureAnsiConsole(styledConsole);
Locale.setDefault(Locale.of("en"));
setBuild();
@@ -119,7 +124,7 @@ public final class Emulator {
ConsoleCommand.load();
Emulator.logging = new Logging();
System.out.println(logo);
System.out.println(startupHero(styledConsole));
long startTime = System.nanoTime();
@@ -153,14 +158,10 @@ public final class Emulator {
Emulator.config.register("camera.price.points.type", "5");
Emulator.config.register("camera.render.delay", "5");
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
Emulator.config.register("gui.enabled", "0");
Emulator.config.register("gui.autostart.enabled", "0");
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
System.out.println();
LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, ");
System.out.println();
LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral.");
LOGGER.info("Version: {}", version);
LOGGER.info("Build: {}", build);
LOGGER.info("Build Timestamp: {} [{}]", formatBuildTimestamp(buildTimestamp, hotelTimezoneId), hotelTimezoneId);
System.out.println(startupCard(hotelTimezoneId));
Emulator.texts.register("camera.permission", "You don't have permission to use the camera!");
Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture.");
Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*");
@@ -198,7 +199,7 @@ public final class Emulator {
Emulator.isReady = true;
Emulator.timeStarted = getIntUnixTimestamp();
if (Emulator.getConfig().getBoolean("gui.enabled", true)) {
if (shouldLaunchGui()) {
EmulatorDashboard.launch();
}
@@ -310,6 +311,97 @@ public final class Emulator {
return -1L;
}
static String startupCard(String hotelTimezoneId) {
return "\n" +
"+----------------------------------------------------------------+\n" +
"| Arcturus Morningstar Extended |\n" +
"| Source : github.com/duckietm/Arcturus-Morningstar-Extended |\n" +
"| Scope : Educational open-source fork by TheGeneral |\n" +
"| Version: " + version + "\n" +
"| Build : " + build + "\n" +
"| Time : " + formatBuildTimestamp(buildTimestamp, hotelTimezoneId) + " [" + hotelTimezoneId + "]\n" +
"+----------------------------------------------------------------+\n";
}
static String startupHero() {
return startupHero(false);
}
static String startupHero(boolean styled) {
if (styled) {
return "\n" +
ANSI_CYAN +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
ANSI_RESET +
"\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_BOLD + ANSI_GREEN + "[OK] MORNINGSTAR EXTENDED" + ANSI_RESET + fit("", 50) + " |\n" +
"| " + ANSI_DIM + "Arcturus game server runtime" + ANSI_RESET + fit("", 48) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n" +
"| " + ANSI_YELLOW + "[VER]" + ANSI_RESET + " Version : " + fit(version, 57) + " |\n" +
"| " + ANSI_YELLOW + "[BLD]" + ANSI_RESET + " Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 57) + " |\n" +
"| " + ANSI_YELLOW + "[JVM]" + ANSI_RESET + " Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / styled console output", 57) + " |\n" +
ANSI_DIM + "+------------------------------------------------------------------------------+" + ANSI_RESET + "\n";
}
return "\n" +
" __ __ ___ ____ _ _ ___ _ _ ____ ____ _____ _ ____ \n" +
" | \\/ |/ _ \\| _ \\| \\ | |_ _| \\ | |/ ___/ ___|_ _|/ \\ | _ \\ \n" +
" | |\\/| | | | | |_) | \\| || || \\| | | _\\___ \\ | | / _ \\ | |_) |\n" +
" | | | | |_| | _ <| |\\ || || |\\ | |_| |___) || |/ ___ \\| _ < \n" +
" |_| |_|\\___/|_| \\_\\_| \\_|___|_| \\_|\\____|____/ |_/_/ \\_\\_| \\_\\\n" +
"\n" +
"+------------------------------------------------------------------------------+\n" +
"| MORNINGSTAR EXTENDED |\n" +
"| Arcturus game server runtime |\n" +
"+------------------------------------------------------------------------------+\n" +
"| Version : " + fit(version, 63) + " |\n" +
"| Build : " + fit(build.isBlank() ? "UNKNOWN" : build, 63) + " |\n" +
"| Runtime : " + fit("Java " + System.getProperty("java.version", "unknown") + " / universal console output", 63) + " |\n" +
"+------------------------------------------------------------------------------+\n";
}
static boolean shouldStyleConsole(Map<String, String> environment, boolean interactiveConsole, String osName, String styleProperty) {
return ConsoleStyle.isEnabled(environment, interactiveConsole, osName, styleProperty);
}
static void configureAnsiConsole(boolean styledConsole) {
if (!styledConsole || !OS_NAME.startsWith("Windows") || CLASS_PATH.contains("idea_rt.jar")) {
return;
}
try {
AnsiConsole.systemInstall();
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ConsoleAppender<ILoggingEvent> appender = (ConsoleAppender<ILoggingEvent>) root.getAppender("Console");
if (appender != null) {
appender.stop();
appender.setWithJansi(true);
appender.start();
}
} catch (Throwable e) {
LOGGER.debug("Unable to install Jansi console bridge; continuing with raw console output.", e);
}
}
static boolean shouldLaunchGui() {
return Emulator.getConfig() != null && Emulator.getConfig().getBoolean("gui.autostart.enabled", false);
}
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);
}
private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) {
if (buildTimestamp <= 0) {
return "UNKNOWN";
@@ -90,7 +90,13 @@ public class InfostandBackgroundManager {
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards, {} borders from infostand_backgrounds.",
LOGGER.info(summary(
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size(),
this.entries.get(Category.BORDER).size()));
LOGGER.debug("Infostand Background Manager assets: {} bg, {} stands, {} overlays, {} cards, {} borders",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
@@ -101,6 +107,11 @@ public class InfostandBackgroundManager {
}
}
static String summary(int backgrounds, int stands, int overlays, int cards, int borders) {
int total = backgrounds + stands + overlays + cards + borders;
return String.format("Infostand Background Manager -> Loaded! (%d assets)", total);
}
public boolean canUse(Habbo habbo, Category category, int id) {
if (id == 0) return true;
if (!this.enforce) return true;
@@ -85,10 +85,10 @@ public abstract class Server {
}
if (!channelFuture.isSuccess()) {
LOGGER.info("Failed to connect to the host ({}:{})@{}", this.host, this.port, this.name);
LOGGER.info("Failed to start {} on {}:{}", this.name, this.host, this.port);
System.exit(0);
} else {
LOGGER.info("Started GameServer on {}:{}@{}", this.host, this.port, this.name);
LOGGER.info("Started {} on {}:{}", this.name, this.host, this.port);
}
}
@@ -100,7 +100,7 @@ public abstract class Server {
} catch(InterruptedException e) {
LOGGER.error("Exception during {} shutdown... HARD STOP", this.name, e);
}
LOGGER.info("GameServer Stopped!");
LOGGER.info("Stopped {}", this.name);
}
public ServerBootstrap getServerBootstrap() {
@@ -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());
}
}
@@ -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());
}
}
@@ -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<String, String> 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<String, String> 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);
}
}
+5 -2
View File
@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="morningstarLevel" class="com.eu.habbo.util.logback.ConsoleLevelConverter" />
<conversionRule conversionWord="morningstarLogger" class="com.eu.habbo.util.logback.ConsoleLoggerConverter" />
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-14thread] %-5level %-36logger{36} - %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} %morningstarLevel [%-12thread] %morningstarLogger | %msg%n</pattern>
</encoder>
</appender>
@@ -65,4 +68,4 @@
<appender-ref ref="FileErrors" />
<appender-ref ref="FileErrorsSql" />
</root>
</configuration>
</configuration>
@@ -0,0 +1,21 @@
package com.eu.habbo;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConsoleLogbackLayoutTest {
@Test
void consolePatternKeepsStartupMessagesReadable() throws Exception {
String logback = Files.readString(Path.of("src/main/resources/logback.xml"));
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");
}
}
@@ -0,0 +1,88 @@
package com.eu.habbo;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EmulatorStartupConsoleTest {
@Test
void startupHeroUsesUniversalAsciiLayout() {
String hero = Emulator.startupHero();
assertTrue(hero.contains("__ __ ___ ____"));
assertTrue(hero.contains("MORNINGSTAR EXTENDED"));
assertTrue(hero.contains("Version"));
assertTrue(hero.contains("Build"));
assertFalse(hero.contains("\u001B["), "startup hero must not require ANSI support");
}
@Test
void startupHeroCanRenderStyledLayoutWhenAnsiIsAvailable() {
String hero = Emulator.startupHero(true);
assertTrue(hero.contains("\u001B["), "styled hero should include ANSI colors");
assertTrue(hero.contains("[OK] MORNINGSTAR EXTENDED"));
assertTrue(hero.contains("[JVM]"));
assertTrue(hero.endsWith("\u001B[0m\n"), "styled hero should reset terminal attributes");
}
@Test
void consoleStyleAutoDetectsWindowsTerminal() {
assertTrue(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
true,
"Windows 11",
"auto"));
}
@Test
void consoleStyleFallsBackWhenOutputIsNotInteractive() {
assertFalse(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
false,
"Windows 11",
"auto"));
}
@Test
void consoleStyleCanBeForcedOff() {
assertFalse(Emulator.shouldStyleConsole(
Map.of("WT_SESSION", "abc123"),
true,
"Windows 11",
"plain"));
}
@Test
void windowsAnsiModeInstallsJansiBeforePrintingStartupHero() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
assertTrue(source.contains("AnsiConsole.systemInstall()"),
"forced ANSI mode must install the Jansi bridge for Windows CMD/System.out");
assertTrue(source.contains("configureAnsiConsole(styledConsole)"),
"console bridge must be configured before startupHero is printed");
assertTrue(source.indexOf("configureAnsiConsole(styledConsole)") < source.indexOf("startupHero(styledConsole)"),
"Jansi must be installed before writing ANSI startup output");
}
@Test
void registersGuiEnabledBeforeReadingIt() throws Exception {
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
assertTrue(source.contains("register(\"gui.enabled\", \"0\")"),
"gui.enabled must be registered disabled by default so it does not log missing config errors or start the UI unexpectedly");
assertTrue(source.contains("register(\"gui.autostart.enabled\", \"0\")"),
"GUI autostart must use a new disabled-by-default key so old gui.enabled=1 settings do not launch the current UI");
assertTrue(source.indexOf("register(\"gui.autostart.enabled\", \"0\")") < source.indexOf("shouldLaunchGui()"),
"GUI autostart must be registered before the launch decision");
assertFalse(source.contains("getBoolean(\"gui.enabled\", true)"),
"GUI must not use a true fallback");
assertFalse(source.contains("getBoolean(\"gui.enabled\", false)"),
"legacy gui.enabled must not control startup anymore");
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.users.infostand;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InfostandBackgroundManagerTest {
@Test
void summaryKeepsStartupLogCompact() {
assertEquals(
"Infostand Background Manager -> Loaded! (260 assets)",
InfostandBackgroundManager.summary(188, 22, 9, 16, 25));
}
}
@@ -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"));
}
}