diff --git a/src/main/java/low/citory/ExampleUsage.java b/src/main/java/low/citory/ExampleUsage.java index 7077349..9f03075 100644 --- a/src/main/java/low/citory/ExampleUsage.java +++ b/src/main/java/low/citory/ExampleUsage.java @@ -28,6 +28,7 @@ public static void main(String[] args) { // Colors work only with "System.out.println()" // Idk how to fix colors with log4j LOGGER.info("Server MOTD: {}", server.getAnsiMotd()); + LOGGER.info("Server Raw MOTD: {}", server.getRawMotd()); LOGGER.info("Players online: {}/{}", server.getPlayersOnline(), server.getMaxPlayers()); LOGGER.info("Server ping: {}ms", server.getServerPing()); } diff --git a/src/main/java/low/citory/MinecraftPinger.java b/src/main/java/low/citory/MinecraftPinger.java index 5e5b51f..b246ebe 100644 --- a/src/main/java/low/citory/MinecraftPinger.java +++ b/src/main/java/low/citory/MinecraftPinger.java @@ -8,8 +8,7 @@ public final class MinecraftPinger { private boolean serverStatus; private String serverVersion; private int protocolVersion; - private String serverMOTD; - private String strippedMOTD; + private String rawMOTD; private String ansiMOTD; private int playersOnline; private int maxPlayers; @@ -44,11 +43,7 @@ public void setProtocol(int protocol) { } public void setRawMotd(String rawMotd) { - this.serverMOTD = rawMotd; - } - - public void setStrippedMotd(String strippedMotd) { - this.strippedMOTD = strippedMotd; + this.rawMOTD = rawMotd; } public void setAnsiMotd(String ansiMotd) { @@ -97,16 +92,8 @@ public int getProtocolVersion() { * Get server motd * @return string */ - public String getMotd() { - return this.serverMOTD; - } - - /** - * Get stripped motd - * @return string - */ - public String getStrippedMotd() { - return this.strippedMOTD; + public String getRawMotd() { + return this.rawMOTD; } /** diff --git a/src/main/java/low/citory/util/AbstractPinger.java b/src/main/java/low/citory/util/AbstractPinger.java index ad34370..53d32b2 100644 --- a/src/main/java/low/citory/util/AbstractPinger.java +++ b/src/main/java/low/citory/util/AbstractPinger.java @@ -10,8 +10,8 @@ protected static void setServerData( pinger.setVersion(version); pinger.setProtocol(protocol); pinger.setRawMotd(rawMotd); - pinger.setStrippedMotd(TextColorizer.stripFormatting(rawMotd)); - pinger.setAnsiMotd(TextColorizer.convertToAnsi(rawMotd)); + pinger.setAnsiMotd(MinecraftANSI.toAnsi(JsonToANSI.convert(rawMotd))); + //Don't know how to convert hex color into ANSI plz finish this. pinger.setPlayersOnline(playersOnline); pinger.setMaxPlayers(maxPlayers); pinger.setServerPing(ping); diff --git a/src/main/java/low/citory/util/JsonToANSI.java b/src/main/java/low/citory/util/JsonToANSI.java new file mode 100644 index 0000000..4195277 --- /dev/null +++ b/src/main/java/low/citory/util/JsonToANSI.java @@ -0,0 +1,57 @@ +package low.citory.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class JsonToANSI { + + public static String convert(String rawMotd) { + + try { + JsonElement root = JsonParser.parseString(rawMotd); + StringBuilder result = new StringBuilder(); + parseElement(root, result); + return result.toString(); + } catch (Exception e) { + e.printStackTrace(); + return "A Minecraft Server"; + } + } + + private static void parseElement(JsonElement element, StringBuilder sb) { + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + + // 添加 text 字段 + if (obj.has("text")) { + String text = obj.get("text").getAsString(); + sb.append(text); + } + + // 递归处理 extra 数组 + if (obj.has("extra")) { + JsonArray extra = obj.getAsJsonArray("extra"); + for (JsonElement extraElement : extra) { + parseElement(extraElement, sb); // 递归处理每个 extra 元素 + } + } + + } else if (element.isJsonArray()) { + // 处理数组(如顶层是数组) + for (JsonElement item : element.getAsJsonArray()) { + parseElement(item, sb); + } + } else if (element.isJsonPrimitive()) { + // 处理原始类型:字符串 + String value = element.getAsString(); + if ("\n".equals(value)) { + sb.append("\n"); + } else { + sb.append(value); + } + } + // 忽略 null 或其他类型 + } +} diff --git a/src/main/java/low/citory/util/MinecraftANSI.java b/src/main/java/low/citory/util/MinecraftANSI.java new file mode 100644 index 0000000..22f0003 --- /dev/null +++ b/src/main/java/low/citory/util/MinecraftANSI.java @@ -0,0 +1,46 @@ +package low.citory.util; + +public class MinecraftANSI { + + /** + * 将 Minecraft 的 § 颜色代码转换为 ANSI 转义序列 + * 用于在支持 ANSI 的终端中显示颜色 + */ + public static String toAnsi(String text) { + if (text == null) return null; + + StringBuilder result = new StringBuilder(); + boolean inColor = false; + boolean inFormat = false; + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '§' && i + 1 < text.length()) { + char code = text.charAt(i + 1); + MinecraftColors color = MinecraftColors.fromChar(code); + if (color != null) { + result.append(color.getAnsiCode()); + inColor = true; + i++; // 跳过 color code + continue; + } + + MinecraftFormat format = MinecraftFormat.fromChar(code); + if (format != null) { + result.append(format.getAnsiCode()); + inFormat = true; + i++; // 跳过 format code + continue; + } + } + result.append(c); + } + + // 最后重置所有样式 + if (inColor || inFormat) { + result.append(MinecraftColors.RESET.getAnsiCode()); + } + + return result.toString(); + } +} diff --git a/src/main/java/low/citory/util/MinecraftColors.java b/src/main/java/low/citory/util/MinecraftColors.java index 328e56e..d7a0030 100644 --- a/src/main/java/low/citory/util/MinecraftColors.java +++ b/src/main/java/low/citory/util/MinecraftColors.java @@ -1,23 +1,23 @@ package low.citory.util; -enum MinecraftColors { - V_0("\u001B[30m"), // BLACK - V_1("\u001B[34m"), // DARK BLUE - V_2("\u001B[32m"), // DARK GREEN - V_3("\u001B[36m"), // DARK AQUA - V_4("\u001B[31m"), // DARK RED - V_5("\u001B[35m"), // DARK PURPLE - V_6("\u001B[33m"), // GOLD - V_7("\u001B[37m"), // GRAY - V_8("\u001B[90m"), // DARK GRAY - V_9("\u001B[94m"), // BLUE - V_A("\u001B[92m"), // GREEN - V_B("\u001B[96m"), // AQUA - V_C("\u001B[91m"), // RED - V_D("\u001B[95m"), // PURPLE - V_E("\u001B[93m"), // YELLOW - V_F("\u001B[97m"), // WHITE - V_R("\u001B[0m"); // RESET +public enum MinecraftColors { + BLACK ("\u001B[30m"), // §0 + DARK_BLUE ("\u001B[34m"), // §1 + DARK_GREEN ("\u001B[32m"), // §2 + DARK_AQUA ("\u001B[36m"), // §3 + DARK_RED ("\u001B[31m"), // §4 + DARK_PURPLE("\u001B[35m"), // §5 + GOLD ("\u001B[33m"), // §6 + GRAY ("\u001B[37m"), // §7 + DARK_GRAY ("\u001B[90m"), // §8 + BLUE ("\u001B[94m"), // §9 + GREEN ("\u001B[92m"), // §a + AQUA ("\u001B[96m"), // §b + RED ("\u001B[91m"), // §c + LIGHT_PURPLE("\u001B[95m"), // §d + YELLOW ("\u001B[93m"), // §e + WHITE ("\u001B[97m"), // §f + RESET ("\u001B[0m"); // §r private final String ansiCode; @@ -33,4 +33,27 @@ public String getAnsiCode() { public String toString() { return ansiCode; } + + public static MinecraftColors fromChar(char code) { + return switch (Character.toLowerCase(code)) { + case '0' -> BLACK; + case '1' -> DARK_BLUE; + case '2' -> DARK_GREEN; + case '3' -> DARK_AQUA; + case '4' -> DARK_RED; + case '5' -> DARK_PURPLE; + case '6' -> GOLD; + case '7' -> GRAY; + case '8' -> DARK_GRAY; + case '9' -> BLUE; + case 'a' -> GREEN; + case 'b' -> AQUA; + case 'c' -> RED; + case 'd' -> LIGHT_PURPLE; + case 'e' -> YELLOW; + case 'f' -> WHITE; + case 'r' -> RESET; + default -> null; + }; + } } diff --git a/src/main/java/low/citory/util/MinecraftFormat.java b/src/main/java/low/citory/util/MinecraftFormat.java new file mode 100644 index 0000000..4595c3d --- /dev/null +++ b/src/main/java/low/citory/util/MinecraftFormat.java @@ -0,0 +1,30 @@ +package low.citory.util; + +public enum MinecraftFormat { + BOLD ("\u001B[1m"), // §l + ITALIC ("\u001B[3m"), // §o + UNDERLINE ("\u001B[4m"), // §n + STRIKETHROUGH ("\u001B[9m"), // §m + RESET ("\u001B[0m"); // §r + + private final String ansiCode; + + MinecraftFormat(String ansiCode) { + this.ansiCode = ansiCode; + } + + public String getAnsiCode() { + return ansiCode; + } + + public static MinecraftFormat fromChar(char code) { + return switch (Character.toLowerCase(code)) { + case 'l' -> BOLD; + case 'o' -> ITALIC; + case 'n' -> UNDERLINE; + case 'm' -> STRIKETHROUGH; + case 'r' -> RESET; + default -> null; + }; + } +} diff --git a/src/main/java/low/citory/util/PacketUtil.java b/src/main/java/low/citory/util/PacketUtil.java index 42b6dcd..95eaa8e 100644 --- a/src/main/java/low/citory/util/PacketUtil.java +++ b/src/main/java/low/citory/util/PacketUtil.java @@ -13,12 +13,14 @@ public final class PacketUtil { // Writers - public static void writeVarInt(DataOutputStream outputStream, int value) throws IOException { + public static void writeVarInt(DataOutputStream out, int value) throws IOException { while (true) { - if ((value & 0xFFFFFF80) == 0) outputStream.writeByte(value); - if ((value & 0xFFFFFF80) == 0) return; + if ((value & 0xFFFFFF80) == 0) { + out.writeByte(value); + return; // 👈 必须 return! + } - outputStream.writeByte(value & 0x7F | 0x80); + out.writeByte((value & 0x7F) | 0x80); value >>>= 7; } } diff --git a/src/main/java/low/citory/util/TextColorizer.java b/src/main/java/low/citory/util/TextColorizer.java index a097643..df0c342 100644 --- a/src/main/java/low/citory/util/TextColorizer.java +++ b/src/main/java/low/citory/util/TextColorizer.java @@ -1,30 +1,31 @@ package low.citory.util; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - public final class TextColorizer { - private static final Pattern PATTERN = Pattern.compile("§([0-9a-fk-or])", Pattern.CASE_INSENSITIVE); - - public static String convertToAnsi(String string) { - Matcher matcher = PATTERN.matcher(string+"§r"); - StringBuilder finallyString = new StringBuilder(); + private static final String COLOR_CODE_PATTERN = "§[0-9a-fk-or]"; - while (matcher.find()) { - char color = matcher.group(1).charAt(0); - String colorCode = "V_"+Character.toUpperCase(color); - String code = Optional.of(MinecraftColors.valueOf(colorCode)). - map(MinecraftColors::getAnsiCode).orElse(""); + /** + * 移除所有 Minecraft 颜色和格式代码 (§r, §a, §l 等) + */ + public static String stripFormatting(String text) { + return stripColorCodes(text); + } - matcher.appendReplacement(finallyString, Matcher.quoteReplacement(code)); - } - matcher.appendTail(finallyString); - return finallyString.toString(); + /** + * 移除所有 Minecraft 颜色和格式代码 + * 此方法可在类内部或外部调用 + */ + public static String stripColorCodes(String text) { + if (text == null) return null; + return text.replaceAll(COLOR_CODE_PATTERN, ""); } - public static String stripFormatting(String text) { - return text.replaceAll("§[0-9a-fk-or]", ""); + /** + * 将 Minecraft 的 § 颜色代码转换为 ANSI 转义序列,用于终端着色 + * 使用 MinecraftANSI 工具类 + */ + public static String convertToAnsi(String string) { + if (string == null) return null; + return MinecraftANSI.toAnsi(string); } } diff --git a/src/main/java/low/citory/versions/ModernPing.java b/src/main/java/low/citory/versions/ModernPing.java index 2f5e7a4..68805ee 100644 --- a/src/main/java/low/citory/versions/ModernPing.java +++ b/src/main/java/low/citory/versions/ModernPing.java @@ -32,7 +32,7 @@ public static boolean pingServer(MinecraftPinger pinger, String serverIP, int se DataOutputStream handshake = new DataOutputStream(rawHandshake); handshake.writeByte(0x00); - PacketUtil.writeVarInt(handshake, -1); + PacketUtil.writeVarInt(handshake, 765); PacketUtil.writeString(handshake, serverIP); handshake.writeShort(serverPORT); PacketUtil.writeVarInt(handshake, 1); @@ -40,10 +40,18 @@ public static boolean pingServer(MinecraftPinger pinger, String serverIP, int se PacketUtil.writeVarInt(outputStream, rawHandshake.size()); outputStream.write(rawHandshake.toByteArray()); - PacketUtil.writeVarInt(outputStream, 1); - PacketUtil.writeVarInt(outputStream, 0); + ByteArrayOutputStream statusReq = new ByteArrayOutputStream(); + DataOutputStream tmp = new DataOutputStream(statusReq); + PacketUtil.writeVarInt(tmp, 0x00); // Status Request packet ID + byte[] reqBytes = statusReq.toByteArray(); + + PacketUtil.writeVarInt(outputStream, reqBytes.length); + outputStream.write(reqBytes); outputStream.flush(); + // Step 1: 读取整个包的长度(VarInt) + int packetLength = PacketUtil.readVarInt(inputStream); + // Step 2: 读取 Packet ID int packetId = PacketUtil.readVarInt(inputStream); if (packetId != 0x00) return false; @@ -61,6 +69,7 @@ private static void parseJsonResponse(MinecraftPinger pinger, String data, long try { Gson gson = new Gson(); JsonObject json = gson.fromJson(data, JsonObject.class); + //System.out.println(json.toString()); JsonObject versionData = json.getAsJsonObject("version"); String version = versionData.get("name").getAsString();