diff --git a/build.gradle.kts b/build.gradle.kts index 752305e..f19e44d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "org.zrnq" -version = "1.1.17" +version = "1.1.18" val ktor_version = "2.3.1" repositories { diff --git a/src/main/kotlin/org/zrnq/mclient/MClient.kt b/src/main/kotlin/org/zrnq/mclient/MClient.kt index 3a1b97e..dedad05 100644 --- a/src/main/kotlin/org/zrnq/mclient/MClient.kt +++ b/src/main/kotlin/org/zrnq/mclient/MClient.kt @@ -3,13 +3,9 @@ package org.zrnq.mclient import gnu.inet.encoding.IDNA import org.xbill.DNS.* import org.zrnq.mclient.output.AbstractOutputHandler -import java.awt.Color -import java.awt.RenderingHints -import java.awt.image.BufferedImage import java.net.Inet4Address import java.net.InetSocketAddress import java.net.Socket -import javax.swing.SwingConstants const val addressPrefix = "_minecraft._tcp." private val dnsResolvers by lazy { @@ -69,34 +65,6 @@ fun pingInternal(target : String, outputHandler : AbstractOutputHandler) { } } -fun renderBasicInfoImage(info : ServerInfo) : BufferedImage { - val border = 20 - val width = 1000 - val height = 200 - val result = createTransparentImage(width, height) - val g = result.createGraphics() - g.font = MClientOptions.FONT - g.setRenderingHints(mapOf( - RenderingHints.KEY_INTERPOLATION to RenderingHints.VALUE_INTERPOLATION_BICUBIC, - RenderingHints.KEY_TEXT_ANTIALIASING to RenderingHints.VALUE_TEXT_ANTIALIAS_ON - )) - if(info.favicon != null) - paintBase64Image(info.favicon, g, border, border, height - 2 * border, height - 2 * border) - else - paintString("NO IMAGE", g, border, (height - g.fontMetrics.height) / 2 , height - 2 * border, height - 2 * border) { - foreground = Color.MAGENTA - horizontalAlignment = SwingConstants.CENTER - } - g.drawRect(border, border, height - 2 * border, height - 2 * border) - paintDescription(info.description, g, height, border, width - border - height, height / 2 - border) - - val sb = StringBuilder("访问地址: ${info.serverAddress} Ping: ${info.latency}") - if(MClientOptions.showServerVersion) sb.append("\n${info.version.limitLength(50)}") - sb.append("\n${info.playerDescription}") - paintDescription(sb.toString(), g, height, height / 2, width - border - height, height / 2 - border) - return result -} - fun getInfo(address : String, port : Int = 25565) : ServerInfo { val socket = Socket() socket.soTimeout = 3000 @@ -122,9 +90,9 @@ fun getInfo(address : String, port : Int = 25565) : ServerInfo { output.flush() // https://wiki.vg/Protocol#Ping : The returned value from server could be any number Packet(input, PLong::class) - (System.currentTimeMillis() - time).toString() + "ms" + (System.currentTimeMillis() - time).toInt() } catch (e : Exception) { - "Failed" + -1 } socket.close() diff --git a/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt b/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt index 5b8844e..1c19f1d 100644 --- a/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt +++ b/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt @@ -4,23 +4,21 @@ import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONObject -class ServerInfo(response : String, val latency : String) { +class ServerInfo(response : String, private val latency : Int) { /**服务器图标*/ val favicon : String? /**服务器描述*/ - val description : String + private val description : String /**服务器版本号*/ - val version : String - /**服务器在线玩家描述*/ - val playerDescription : String + private val version : String /**在线人数*/ val onlinePlayerCount : Int? /**服务器宣称的最大人数*/ - val maxPlayerCount : Int? + private val maxPlayerCount : Int? /**服务器提供的部分在线玩家列表*/ - val samplePlayerList : String? + private val samplePlayerList : String? /**服务器的显示地址*/ - lateinit var serverAddress : String + private lateinit var serverAddress : String init { val json = JSON.parseObject(response) @@ -31,16 +29,7 @@ class ServerInfo(response : String, val latency : String) { onlinePlayerCount = playerJson?.getIntValue("online") maxPlayerCount = playerJson?.getIntValue("max") - samplePlayerList = playerJson?.getJSONArray("sample")?.toPlayerListString() - - playerDescription = run { - if(onlinePlayerCount == null) return@run "服务器未提供在线玩家信息" - var playerCount = "在线人数: $onlinePlayerCount/$maxPlayerCount " - if(!MClientOptions.showPlayerList) return@run playerCount - playerCount += "玩家列表: " - if(samplePlayerList == null) return@run playerCount + "没有信息" - return@run playerCount + samplePlayerList.limitLength(50) - } + samplePlayerList = playerJson?.getJSONArray("sample")?.toPlayerListString(10) } fun setAddress(address : String) : ServerInfo { @@ -48,12 +37,59 @@ class ServerInfo(response : String, val latency : String) { return this } + private fun getDescriptionHTML(): String + = if(description.startsWith("{")) jsonStringToHTML(JSON.parseObject(description)) + else jsonStringToHTML(JSON.parseObject("{\"text\":\"$description\"}")) + + private fun getPingHTML(): String { + val bars = when(latency) { + -1 -> "red" to 0 + in 0 until 100 -> "green" to 5 + in 100 until 300 -> "green" to 4 + in 300 until 500 -> "green" to 3 + in 500 until 1000 -> "yellow" to 2 + else -> "red" to 1 + }.let { colorMap[it.first] to it.second } + if(latency < 0) return "失败 [×]" + return "${latency}ms " + + "[${"|".repeat(bars.second)}" + + "${"|".repeat(5 - bars.second)}]" + } + + private fun getPlayerDescriptionHTML(): String { + if(onlinePlayerCount == null) return "服务器未提供在线玩家信息" + var playerCount = "在线人数: $onlinePlayerCount/$maxPlayerCount " + if(!MClientOptions.showPlayerList) return playerCount + playerCount += "玩家列表: " + if(samplePlayerList == null) return playerCount + "没有信息" + return playerCount + jsonStringToHTML(JSON.parseObject("{\"text\":\"$samplePlayerList\"}")) + } + + + fun toHTMLString(): String { + val sb = StringBuilder("
") + sb.append(getDescriptionHTML()) + .append("
") + .append("
访问地址: $serverAddress Ping: ") + .append(getPingHTML()) + .append("
") + if(MClientOptions.showServerVersion) { + sb.append("
") + .append(version.limitLength(50)) + .append("
") + } + sb.append("
") + .append(getPlayerDescriptionHTML()) + .append("
") + .append("") + return sb.toString() + } companion object { - fun JSONArray?.toPlayerListString() : String { + fun JSONArray?.toPlayerListString(limit: Int) : String { return if(this == null) "没有信息" else if(isEmpty()) "空" - else joinToString(", ") { (it as JSONObject).getString("name") } + else joinToString(", ", limit = limit) { (it as JSONObject).getString("name") } } } } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mclient/Utils.kt b/src/main/kotlin/org/zrnq/mclient/Utils.kt index f98acfb..78fb060 100644 --- a/src/main/kotlin/org/zrnq/mclient/Utils.kt +++ b/src/main/kotlin/org/zrnq/mclient/Utils.kt @@ -4,16 +4,13 @@ import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONObject import gnu.inet.encoding.IDNA -import java.awt.AlphaComposite -import java.awt.Color -import java.awt.Graphics2D -import java.awt.Toolkit +import java.awt.* import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.util.* import javax.imageio.ImageIO +import javax.swing.JEditorPane import javax.swing.JFrame -import javax.swing.JLabel fun Int.secondToReadableTime() : String { return when { @@ -36,6 +33,57 @@ inline fun Exception.matches(msg : String? = null) = this::class == fun String.limitLength(max : Int) = if (length > max) this.substring(0, max) + "..." else this +fun renderBasicInfoImage(info: ServerInfo) : BufferedImage { + val margin = 20 + val width = 1000 + val iconSize = 160 + val iconCenter = 100 + val textWidth = width - iconSize - 3 * margin + val textX = iconSize + 2 * margin + + val textContent = info.toHTMLString() + // Creating a new JEditorPane every time since swing objects are not thread safe + val textRenderer = JEditorPane("text/html", textContent) + textRenderer.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true) + textRenderer.text = textContent + textRenderer.background = Color(0,0,0,0) + textRenderer.font = MClientOptions.FONT + textRenderer.setSize(textWidth, Short.MAX_VALUE.toInt()) + + val textSize = textRenderer.preferredSize + val imageHeight = (textSize.height + 2 * margin).coerceAtLeast(iconSize + 2 * margin) + val result = createTransparentImage(width, imageHeight) + + val g = result.createGraphics() + g.font = MClientOptions.FONT + g.setRenderingHints(mapOf( + RenderingHints.KEY_INTERPOLATION to RenderingHints.VALUE_INTERPOLATION_BICUBIC, + RenderingHints.KEY_TEXT_ANTIALIASING to RenderingHints.VALUE_TEXT_ANTIALIAS_ON + )) + if(info.favicon != null) + paintBase64Image(info.favicon, g, margin, margin, iconSize, iconSize) + else + paintStringWithBackground("NO IMAGE", g, iconCenter, iconCenter, Color.WHITE, Color(0xaa0000), 15, 10) + g.color = Color.WHITE + g.drawRect(margin, margin, iconSize, iconSize) + + textRenderer.paint(g.create(textX, margin, textWidth, textSize.height)) + return result +} + +fun paintStringWithBackground(str : String, g : Graphics2D, x : Int, y : Int, fg : Color, bg : Color, horizontalPadding : Int, verticalPadding : Int) { + val fontMetrics = g.fontMetrics + val textWidth = fontMetrics.stringWidth(str) + val textX = x - textWidth / 2 + val textY = y + fontMetrics.ascent - fontMetrics.height / 2 + val rectX = textX - horizontalPadding + val rectY = y - fontMetrics.height / 2 - verticalPadding + g.color = bg + g.fillRect(rectX, rectY, textWidth + 2 * horizontalPadding, fontMetrics.height + 2 * verticalPadding) + g.color = fg + g.drawString(str, textX, textY) +} + fun paintBase64Image(img : String, g : Graphics2D, x : Int, y : Int, w : Int, h : Int) { val imgDescriptor = img.split(",").let { it[0].substring(11, it[0].length - 7) to it[1].replace("\n", "") @@ -44,23 +92,6 @@ fun paintBase64Image(img : String, g : Graphics2D, x : Int, y : Int, w : Int, h g.drawImage(image, x, y, w, h, null) } -fun paintString(str : String, g : Graphics2D, x : Int, y : Int, w : Int, h : Int, block : JLabel.() -> Unit = {}) = JLabel().apply { - setSize(w, h) - text = "${str.replace(" ", " ").replace("\n", "
")}
" - foreground = Color.WHITE - font = g.font - block() - paint(g.create(x, y, w, h)) -} - -fun paintDescription(desc : String, g : Graphics2D, x : Int, y : Int, w : Int, h : Int) = JLabel().apply { - setSize(w, h) - text = if(desc.startsWith("{")) jsonStringToHTML(JSON.parseObject(desc)) - else jsonStringToHTML(JSON.parseObject("{\"text\":\"$desc\"}")) - font = g.font - paint(g.create(x, y, w, h)) -} - fun createTransparentImage(width: Int, height: Int) : BufferedImage { val result = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) val g = result.createGraphics() @@ -94,21 +125,23 @@ fun BufferedImage.addBackground() : BufferedImage { return background } -fun flatTextJSON(result : JSONArray, src : JSONObject) { - val currentIndex = result.size - val paragraph = JSONObject() - for(key in src.keys) { - if(key == "extra") { - when (val extra = src[key]) { - is JSONArray -> for(i in extra.indices) flatTextJSON(result, extra.getJSONObject(i)) - is JSONObject -> flatTextJSON(result, extra) - else -> throw IllegalArgumentException("Description syntax error") +fun flatJsonEntity(result: JSONArray, entity: Any) { + when(entity) { + is String -> result.add(JSONObject().apply { put("text", entity) }) + is JSONObject -> { + val flatObj = JSONObject() + val currentIndex = result.size + for(key in entity.keys) { + if(key == "extra") flatJsonEntity(result, entity[key]!!) + flatObj[key] = entity[key] } - } else { - paragraph[key] = src[key] + result.add(currentIndex, flatObj) + } + is JSONArray -> { + for(element in entity) flatJsonEntity(result, element) } } - result.add(currentIndex, paragraph) + } private const val RAW = 8 @@ -121,12 +154,8 @@ private const val STRIKE = 4 fun jsonStringToHTML(json : JSON) : String{ val line = JSONArray() - when (json) { - is JSONObject -> flatTextJSON(line, json) - is JSONArray -> for(i in json.indices) flatTextJSON(line, json.getJSONObject(i)) - else -> throw IllegalArgumentException("Description syntax error") - } - val builder = StringBuilder("") + flatJsonEntity(line, json) + val builder = StringBuilder() val attributes = Array(RAW * 2) { null } @@ -200,7 +229,6 @@ fun jsonStringToHTML(json : JSON) : String{ } builder.append(spanEnd) } - builder.append("") return builder.toString() } diff --git a/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt b/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt index a3af366..9d4e284 100644 --- a/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt +++ b/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt @@ -15,14 +15,15 @@ import kotlin.random.Random object ImageUtil { fun BufferedImage.appendPlayerHistory(address : String) : BufferedImage { + val playerHistoryHeight = 200 if(!PluginConfig.recordOnlinePlayer.contains(address)) return this.addBackground() val history = PluginData.getHistory(address) - val result = createTransparentImage(1000, 400) + val result = createTransparentImage(1000, height + playerHistoryHeight) val historyImage = renderPlayerHistory(history) val g = result.createGraphics() g.drawImage(this, 0, 0, null) - g.drawImage(historyImage, 0, 200, null) + g.drawImage(historyImage, 0, height, null) return result.addBackground() } fun renderPlayerHistory(history : MutableList>) : BufferedImage {