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 {