diff --git a/build.gradle.kts b/build.gradle.kts index 76ba8ba..a5c6f46 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "org.zrnq" -version = "1.1.8" +version = "1.1.9" repositories { maven("https://maven.aliyun.com/repository/public") diff --git a/src/main/kotlin/org/zrnq/mclient/GUIMain.kt b/src/main/kotlin/org/zrnq/mclient/GUIMain.kt index 4dec18a..8ff1424 100644 --- a/src/main/kotlin/org/zrnq/mclient/GUIMain.kt +++ b/src/main/kotlin/org/zrnq/mclient/GUIMain.kt @@ -4,12 +4,8 @@ import com.alibaba.fastjson.parser.ParserConfig import org.zrnq.mclient.output.GUIOutputHandler import java.awt.Font -lateinit var FONT : Font -lateinit var dnsServerList : List - fun main() { - FONT = Font("Microsoft YaHei UI", Font.PLAIN, 20) - dnsServerList = listOf("223.5.5.5", "8.8.8.8", "114.114.114.114") + MClientOptions.FONT = Font("Microsoft YaHei UI", Font.PLAIN, 20) ParserConfig.getGlobalInstance().isSafeMode = true GUIOutputHandler() } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mclient/MClient.kt b/src/main/kotlin/org/zrnq/mclient/MClient.kt index 2d06548..b0a06e6 100644 --- a/src/main/kotlin/org/zrnq/mclient/MClient.kt +++ b/src/main/kotlin/org/zrnq/mclient/MClient.kt @@ -1,20 +1,18 @@ package org.zrnq.mclient -import com.alibaba.fastjson.JSON -import com.alibaba.fastjson.JSONArray -import com.alibaba.fastjson.JSONObject import org.xbill.DNS.* import org.zrnq.mclient.output.AbstractOutputHandler -import java.awt.* +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.* +import javax.swing.SwingConstants const val addressPrefix = "_minecraft._tcp." private val dnsResolvers by lazy { - dnsServerList.map { + MClientOptions.dnsServerList.map { SimpleResolver(Inet4Address.getByName(it)) .also { resolver -> resolver.timeout = java.time.Duration.ofSeconds(2) } } @@ -25,7 +23,7 @@ private fun Resolver.query(name : String) : List { .getSection(Section.ANSWER) } -fun pingInternal(target : String, outputHandler : AbstractOutputHandler, showTrueAddress : Boolean = true) { +fun pingInternal(target : String, outputHandler : AbstractOutputHandler) { try { outputHandler.beforePing() val option = target.split(":") @@ -53,7 +51,9 @@ fun pingInternal(target : String, outputHandler : AbstractOutputHandler, showTru for(it in addressList) { try { outputHandler.onAttemptAddress("${it.first}:${it.second}") - outputHandler.onSuccess(if(showTrueAddress) renderInfoImage(it.first, it.second) else renderInfoImage(it.first, it.second, target)) + val info = getInfo(it.first, it.second) + .let { if(!MClientOptions.showTrueAddress) it.setAddress(target) else it } + outputHandler.onSuccess(info) outputHandler.afterPing() return } catch (ex : Exception) { @@ -67,49 +67,36 @@ fun pingInternal(target : String, outputHandler : AbstractOutputHandler, showTru } } -fun renderInfoImage(address : String, port : Int, renderAddress : String = "$address:$port") : BufferedImage { - val info = getInfo(address, port) +fun renderBasicInfoImage(info : ServerInfo) : BufferedImage { val border = 20 val width = 1000 val height = 200 - val json = JSON.parseObject(info.first) val result = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) val g = result.createGraphics() - g.font = FONT + 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(json.containsKey("favicon")) - paintBase64Image(json.getString("favicon"), g, border, border, height - 2 * border, height - 2 * border) + 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(json.getString("description"), g, height, border, width - border - height, height / 2 - border) - val playerJson = json.getJSONObject("players") - var playerDescription = "获取失败" - if(playerJson != null) - playerDescription = "${playerJson["online"]}/${playerJson["max"]} " - playerDescription += - if(playerJson.containsKey("sample")) "玩家列表:${getPlayerList(playerJson.getJSONArray("sample")).limitLength(50)}" - else "玩家列表:没有信息" + paintDescription(info.description, g, height, border, width - border - height, height / 2 - border) + paintString(""" - 访问地址: $renderAddress Ping: ${info.second} - ${json.getJSONObject("version").getString("name").limitLength(50)} - 在线人数: $playerDescription""".trimIndent() + 访问地址: ${info.serverAddress} Ping: ${info.latency} + ${info.version.limitLength(50)} + ${info.playerDescription}""".trimIndent() , g, height, height / 2, width - border - height, height / 2 - border) return result } -fun getPlayerList(list : JSONArray) : String { - return if(list.isEmpty()) "空" - else list.joinToString(", ") { (it as JSONObject).getString("name") } -} - -fun getInfo(address : String, port : Int = 25565) : Pair { +fun getInfo(address : String, port : Int = 25565) : ServerInfo { val socket = Socket() socket.soTimeout = 3000 socket.connect(InetSocketAddress(address, port)) @@ -140,5 +127,5 @@ fun getInfo(address : String, port : Int = 25565) : Pair { } socket.close() - return result to latency + return ServerInfo(result, latency).setAddress("$address:$port") } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mclient/MClientOptions.kt b/src/main/kotlin/org/zrnq/mclient/MClientOptions.kt new file mode 100644 index 0000000..56b198f --- /dev/null +++ b/src/main/kotlin/org/zrnq/mclient/MClientOptions.kt @@ -0,0 +1,42 @@ +package org.zrnq.mclient + +import org.zrnq.mcmotd.McMotd +import org.zrnq.mcmotd.PluginConfig +import java.awt.Font +import java.awt.GraphicsEnvironment + +object MClientOptions { + lateinit var FONT : Font + var dnsServerList = listOf("223.5.5.5", "8.8.8.8") + var showTrueAddress = false + var recordInterval = 300 + + fun loadPluginConfig() { + dnsServerList = if(PluginConfig.dnsServerList.isEmpty()) { + McMotd.logger.warning("配置文件中没有填写DNS服务器地址,正在使用默认的DNS服务器") + listOf("223.5.5.5", "8.8.8.8") + } else PluginConfig.dnsServerList + showTrueAddress = PluginConfig.showTrueAddress + recordInterval = PluginConfig.recordInterval + + val fontList = mutableListOf() + for(f in GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts) { + if(f.name == PluginConfig.fontName) { + FONT = f.deriveFont(20f) + return + } + if(f.canDisplay('啊')) { + fontList.add(f) + } + } + McMotd.logger.warning("找不到指定的字体 : ${PluginConfig.fontName},您可以在mcmotd.yml中修改字体名称") + FONT = if(fontList.isEmpty()) { + McMotd.logger.error("找不到可用的字体, 请检查您的系统是否安装了中文字体") + Font(PluginConfig.fontName, Font.PLAIN, 20) + } else { + McMotd.logger.info("检测到可用的字体列表: ${fontList.joinToString(",") { it.name }}") + McMotd.logger.warning("正在使用第一个可用的字体: ${fontList[0].name}") + fontList[0].deriveFont(20f) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt b/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt new file mode 100644 index 0000000..8daec37 --- /dev/null +++ b/src/main/kotlin/org/zrnq/mclient/ServerInfo.kt @@ -0,0 +1,57 @@ +package org.zrnq.mclient + +import com.alibaba.fastjson.JSON +import com.alibaba.fastjson.JSONArray +import com.alibaba.fastjson.JSONObject + +class ServerInfo(response : String, val latency : String) { + /**服务器图标*/ + val favicon : String? + /**服务器描述*/ + val description : String + /**服务器版本号*/ + val version : String + /**服务器在线玩家描述*/ + val playerDescription : String + /**在线人数*/ + val onlinePlayerCount : Int? + /**服务器宣称的最大人数*/ + val maxPlayerCount : Int? + /**服务器提供的部分在线玩家列表*/ + val samplePlayerList : String? + /**服务器的显示地址*/ + lateinit var serverAddress : String + + init { + val json = JSON.parseObject(response) + favicon = json.getString("favicon") + description = json.getString("description") + version = json.getJSONObject("version").getString("name") + val playerJson = json.getJSONObject("players") + + onlinePlayerCount = playerJson?.getIntValue("online") + maxPlayerCount = playerJson?.getIntValue("max") + samplePlayerList = playerJson?.getJSONArray("sample")?.toPlayerListString() + + playerDescription = run { + if(onlinePlayerCount == null) return@run "服务器未提供在线玩家信息" + val playerCount = "在线人数: $onlinePlayerCount/$maxPlayerCount 玩家列表: " + if(samplePlayerList == null) return@run playerCount + "没有信息" + return@run playerCount + samplePlayerList.limitLength(50) + } + } + + fun setAddress(address : String) : ServerInfo { + serverAddress = address + return this + } + + + companion object { + fun JSONArray?.toPlayerListString() : String { + return if(this == null) "没有信息" + else if(isEmpty()) "空" + else joinToString(", ") { (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 c68ebe8..9c69a6c 100644 --- a/src/main/kotlin/org/zrnq/mclient/Utils.kt +++ b/src/main/kotlin/org/zrnq/mclient/Utils.kt @@ -12,6 +12,14 @@ import javax.imageio.ImageIO import javax.swing.JFrame import javax.swing.JLabel +fun Int.secondToReadableTime() : String { + return when { + this < 60 -> "${this}s" + this < 3600 -> String.format("%.2fmin", this.toFloat() / 60) + else -> String.format("%.2fh", this.toFloat() / 3600) + } +} + fun Exception.translateCommonException() = when { matches("Connection timed out: connect") -> "连接服务器超时" diff --git a/src/main/kotlin/org/zrnq/mclient/output/OutputHandlers.kt b/src/main/kotlin/org/zrnq/mclient/output/OutputHandlers.kt index 344a3f2..feec7be 100644 --- a/src/main/kotlin/org/zrnq/mclient/output/OutputHandlers.kt +++ b/src/main/kotlin/org/zrnq/mclient/output/OutputHandlers.kt @@ -1,16 +1,12 @@ package org.zrnq.mclient.output import net.mamoe.mirai.utils.MiraiLogger -import org.zrnq.mclient.FONT -import org.zrnq.mclient.centerOnScreen -import org.zrnq.mclient.pingInternal -import org.zrnq.mclient.translateCommonException +import org.zrnq.mclient.* import java.awt.BorderLayout import java.awt.Color import java.awt.Dimension import java.awt.event.KeyAdapter import java.awt.event.KeyEvent -import java.awt.image.BufferedImage import javax.swing.* import javax.swing.border.EmptyBorder import kotlin.concurrent.thread @@ -35,7 +31,7 @@ abstract class AbstractOutputHandler { /** * Called if one server address returns a valid response, and before afterPing(). * */ - abstract fun onSuccess(image : BufferedImage) + abstract fun onSuccess(info : ServerInfo) /** * Called after each ping request, and after onFailure() and onSuccess(). * */ @@ -47,12 +43,12 @@ abstract class AbstractOutputHandler { class GUIOutputHandler : AbstractOutputHandler() { private val errBuilder = StringBuilder() private val mainFrame = JFrame("Ping MC Server") - private val progress = JProgressBar().apply { isIndeterminate = true; isVisible = false; isStringPainted = true; font = FONT } - private val resultLabel = JLabel().apply { font = FONT; foreground = Color.RED } + private val progress = JProgressBar().apply { isIndeterminate = true; isVisible = false; isStringPainted = true; font = MClientOptions.FONT } + private val resultLabel = JLabel().apply { font = MClientOptions.FONT; foreground = Color.RED } private val textField = JTextField() init { textField.apply { - font = FONT + font = MClientOptions.FONT preferredSize = Dimension(500, preferredSize.height) addKeyListener(object : KeyAdapter() { override fun keyPressed(e : KeyEvent) { @@ -67,7 +63,7 @@ class GUIOutputHandler : AbstractOutputHandler() { } val inputPane = JPanel().apply { layout = BorderLayout(20, 20) - add(JLabel("Ping Target:").apply { font = FONT }, BorderLayout.WEST) + add(JLabel("Ping Target:").apply { font = MClientOptions.FONT }, BorderLayout.WEST) add(JPanel().apply { layout = BorderLayout() add(progress, BorderLayout.CENTER) @@ -108,8 +104,8 @@ class GUIOutputHandler : AbstractOutputHandler() { resultLabel.text = errBuilder.append("").toString() } - override fun onSuccess(image : BufferedImage) { - resultLabel.icon = ImageIcon(image) + override fun onSuccess(info : ServerInfo) { + resultLabel.icon = ImageIcon(renderBasicInfoImage(info)) } override fun afterPing() { @@ -122,7 +118,7 @@ class GUIOutputHandler : AbstractOutputHandler() { class APIOutputHandler( private val logger : MiraiLogger, private val fail : (String) -> Unit, - private val success : (BufferedImage) -> Unit + private val success : (ServerInfo) -> Unit ) : AbstractOutputHandler() { private val errBuilder = StringBuilder() override fun beforePing() { @@ -136,12 +132,12 @@ class APIOutputHandler( val message = exception.translateCommonException() if(message.contains(':')) logger.warning("MC Ping Failed", exception) - errBuilder.append("$message\n") + errBuilder.append("$address:$message\n") } override fun onFailure() = fail(errBuilder.toString()) - override fun onSuccess(image : BufferedImage) = success(image) + override fun onSuccess(info : ServerInfo) = success(info) override fun afterPing() = Unit } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt b/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt new file mode 100644 index 0000000..5ece4c8 --- /dev/null +++ b/src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt @@ -0,0 +1,113 @@ +package org.zrnq.mcmotd + +import org.zrnq.mclient.MClientOptions +import java.awt.* +import java.awt.image.BufferedImage +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + +object ImageUtil { + fun BufferedImage.appendPlayerHistory(address : String) : BufferedImage { + if(!PluginConfig.recordOnlinePlayer.contains(address)) return this + val history = PluginData.getHistory(address) + val result = BufferedImage(1000, 400, BufferedImage.TYPE_INT_RGB) + val historyImage = renderPlayerHistory(history) + + val g = result.createGraphics() + g.drawImage(this, 0, 0, null) + g.drawImage(historyImage, 0, 200, null) + return result + } + fun renderPlayerHistory(history : MutableList>) : BufferedImage { + val height = 200 + val width = 1000 + val result = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) + val g = result.createGraphics() + g.color = Color.WHITE + g.font = MClientOptions.FONT + g.setRenderingHints(mapOf( + RenderingHints.KEY_TEXT_ANTIALIASING to RenderingHints.VALUE_TEXT_ANTIALIAS_ON, + RenderingHints.KEY_ANTIALIASING to RenderingHints.VALUE_ANTIALIAS_ON)) + g.drawString("在线人数趋势", 20, 20) + + if(history.size <= 1) { + g.color = Color(254, 71, 81) + g.paintTextCC("-=没有足够的数据来绘制图表,稍后再来吧=-", width / 2, height / 2) + return result + } + + val minTime = history.first().first + val maxTime = history.last().first + val timeRange = maxTime - minTime + val maxCount = history.maxOf { it.second }.coerceAtLeast(1) + + val minX = max(40, 20 + g.fontMetrics.stringWidth(maxCount.toString())) + val maxX = width - 40 + val minY = 20 + g.fontMetrics.height + val maxY = height - 30 + val xRange = maxX - minX + val yRange = maxY - minY + + g.color = Color.GRAY + g.drawLine(minX, minY, minX, maxY) + (1..4).map { + it * yRange / 4 + minY + }.forEach { + g.drawLine(minX, it, maxX, it) + } + + g.color = Color.WHITE + + g.paintTextRB("0", minX, maxY) + g.paintTextRC(maxCount.toString(), minX, minY) + + val format = SimpleDateFormat("HH:mm") + + // x-axis ticks + (0 .. 4).map { + format.format(Date(it * timeRange / 4 + minTime)) to + it * xRange / 4 + minX + }.forEach { + g.paintTextCT(it.first, it.second, maxY) + } + + val plot = history.map { + ((it.first - minTime) * xRange / timeRange).toInt() to + (it.second * yRange / maxCount) + }.map { + minX + it.first to + maxY - it.second + } + + val polygon = plot.toMutableList().also { + it.add(maxX to maxY) + it.add(minX to maxY) + } + + g.paint = GradientPaint(minX.toFloat(), minY.toFloat(), Color(132, 188, 60, 150), + minX.toFloat(), maxY.toFloat(), Color(132, 188, 60, 0)) + g.fillPolygon(polygon.map { it.first }.toIntArray(), polygon.map { it.second }.toIntArray(), polygon.size) + + g.color = Color(132, 188, 60) + g.stroke = BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND) + + g.drawPolyline(plot.map { it.first }.toIntArray(), plot.map { it.second }.toIntArray(), plot.size) + + return result + } + /**指定字符串的右侧中心坐标来绘制*/ + fun Graphics2D.paintTextRC(str : String, x : Int, y : Int) { + drawString(str, x - fontMetrics.stringWidth(str), y + fontMetrics.ascent - fontMetrics.height / 2) + } + fun Graphics2D.paintTextRB(str : String, x : Int, y : Int) { + drawString(str, x - fontMetrics.stringWidth(str), y - fontMetrics.descent) + } + /**指定字符串的中心顶部坐标来绘制*/ + fun Graphics2D.paintTextCT(str : String, x : Int, y : Int) { + drawString(str, x - fontMetrics.stringWidth(str) / 2, y + fontMetrics.ascent) + } + fun Graphics2D.paintTextCC(str : String, x : Int, y : Int) { + drawString(str, x - fontMetrics.stringWidth(str) / 2, y + fontMetrics.ascent - fontMetrics.height / 2) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mcmotd/McMotd.kt b/src/main/kotlin/org/zrnq/mcmotd/McMotd.kt index 9de30ae..0a4ecc4 100644 --- a/src/main/kotlin/org/zrnq/mcmotd/McMotd.kt +++ b/src/main/kotlin/org/zrnq/mcmotd/McMotd.kt @@ -7,10 +7,10 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.info -import org.zrnq.mclient.FONT -import org.zrnq.mclient.dnsServerList -import java.awt.Font -import java.awt.GraphicsEnvironment +import org.zrnq.mclient.MClientOptions +import org.zrnq.mclient.output.APIOutputHandler +import org.zrnq.mclient.pingInternal +import java.util.* lateinit var miraiLogger : MiraiLogger @@ -18,7 +18,7 @@ object McMotd : KotlinPlugin( JvmPluginDescription( id = "org.zrnq.mcmotd", name = "Minecraft MOTD Fetcher", - version = "1.1.8", + version = "1.1.9", ) { author("ZRnQ") info("""以图片的形式获取指定Minecraft服务器的基本信息""") @@ -33,36 +33,39 @@ object McMotd : KotlinPlugin( QueryCommand.register() BindCommand.register() DelCommand.register() - dnsServerList = if(PluginConfig.dnsServerList.isEmpty()) { - logger.warning("配置文件中没有填写DNS服务器地址,正在使用默认的DNS服务器") - listOf("223.5.5.5", "8.8.8.8") - } else PluginConfig.dnsServerList - val fontList = mutableListOf() - for(f in GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts) { - if(f.name == PluginConfig.fontName) { - FONT = f.deriveFont(20f) - return - } - if(f.canDisplay('啊')) { - fontList.add(f) - } - } - logger.warning("找不到指定的字体 : ${PluginConfig.fontName},您可以在mcmotd.yml中修改字体名称") - FONT = if(fontList.isEmpty()) { - logger.error("找不到可用的字体, 请检查您的系统是否安装了中文字体") - Font(PluginConfig.fontName, Font.PLAIN, 20) - } else { - logger.info("检测到可用的字体列表: ${fontList.joinToString(",") { it.name }}") - logger.warning("正在使用第一个可用的字体: ${fontList[0].name}") - fontList[0].deriveFont(20f) - } + RecordCommand.register() + MClientOptions.loadPluginConfig() + startRecord() } override fun onDisable() { logger.info { "McMotd is unloading" } QueryCommand.unregister() BindCommand.unregister() - DelCommand.register() + DelCommand.unregister() + RecordCommand.unregister() + stopRecord() + } + + private lateinit var timer : Timer + + private fun startRecord() { + timer = Timer() + timer.schedule(object : TimerTask() { + override fun run() { + PluginConfig.recordOnlinePlayer.forEach { address -> + pingInternal(address, APIOutputHandler(miraiLogger, + { miraiLogger.warning("Periodic check failed for $address: $it") }, + { if(it.onlinePlayerCount == null) miraiLogger.warning("Periodic check: target server ($address) does not supply online player count") + else PluginData.recordHistory(address, it.onlinePlayerCount) }) + ) + } + } + }, MClientOptions.recordInterval.toLong() * 1000, MClientOptions.recordInterval.toLong() * 1000) + } + + private fun stopRecord() { + timer.cancel() } } diff --git a/src/main/kotlin/org/zrnq/mcmotd/PluginConfig.kt b/src/main/kotlin/org/zrnq/mcmotd/PluginConfig.kt index ee8efde..705bead 100644 --- a/src/main/kotlin/org/zrnq/mcmotd/PluginConfig.kt +++ b/src/main/kotlin/org/zrnq/mcmotd/PluginConfig.kt @@ -7,4 +7,7 @@ object PluginConfig : AutoSavePluginConfig("mcmotd") { val fontName by value("Microsoft YaHei") val showTrueAddress by value(false) val dnsServerList by value(mutableListOf("223.5.5.5", "8.8.8.8")) + val recordOnlinePlayer by value(mutableListOf()) + val recordInterval by value(300) + val recordLimit by value(21600) } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mcmotd/PluginData.kt b/src/main/kotlin/org/zrnq/mcmotd/PluginData.kt index 1e910e5..2a97696 100644 --- a/src/main/kotlin/org/zrnq/mcmotd/PluginData.kt +++ b/src/main/kotlin/org/zrnq/mcmotd/PluginData.kt @@ -6,6 +6,8 @@ import net.mamoe.mirai.console.data.value object PluginData : AutoSavePluginData("mcmotd_relation") { var relation by value>>>() + var history by value>>>() + operator fun get(groupId : Long) : MutableList>? { val result = relation[groupId] ?: return null @@ -22,4 +24,18 @@ object PluginData : AutoSavePluginData("mcmotd_relation") { else relation[groupId] = value } + + fun getHistory(address : String) : MutableList> { + val result = history[address] + ?: return mutableListOf() + val limit = System.currentTimeMillis() - PluginConfig.recordLimit * 1000 + result.removeIf { it.first < limit } + return result + } + + fun recordHistory(address : String, count : Int) { + val target = getHistory(address) + target.add(System.currentTimeMillis() to count) + history[address] = target + } } \ No newline at end of file diff --git a/src/main/kotlin/org/zrnq/mcmotd/QueryCommand.kt b/src/main/kotlin/org/zrnq/mcmotd/QueryCommand.kt index 028e054..ae2d9d8 100644 --- a/src/main/kotlin/org/zrnq/mcmotd/QueryCommand.kt +++ b/src/main/kotlin/org/zrnq/mcmotd/QueryCommand.kt @@ -9,6 +9,9 @@ import net.mamoe.mirai.console.util.sendAnsiMessage import net.mamoe.mirai.message.data.At import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import org.zrnq.mclient.output.APIOutputHandler +import org.zrnq.mclient.renderBasicInfoImage +import org.zrnq.mclient.secondToReadableTime +import org.zrnq.mcmotd.ImageUtil.appendPlayerHistory import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -53,7 +56,7 @@ object QueryCommand : SimpleCommand(McMotd, "mcp", description = "获取指定M private suspend fun CommandSender.doPing(target : String) = withContext(Dispatchers.IO) { var error : String? = null var image : BufferedImage? = null - org.zrnq.mclient.pingInternal(target, APIOutputHandler(McMotd.logger, { error = it }, { image = it }), PluginConfig.showTrueAddress) + org.zrnq.mclient.pingInternal(target, APIOutputHandler(McMotd.logger, { error = it }, { image = renderBasicInfoImage(it).appendPlayerHistory(target) })) if(image == null) reply(error!!) else @@ -97,6 +100,39 @@ object DelCommand : SimpleCommand(McMotd, "mcdel", description = "删除当前 } } +@Suppress("unused") +object RecordCommand : SimpleCommand(McMotd, "mcrec", description = "指定需要记录在线人数的服务器") { + @Handler + suspend fun MemberCommandSender.handle() { + if(PluginConfig.recordOnlinePlayer.isEmpty()) { + reply("没有已启用在线人数记录的服务器,使用\"/mcrec <服务器地址> true\"以开始记录指定服务器的在线人数") + return + } + reply("已启用在线人数记录的服务器:${PluginConfig.recordOnlinePlayer.joinToString(",")}。每${PluginConfig.recordInterval.secondToReadableTime()}记录一次在线人数,最多保存${PluginConfig.recordLimit.secondToReadableTime()}之前的记录。") + } + + @Handler + suspend fun MemberCommandSender.handle(address : String) { + if(PluginConfig.recordOnlinePlayer.contains(address)) + reply("服务器[$address]已启用在线人数记录,使用\"/mcrec $address false\"来禁用此服务器的在线人数记录功能") + else + reply("服务器[$address]未启用在线人数记录,使用\"/mcrec $address true\"来在此服务器上启用在线人数记录") + } + + @Handler + suspend fun MemberCommandSender.handle(address : String, enable : Boolean) { + if(enable) { + if(!PluginConfig.recordOnlinePlayer.contains(address)) + PluginConfig.recordOnlinePlayer.add(address) + reply("已开始记录${address}的在线人数") + } else { + PluginConfig.recordOnlinePlayer.remove(address) + PluginData.history.remove(address) + reply("已停止记录${address}的在线人数") + } + } +} + private suspend fun CommandSender.reply(message : String) { if(user == null) sendAnsiMessage { lightPurple().append(message) } else sendMessage(At(user!!.id) + message)