Skip to content

Commit

Permalink
Allow image height to grow for more text. fix #21
Browse files Browse the repository at this point in the history
Add ping indicator bar.
Change text renderer to JEditorPane.
Fix a parse error when "extra" field is JSONArray with String element.
  • Loading branch information
Under-estimate committed Jan 27, 2024
1 parent dbc32aa commit 6762c8e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 98 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "org.zrnq"
version = "1.1.17"
version = "1.1.18"
val ktor_version = "2.3.1"

repositories {
Expand Down
36 changes: 2 additions & 34 deletions src/main/kotlin/org/zrnq/mclient/MClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
76 changes: 56 additions & 20 deletions src/main/kotlin/org/zrnq/mclient/ServerInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -31,29 +29,67 @@ 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 {
serverAddress = address
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 "失败 [<span style='color:${bars.first};text-shadow: gray 2px 2px;'>×</span>]"
return "${latency}ms " +
"[<span style='color:${bars.first}; font-weight:bold;'>${"|".repeat(bars.second)}</span>" +
"<span style='color:${colorMap["gray"]}; font-weight:bold;'>${"|".repeat(5 - bars.second)}</span>]"
}

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("<!DOCTYPE html><html><head></head><body><div>")
sb.append(getDescriptionHTML())
.append("</div>")
.append("<div style='color:white;margin-top: 10px;'>访问地址: $serverAddress Ping: ")
.append(getPingHTML())
.append("</div>")
if(MClientOptions.showServerVersion) {
sb.append("<div style='color:white;'>")
.append(version.limitLength(50))
.append("</div>")
}
sb.append("<div style='color:white;'>")
.append(getPlayerDescriptionHTML())
.append("</div>")
.append("</body></html>")
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") }
}
}
}
110 changes: 69 additions & 41 deletions src/main/kotlin/org/zrnq/mclient/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,6 +33,57 @@ inline fun <reified E> 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", "")
Expand All @@ -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 = "<html><span style='color:white;white-space:nowrap;text-overflow:ellipsis;'>${str.replace(" ", "&nbsp;").replace("\n", "<br />")}</span></html>"
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()
Expand Down Expand Up @@ -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
Expand All @@ -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("<html>")
flatJsonEntity(line, json)
val builder = StringBuilder()

val attributes = Array<Any?>(RAW * 2) { null }

Expand Down Expand Up @@ -200,7 +229,6 @@ fun jsonStringToHTML(json : JSON) : String{
}
builder.append(spanEnd)
}
builder.append("</html>")
return builder.toString()
}

Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pair<Long, Int>>) : BufferedImage {
Expand Down

0 comments on commit 6762c8e

Please sign in to comment.