diff --git a/README.md b/README.md
index d9196ae..ea98292 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,10 @@
[![](https://img.shields.io/github/v/release/limbang/mirai-console-minecraft-plugin?include_prereleases)](https://github.com/limbang/mirai-console-minecraft-plugin/releases)
![](https://img.shields.io/github/downloads/limbang/mirai-console-minecraft-plugin/total)
[![](https://img.shields.io/github/license/limbang/mirai-console-minecraft-plugin)](https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE)
-[![](https://img.shields.io/badge/mirai-2.12.0-69c1b9)](https://github.com/mamoe/mirai)
+[![](https://img.shields.io/badge/mirai-2.16.0-69c1b9)](https://github.com/mamoe/mirai)
本项目是基于 Mirai Console 编写的插件
-
用于 ping 服务器状态,和查看服务器 tps,基于doctor库实现
+用于 ping 服务器状态
戳一戳机器人头像可以获取帮助
@@ -15,29 +15,10 @@
## 命令
```shell
-/mc addLogin # 添加登陆信息
/mc addServer [port] # 添加服务器,端口默认 25565
-/mc addServerLogin [port] # 添加带登陆信息带服务器,端口默认 25565
-/mc deleteLogin # 删除登陆信息
/mc deleteServer # 删除服务器
-/mc loginInfo # 查看登陆信息
-/mc setTps # 设置tps功能启用
/mc setAllToImg # 设置All消息转换为图片功能是否启动
```
-### 使用TPS功能
-1. 使用指令添加登陆信息,比如添加遗落之地的皮肤站演示如下
-```shell
-/mc addLogin nano https://skin.blackyin.xyz/api/yggdrasil/authserver https://skin.blackyin.xyz/api/yggdrasil/sessionserver 账号 密码
-```
-2. 然后添加服务器的时候用,`addServerLogin`命令添加,loginName 就是刚才设置的 nano,就可以使用了
-
-mc addLogin url参考 [yggdrasil](https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#%E4%BC%9A%E8%AF%9D%E9%83%A8%E5%88%86)
-
-- name 登陆配置名称
-- authServerUrl 验证服务器地址 正版地址为:https://authserver.mojang.com
-- sessionServerUrl 会话服务器地址 正版地址为:https://sessionserver.mojang.com
-- username 账号
-- password 密码
```shell
# 设置触发指令
@@ -46,24 +27,14 @@ mc addLogin url参考 [yggdrasil](https://github.com/yushijinhun/authlib-injecto
name 可设置如下
- PING `ping服务器`
- LIST `查询列表`
- - TPS `查询tps`
- PING_ALL `ping全部服务器`
-## 版本支持
-
-TPS 暂时只支持 Forge 端
-
-----
-
## 功能展示
戳一戳功能:
![](img/Screenshot_20220319_195629.jpg)
-tps 功能:
-![](img/1704DCA5-EC7F-4EF9-BF80-10DAC604836D.png)
-
直 ping 地址功能:
![](img/ABCBBD85-E183-41FE-BA3A-9D88853F43B3.png)
diff --git a/build.gradle.kts b/build.gradle.kts
index dadbf23..cde8248 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,25 +1,20 @@
plugins {
- val kotlinVersion = "1.8.10"
+ val kotlinVersion = "1.9.0"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
- id("net.mamoe.mirai-console") version "2.14.0"
+ id("net.mamoe.mirai-console") version "2.16.0"
}
group = "top.limbang"
-version = "1.1.13"
+version = "1.2.0"
repositories {
mavenCentral()
- maven("https://jitpack.io")
}
dependencies {
- implementation("top.fanua.doctor:doctor-client:1.3.13")
- implementation("top.fanua.doctor:doctor-plugin-forge-fix:1.3.13")
compileOnly("top.limbang:mirai-plugin-general-interface:1.0.2")
testImplementation("org.slf4j:slf4j-simple:2.0.5")
- "shadowLink"("top.fanua.doctor:doctor-client")
- "shadowLink"("top.fanua.doctor:doctor-plugin-forge-fix")
}
diff --git a/src/main/kotlin/ServerListPing.kt b/src/main/kotlin/ServerListPing.kt
new file mode 100644
index 0000000..d5604b6
--- /dev/null
+++ b/src/main/kotlin/ServerListPing.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2023 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
+ */
+
+package top.limbang.minecraft
+
+import kotlinx.serialization.json.*
+import top.limbang.minecraft.entity.Player
+import top.limbang.minecraft.entity.PlayerInfo
+import top.limbang.minecraft.entity.ServerStatus
+import top.limbang.minecraft.network.MinecraftInputStream
+import top.limbang.minecraft.network.MinecraftOutputStream
+import top.limbang.minecraft.utlis.getForgeDate
+import java.io.ByteArrayOutputStream
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.IOException
+import java.net.Socket
+
+
+fun ping(host: String, port: Int) = MinecraftClient.ping(host, port)
+
+object MinecraftClient {
+
+ /**
+ * Ping
+ *
+ * @param host 地址
+ * @param port 端口
+ * @return 延迟 和 服务器状态
+ */
+ fun ping(host: String, port: Int): Pair {
+ Socket(host, port).use { socket ->
+ val inStream = MinecraftInputStream(DataInputStream(socket.getInputStream()))
+ val outStream = MinecraftOutputStream(DataOutputStream(socket.getOutputStream()))
+
+ // 发送握手包
+ val handshake = encodeHandshake(address = host, port = port)
+ outStream.sendPacket(handshake)
+ // 发送状态请求数据包
+ outStream.writeVarInt(0x01)
+ outStream.writeByte(0x00)
+
+ // 读取握手包响应
+ val jsonString = decodeHandshakeResponse(inStream)
+ // 获取ping延时
+ val delay = getPingDelay(inStream, outStream)
+ val serverStatus = handleServerStatusJson(jsonString)
+ //decodeFavicon(serverStatus.favicon)
+ return Pair(delay,serverStatus)
+ }
+ }
+
+ /**
+ * 处理服务器状态JSON
+ *
+ * @param json
+ * @return
+ */
+ fun handleServerStatusJson(json: String): ServerStatus {
+ val element = Json.parseToJsonElement(json)
+ // 解析版本信息
+ val versionObject = element.jsonObject["version"] ?: throw RuntimeException("未找到版本信息...")
+ val versionName = versionObject.jsonObject["name"]!!.jsonPrimitive.content
+ val versionNumber = versionObject.jsonObject["protocol"]!!.jsonPrimitive.int
+ // 解析玩家信息
+ val playerObject = element.jsonObject["players"]!!
+ val playerMax = playerObject.jsonObject["max"]!!.jsonPrimitive.int
+ val playerOnline = playerObject.jsonObject["online"]!!.jsonPrimitive.int
+ val players = mutableListOf()
+ playerObject.jsonObject["sample"]?.jsonArray?.forEach {
+ players.add(
+ Player(
+ id = it.jsonObject["id"]!!.jsonPrimitive.content,
+ name = it.jsonObject["name"]!!.jsonPrimitive.content
+ )
+ )
+ }
+ val playerInfo = PlayerInfo(playerMax = playerMax, playerOnline = playerOnline, players = players)
+ // 解析服务器头像
+ val favicon = element.jsonObject["favicon"]?.jsonPrimitive?.content ?: ""
+ // 解析描述
+ val description = if (versionNumber == 5) element.jsonObject["description"]!!.jsonPrimitive.content
+ else element.jsonObject["description"]!!.jsonObject["text"]!!.jsonPrimitive.content
+
+ return ServerStatus(
+ favicon = favicon,
+ description = description,
+ playerInfo = playerInfo,
+ versionName = versionName,
+ versionNumber = versionNumber,
+ forgeData = getForgeDate(element, versionNumber)
+ )
+ }
+
+ /**
+ * 根据 minecraft 协议发送数据包
+ *
+ */
+ private fun MinecraftOutputStream.sendPacket(packet: ByteArray) {
+ writeVarInt(packet.size)
+ write(packet)
+ }
+
+ /**
+ * 根据 minecraft 协议编码握手包
+ *
+ * @param version 服务器版本 默认为:-1
+ * @param address 服务器地址
+ * @param port 服务器端口
+ * @param state 下一步状态 1:状态 2:登录
+ * @return
+ */
+ private fun encodeHandshake(version: Int = -1, address: String, port: Int, state: Int = 1): ByteArray {
+ val byte = ByteArrayOutputStream()
+ val handshake = MinecraftOutputStream(DataOutputStream(byte))
+ handshake.writeByte(0x00)
+ handshake.writeVarInt(version)
+ handshake.writeUTF(address)
+ handshake.writeShort(port)
+ handshake.writeVarInt(state)
+ return byte.toByteArray()
+ }
+
+ /**
+ * 根据 minecraft 协议解码握手包状态响应
+ *
+ * @return 服务器状态 json 字符串
+ */
+ private fun decodeHandshakeResponse(inStream: MinecraftInputStream): String {
+ val responseLength = inStream.readVarInt()
+ val packetId = inStream.readVarInt()
+ if (packetId == -1) throw IOException("Premature end of stream.")
+ if (packetId != 0x00) throw IOException("Invalid packetID: $packetId")
+ return inStream.readUTF(responseLength)
+ }
+
+ /**
+ * 获取 ping 延迟
+ *
+ * @param outStream
+ * @param inStream
+ * @return 成功返回延迟时间(毫秒),失败返回-1
+ */
+ private fun getPingDelay(inStream: MinecraftInputStream, outStream: MinecraftOutputStream) = try {
+ // 发送 ping 包
+ val now = System.currentTimeMillis()
+ outStream.writeByte(0x09)
+ outStream.writeByte(0x01)
+ outStream.writeLong(now)
+ // 读取 ping 响应
+ inStream.readVarInt()
+ val id = inStream.readVarInt()
+ if (id == -1) throw IOException("Premature end of stream.")
+ if (id != 0x01) throw IOException("Invalid packetID")
+ (System.currentTimeMillis() - inStream.readLong()).toInt()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ -1
+ }
+}
+
+
+
+
+
diff --git a/src/main/kotlin/entity/ServerStatus.kt b/src/main/kotlin/entity/ServerStatus.kt
new file mode 100644
index 0000000..1d406d4
--- /dev/null
+++ b/src/main/kotlin/entity/ServerStatus.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
+ */
+
+package top.limbang.minecraft.entity
+
+
+/**
+ * ### 服务器状态信息
+ *
+ * - [favicon] 服务器图标
+ * - [description] 服务器描述
+ * - [playerInfo] 玩家信息
+ * - [versionName] 版本名称
+ * - [versionNumber] 版本号
+ * - [forgeData] Forge相关信息
+ */
+data class ServerStatus(
+ val favicon: String,
+ val description: String,
+ val playerInfo: PlayerInfo,
+ val versionName: String,
+ val versionNumber: Int,
+ val forgeData: ForgeData
+)
+
+/**
+ * 玩家信息
+ *
+ * @property playerMax 最大玩家数
+ * @property playerOnline 当前在线玩家数
+ * @property players 玩家列表
+ */
+data class PlayerInfo(val playerMax: Int, val playerOnline: Int, val players: List)
+
+/**
+ * 玩家
+ *
+ * @property name 玩家名称
+ * @property id 玩家ID
+ */
+data class Player(val name: String, val id: String)
+
+/**
+ * 模组信息
+ *
+ * @property id 模组ID
+ * @property version 模组版本
+ */
+data class Mod(val id: String, val version: String)
+
+/**
+ * 通道信息
+ *
+ * @property name 通道名称
+ * @property version 通道版本
+ * @property requiredOnClient 是否在客户端必需
+ */
+data class Channel(val name: String, val version: String, val requiredOnClient: Boolean)
+
+/**
+ * Forge相关信息
+ *
+ * @property fmlNetworkVersion FML网络版本
+ * @property truncated 是否被截断
+ * @property mods 模组列表
+ * @property channels 通道列表
+ * @constructor Create empty Forge data
+ */
+data class ForgeData(
+ val fmlNetworkVersion: Int,
+ val truncated: Boolean,
+ val mods: List,
+ val channels: List
+)
diff --git a/src/main/kotlin/extension/ByteArrayStreamExtension.kt b/src/main/kotlin/extension/ByteArrayStreamExtension.kt
deleted file mode 100644
index 3db0b38..0000000
--- a/src/main/kotlin/extension/ByteArrayStreamExtension.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright 2020-2022 limbang and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
- */
-
-package top.limbang.minecraft.extension
-
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-
-/**
- * ### ByteArrayOutputStream 转 ByteArrayInputStream
- *
- */
-fun ByteArrayOutputStream.toInput(): ByteArrayInputStream {
- return ByteArrayInputStream(this.toByteArray())
-}
\ No newline at end of file
diff --git a/src/main/kotlin/Minecraft.kt b/src/main/kotlin/mirai/Minecraft.kt
similarity index 62%
rename from src/main/kotlin/Minecraft.kt
rename to src/main/kotlin/mirai/Minecraft.kt
index 59fa676..0766c01 100644
--- a/src/main/kotlin/Minecraft.kt
+++ b/src/main/kotlin/mirai/Minecraft.kt
@@ -7,32 +7,24 @@
* https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
*/
-package top.limbang.minecraft
+package top.limbang.minecraft.mirai
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
import net.mamoe.mirai.console.plugin.id
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
-import net.mamoe.mirai.contact.Contact.Companion.uploadImage
-import net.mamoe.mirai.contact.nameCardOrNick
import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.event.events.NudgeEvent
-import net.mamoe.mirai.event.subscribeGroupMessages
-import top.limbang.minecraft.PluginCompositeCommand.renameServer
-import top.limbang.minecraft.PluginData.isTps
-import top.limbang.minecraft.service.ImageService.createErrorImage
-import top.limbang.minecraft.service.ServerService.getServerList
-import top.limbang.minecraft.service.ServerService.getTPS
-import top.limbang.minecraft.service.ServerService.pingALlServer
-import top.limbang.minecraft.service.ServerService.pingServer
+import net.mamoe.mirai.event.registerTo
+import top.limbang.minecraft.mirai.PluginCompositeCommand.renameServer
import top.limbang.mirai.event.GroupRenameEvent
object Minecraft : KotlinPlugin(
JvmPluginDescription(
id = "top.limbang.minecraft",
name = "Minecraft",
- version = "1.1.13",
+ version = "1.2.0",
) {
author("limbang")
info("""Minecraft插件""")
@@ -58,30 +50,12 @@ object Minecraft : KotlinPlugin(
PluginCompositeCommand.register()
val ping = PluginData.commandMap[CommandName.PING] ?: "!"
val list = PluginData.commandMap[CommandName.LIST] ?: "!list"
- val tps = PluginData.commandMap[CommandName.TPS] ?: "!tps"
val pingAll = PluginData.commandMap[CommandName.PING_ALL] ?: "!all"
// 创建事件通道
val eventChannel = GlobalEventChannel.parentScope(this)
- eventChannel.subscribeGroupMessages {
- case(list) quoteReply { getServerList() }
- case(pingAll) reply { pingALlServer() }
- startsWith(ping) quoteReply {
- pingServer(it.substringAfter(ping).trim()) ?: subject.uploadImage(createErrorImage(sender.nameCardOrNick), "jpg")
- }
- startsWith(tps) {
- if (!isTps) return@startsWith
- getTPS(it, group, sender.nameCardOrNick)
- }
- startsWith("!ping") quoteReply {
- val parameter = it.trim().split(Regex("\\s"))
- if (parameter.size < 2 || parameter.size > 3) return@quoteReply "参数不正确:!ping <地址> [端口]"
- val address = parameter[1]
- val port = if (parameter.size == 3) parameter[2].toInt() else 25565
- pingServer(address, port, address) ?: subject.uploadImage(createErrorImage(sender.nameCardOrNick), "jpg")
- }
- }
+ MinecraftListener.registerTo(eventChannel)
eventChannel.subscribeAlways {
if (target.id == bot.id) {
@@ -90,7 +64,6 @@ object Minecraft : KotlinPlugin(
"Ping服务器:$ping 服务器名称\n" +
"Ping服务器:!ping <地址> [端口]\n" +
"Ping所有服务器:$pingAll\n" +
- "TPS:$tps 服务器名称\n" +
"查看服务器列表:$list"
)
}
diff --git a/src/main/kotlin/mirai/MinecraftListener.kt b/src/main/kotlin/mirai/MinecraftListener.kt
new file mode 100644
index 0000000..1677af5
--- /dev/null
+++ b/src/main/kotlin/mirai/MinecraftListener.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020-2022 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
+ */
+
+package top.limbang.minecraft.mirai
+
+import kotlinx.coroutines.launch
+import net.mamoe.mirai.contact.Contact.Companion.uploadImage
+import net.mamoe.mirai.event.EventHandler
+import net.mamoe.mirai.event.SimpleListenerHost
+import net.mamoe.mirai.event.events.GroupMessageEvent
+import net.mamoe.mirai.message.data.Message
+import net.mamoe.mirai.message.data.PlainText
+import net.mamoe.mirai.message.data.buildForwardMessage
+import top.limbang.minecraft.entity.ServerStatus
+import top.limbang.minecraft.mirai.PluginData.serverMap
+import top.limbang.minecraft.ping
+import top.limbang.minecraft.utlis.toImage
+import top.limbang.minecraft.utlis.toInput
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeoutException
+
+
+object MinecraftListener : SimpleListenerHost() {
+
+ /**
+ * 显示服务器列表
+ *
+ */
+ @EventHandler
+ fun GroupMessageEvent.list() {
+ if (message.contentToString() != PluginData.commandMap[CommandName.LIST]) return
+ val msg = if (serverMap.isEmpty()) {
+ "无服务器列表..."
+ } else {
+ var names = ""
+ for ((name, address) in serverMap) {
+ names += "[$name]${address.address}:${address.port}\n"
+ }
+ "服务器列表为:\n$names"
+ }
+ launch { group.sendMessage(msg) }
+ }
+
+ /**
+ * ping 所有服务器
+ *
+ */
+ @EventHandler
+ fun GroupMessageEvent.pingAll() {
+ if (message.contentToString() != PluginData.commandMap[CommandName.PING_ALL]) return
+ if (PluginData.isAllToImg) {
+ var imgMessage = ""
+ serverMap.forEach {
+ imgMessage += pingServer(it.value.address, it.value.port, it.key)
+ imgMessage += "\n\n\n"
+ }
+ launch {
+ val img = group.uploadImage(imgMessage.trimEnd().toImage().toInput(), "png")
+ group.sendMessage(img)
+ }
+ } else {
+ val message = buildForwardMessage {
+ serverMap.forEach {
+ bot says (pingServer(it.value.address, it.value.port, it.key))
+ }
+ }
+ launch {group.sendMessage(message)}
+ }
+ }
+
+ @EventHandler
+ fun GroupMessageEvent.pingAddress(){
+ val content = message.contentToString()
+ val match = """^!ping\s?(.*)\s([0-9]*)""".toRegex().find(content) ?: return
+ val (address,port) = match.destructured
+ launch {group.sendMessage(pingServer(address, port.toInt(), address))}
+ }
+
+ @EventHandler
+ fun GroupMessageEvent.ping() {
+ val content = message.contentToString()
+ val match = """^${PluginData.commandMap[CommandName.PING]}\s?(.*)""".toRegex().find(content) ?: return
+ val (name) = match.destructured
+ serverMap[name]?.run {
+ launch { group.sendMessage(pingServer(address, port, name) ) }
+ }
+ }
+
+ /**
+ * ping 服务器
+ *
+ * @param address 地址
+ * @param port 端口
+ * @param name 昵称
+ * @return
+ */
+ private fun pingServer(address: String, port: Int, name: String): Message {
+ return try {
+ val (delay, serverStatus) = ping(address, port)
+ serverStatus.toMessage(name, delay)
+ } catch (e: TimeoutException) {
+ Minecraft.logger.error("获取ping信息,等待超时...")
+ return PlainText("获取ping信息,等待超时...")
+ } catch (e: ExecutionException) {
+ Minecraft.logger.error("获取ping信息失败,${e.message}")
+ return PlainText("获取ping信息失败,${e.message}")
+ } catch (e: NullPointerException) {
+ Minecraft.logger.error("服务器正在启动请稍后。。。")
+ return PlainText("服务器正在启动请稍后。。。")
+ }
+ }
+
+ /**
+ * 把服务器信息转成消息
+ *
+ * @param name 昵称
+ * @param delay 服务器延迟
+ *
+ * @return [Message]
+ */
+ private fun ServerStatus.toMessage(name: String, delay: Int): Message {
+ var sampleName = ""
+ playerInfo.players.forEach { sampleName += "[${it.name}] " }
+
+ var serverList = ""
+ serverMap.forEach { serverList += "[${it.key}] " }
+
+ return PlainText(
+ "服务器信息如下:\n" +
+ "名 称: $name\n" +
+ "延 迟: $delay ms\n" +
+ "版 本: ${versionName}\n" +
+ "描 述: ${descriptionColourHandle(description)}\n" +
+ "在线人数: ${playerInfo.playerOnline}/${playerInfo.playerMax}\n" +
+ "$sampleName\n" +
+ "mod个数: ${forgeData.mods.size}\n" +
+ "服务器列表:$serverList"
+ )
+ }
+
+ /**
+ * 服务器描述颜色处理
+ */
+ private fun descriptionColourHandle(description: String): String {
+ return description.replace("""§[\da-z]""".toRegex(), "")
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/PluginCompositeCommand.kt b/src/main/kotlin/mirai/PluginCompositeCommand.kt
similarity index 56%
rename from src/main/kotlin/PluginCompositeCommand.kt
rename to src/main/kotlin/mirai/PluginCompositeCommand.kt
index c4c1778..630a48a 100644
--- a/src/main/kotlin/PluginCompositeCommand.kt
+++ b/src/main/kotlin/mirai/PluginCompositeCommand.kt
@@ -7,18 +7,16 @@
* https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
*/
-package top.limbang.minecraft
+package top.limbang.minecraft.mirai
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.UserCommandSender
-import net.mamoe.mirai.console.command.getGroupOrNull
import net.mamoe.mirai.console.plugin.id
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.event.broadcast
-import top.limbang.minecraft.PluginData.isAllToImg
-import top.limbang.minecraft.PluginData.isTps
-import top.limbang.minecraft.PluginData.serverMap
+import top.limbang.minecraft.mirai.PluginData.isAllToImg
+import top.limbang.minecraft.mirai.PluginData.serverMap
import top.limbang.mirai.event.GroupRenameEvent
@@ -34,63 +32,13 @@ object PluginCompositeCommand : CompositeCommand(Minecraft, "mc") {
}
- @SubCommand
- @Description("查看登陆信息")
- suspend fun CommandSender.loginInfo() {
- var loginInfo = ""
- PluginData.loginMap.forEach {
- loginInfo += "[${it.key}] "
- }
- if (loginInfo.isEmpty())
- sendMessage("无登陆信息...")
- else
- sendMessage("登陆配置信息名称:$loginInfo")
- }
-
- @SubCommand
- @Description("添加登陆信息")
- suspend fun CommandSender.addLogin(
- name: String, authServerUrl: String, sessionServerUrl: String, username: String, password: String
- ) {
- val group = getGroupOrNull()
- if (group != null) {
- sendMessage("此配置不能在群配置.")
- return
- }
- LoginInfo(authServerUrl, sessionServerUrl, username, password)
- .also { PluginData.loginMap[name] = it }
- sendMessage("[$name]登陆配置添加成功.")
- }
-
- @SubCommand
- @Description("删除登陆信息")
- suspend fun CommandSender.deleteLogin(name: String) {
- if (PluginData.loginMap.keys.remove(name)) {
- sendMessage("登陆配置[$name]删除成功.")
- } else {
- sendMessage("登陆配置[$name]删除失败.")
- }
- }
-
@SubCommand
@Description("添加服务器,端口默认 25565")
suspend fun CommandSender.addServer(name: String, address: String, port: Int = 25565) {
- serverMap[name] = ServerAddress(address, port, null)
+ serverMap[name] = ServerAddress(address, port)
sendMessage("服务器[$name]添加成功.")
}
- @SubCommand
- @Description("添加带登陆信息带服务器,端口默认 25565")
- suspend fun CommandSender.addServerLogin(loginName: String, name: String, address: String, port: Int = 25565) {
- val loginInfo = PluginData.loginMap[loginName]
- if (loginInfo == null) {
- sendMessage("[$loginName]该登陆信息尚未配置.")
- return
- }
-
- serverMap[name] = ServerAddress(address, port, loginInfo)
- sendMessage("服务器[$name]添加成功.")
- }
@SubCommand
@Description("删除服务器")
@@ -127,12 +75,6 @@ object PluginCompositeCommand : CompositeCommand(Minecraft, "mc") {
} else false
}
- @SubCommand
- @Description("设置tps功能启用")
- suspend fun CommandSender.setTps(value: Boolean) {
- isTps = value
- sendMessage("tps功能:$isTps")
- }
@SubCommand
@Description("设置All消息转换为图片")
diff --git a/src/main/kotlin/PluginData.kt b/src/main/kotlin/mirai/PluginData.kt
similarity index 61%
rename from src/main/kotlin/PluginData.kt
rename to src/main/kotlin/mirai/PluginData.kt
index b1cfc07..6238e81 100644
--- a/src/main/kotlin/PluginData.kt
+++ b/src/main/kotlin/mirai/PluginData.kt
@@ -7,7 +7,7 @@
* https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
*/
-package top.limbang.minecraft
+package top.limbang.minecraft.mirai
import kotlinx.serialization.Serializable
import net.mamoe.mirai.console.data.AutoSavePluginData
@@ -19,30 +19,13 @@ import net.mamoe.mirai.console.data.value
*
* - [address] 服务器地址
* - [port] 服务器端口
- * - [loginInfo] 登陆信息
*/
@Serializable
-data class ServerAddress(val address: String, val port: Int, val loginInfo: LoginInfo?)
+data class ServerAddress(val address: String, val port: Int)
-/**
- * ### 登陆信息
- *
- * - [authServerUrl] 验证地址
- * - [sessionServerUrl] 会话地址
- * - [username] 用户名
- * - [password] 密码 base64 加密过的
- */
-@Serializable
-data class LoginInfo(
- val authServerUrl: String,
- val sessionServerUrl: String,
- val username: String,
- val password: String
-)
-
enum class CommandName{
- PING,LIST,TPS,PING_ALL
+ PING,LIST,PING_ALL
}
/**
@@ -50,12 +33,8 @@ enum class CommandName{
*/
object PluginData : AutoSavePluginData("minecraft") {
var serverMap: MutableMap by value()
- var loginMap: MutableMap by value()
var commandMap: MutableMap by value()
- @ValueDescription("tps查看,默认打开")
- var isTps: Boolean by value(true)
-
@ValueDescription("All消息是否是图片,默认 false")
var isAllToImg: Boolean by value(false)
}
diff --git a/src/main/kotlin/network/MinecraftInputStream.kt b/src/main/kotlin/network/MinecraftInputStream.kt
new file mode 100644
index 0000000..2171621
--- /dev/null
+++ b/src/main/kotlin/network/MinecraftInputStream.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2023 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
+ */
+
+package top.limbang.minecraft.network
+
+import java.io.DataInput
+import java.io.DataInputStream
+import java.io.IOException
+
+/**
+ * ## 用于从 DataInputStream 中读取 Minecraft 协议相关数据的自定义输入流。
+ *
+ * @property input 内部使用的 DataInputStream 对象
+ */
+class MinecraftInputStream(private val input: DataInputStream) : DataInput by input {
+
+ /**
+ * ## 从 DataInputStream 中读取 Minecraft VarInt。
+ *
+ * VarInt 是一种用于表示不定长度整数的压缩格式,用于减少数据传输时的字节长度。
+ *
+ * VarInt 使用了由低到高的每7位来存储整数值的不同部分,最高位表示是否还有后续字节。
+ *
+ * @return 读取到的 VarInt 值。
+ * @throws RuntimeException 如果 VarInt 超出规定的最大长度(超过5个字节)。
+ */
+ fun readVarInt(): Int {
+ var result = 0
+ var numRead = 0
+ var read: Byte
+ do {
+ read = readByte()
+ result = result or (read.toInt() and 127 shl numRead++ * 7)
+ if (numRead > 5) throw RuntimeException("VarInt too big")
+ } while (read.toInt() and 128 == 128)
+ return result
+ }
+
+ /**
+ * ## 从 DataInputStream 中读取一个 UTF-8 编码字符串。
+ *
+ * 默认情况下,该方法读取的字符串长度受到 Short.MAX_VALUE 的限制。
+ *
+ * @return 读取到的 UTF-8 编码字符串。
+ */
+ override fun readUTF(): String {
+ return readUTF(Short.MAX_VALUE.toInt())
+ }
+
+ /**
+ * ## 从 DataInputStream 中读取一个 UTF-8 编码字符串。
+ *
+ * @param maxLength 最大允许的字符串长度。
+ * @return 读取到的 UTF-8 编码字符串。
+ * @throws IOException 如果读取到的字符串长度无效。
+ */
+ fun readUTF(maxLength: Int): String {
+ val length = readVarInt()
+ if (length < 0 || length > maxLength) throw IOException("Invalid string length.")
+ val bytes = ByteArray(length)
+ readFully(bytes)
+ return String(bytes, Charsets.UTF_8)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/network/MinecraftOutputStream.kt b/src/main/kotlin/network/MinecraftOutputStream.kt
new file mode 100644
index 0000000..f72ae43
--- /dev/null
+++ b/src/main/kotlin/network/MinecraftOutputStream.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
+ */
+
+package top.limbang.minecraft.network
+
+import java.io.DataOutput
+import java.io.DataOutputStream
+
+/**
+ * ## 用于将 Minecraft 协议相关数据写入 DataOutputStream 的自定义输出流。
+ *
+ * @property output 内部使用的 DataOutputStream 对象
+ */
+class MinecraftOutputStream(private val output: DataOutputStream) : DataOutput by output {
+
+ /**
+ * ## 将整数值编码成 VarInt,并写入到 DataOutputStream 中。
+ *
+ * VarInt 是一种用于表示不定长度整数的压缩格式,用于减少数据传输时的字节长度。
+ *
+ * VarInt 使用了由低到高的每7位来存储整数值的不同部分,最高位表示是否还有后续字节。
+ *
+ * @param value 待编码的整数值。
+ */
+ fun writeVarInt(value: Int) {
+ var valueCopy = value
+ do {
+ var temp = valueCopy and 127
+ valueCopy = valueCopy ushr 7
+ if (valueCopy != 0) temp = temp or 128
+ writeByte(temp)
+ } while (valueCopy != 0)
+ }
+
+ /**
+ * ## 写入 UTF-8 编码字符串到 DataOutputStream 中。
+ *
+ * @param value 待写入的 UTF-8 编码字符串。
+ */
+ override fun writeUTF(value: String) {
+ val bytes = value.toByteArray(Charsets.UTF_8)
+ writeVarInt(bytes.size)
+ write(bytes)
+ }
+
+ fun size() = output.size()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/service/ImageService.kt b/src/main/kotlin/service/ImageService.kt
deleted file mode 100644
index 446fbfd..0000000
--- a/src/main/kotlin/service/ImageService.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2020-2022 limbang and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
- */
-
-package top.limbang.minecraft.service
-
-import top.limbang.minecraft.Minecraft
-import top.limbang.minecraft.extension.addSubtitles
-import top.limbang.minecraft.extension.readImage
-import top.limbang.minecraft.extension.toInput
-import java.io.ByteArrayInputStream
-
-object ImageService {
-
- private val errorMsgList = listOf(
- "希望睡醒服务器就好了.",
- "你把服务器玩坏了!!!",
- "等我喝口奶在试试.",
- "服务器连接不上,完蛋了.",
- "你干了啥?为啥连接不上.",
- "服务器故障!服务器故障!"
- )
-
- /**
- * ### 创建错误图片
- * @param name 提醒人的名称
- */
- fun createErrorImage(name: String): ByteArrayInputStream {
- val randoms = (0..5).random()
- val subtitles = name + errorMsgList[randoms]
- return createSubtitlesImage(subtitles, "$randoms.jpg")
- }
-
- /**
- * ### 创建字幕图片
- * @param subtitles 字幕
- * @param resourceName 资源图片名称
- */
- fun createSubtitlesImage(subtitles: String, resourceName: String): ByteArrayInputStream {
- val resourceImageStream =
- Minecraft.getResourceAsStream(resourceName) ?: throw RuntimeException("读取不到资源文件.")
- val image = resourceImageStream.use { resourceImageStream -> resourceImageStream.readImage() }
- return image.addSubtitles(subtitles).toInput()
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/service/ServerService.kt b/src/main/kotlin/service/ServerService.kt
deleted file mode 100644
index b1a0ff8..0000000
--- a/src/main/kotlin/service/ServerService.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2020-2022 limbang and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
- */
-
-package top.limbang.minecraft.service
-
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import net.mamoe.mirai.contact.Contact.Companion.sendImage
-import net.mamoe.mirai.contact.Contact.Companion.uploadImage
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.event.events.GroupMessageEvent
-import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.message.data.PlainText
-import net.mamoe.mirai.message.data.buildForwardMessage
-import top.fanua.doctor.client.MinecraftClient
-import top.fanua.doctor.client.entity.ServerInfo
-import top.fanua.doctor.client.running.AutoVersionForgePlugin
-import top.fanua.doctor.client.running.tps.TpsPlugin
-import top.fanua.doctor.client.running.tps.tpsTools
-import top.fanua.doctor.client.utils.ServerInfoUtils
-import top.fanua.doctor.client.utils.substringBetween
-import top.fanua.doctor.network.handler.onPacket
-import top.fanua.doctor.network.handler.oncePacket
-import top.fanua.doctor.plugin.fix.PluginFix
-import top.fanua.doctor.protocol.definition.play.client.JoinGamePacket
-import top.fanua.doctor.protocol.definition.play.client.PlayerPositionAndLookPacket
-import top.limbang.minecraft.PluginData
-import top.limbang.minecraft.PluginData.serverMap
-import top.limbang.minecraft.extension.toImage
-import top.limbang.minecraft.extension.toInput
-import top.limbang.minecraft.service.ImageService.createSubtitlesImage
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-
-object ServerService {
-
- /**
- * ping 所有服务器
- *
- * @return
- */
- suspend fun GroupMessageEvent.pingALlServer(): Message {
- if (PluginData.isAllToImg) {
- var imgMessage = ""
- serverMap.forEach {
- imgMessage += pingServer(it.value.address, it.value.port, it.key)
- ?: PlainText("[${it.key}]服务器连接失败...")
- imgMessage += "\n\n\n"
- }
- return group.uploadImage(imgMessage.trimEnd().toImage().toInput(),"png")
- } else {
- return buildForwardMessage {
- serverMap.forEach {
- bot says (pingServer(it.value.address, it.value.port, it.key)
- ?: PlainText("[${it.key}]服务器连接失败...\n"))
- }
- }
- }
- }
-
- /**
- * 根据预设好的名称 ping 服务器
- *
- * @param name 名称
- * @return
- */
- fun pingServer(name: String): Any? {
- if (name.isEmpty()) return Unit
- val server = serverMap[name] ?: return Unit
- return pingServer(server.address, server.port, name)
- }
-
- /**
- * ping 服务器
- *
- * @param address 地址
- * @param port 端口
- * @param name 昵称
- * @return
- */
- fun pingServer(address: String, port: Int, name: String): Message? {
- return try {
- ServerInfoUtils.getServiceInfo(
- MinecraftClient.ping(address, port).get(5000, TimeUnit.MILLISECONDS)
- ).toMessage(name)
- } catch (e: TimeoutException) {
- println("获取ping信息,等待超时...")
- return null
- } catch (e: ExecutionException) {
- println("获取ping信息失败,${e.message}")
- return null
- } catch (e: NullPointerException) {
- println("服务器正在启动请稍后。。。")
- return null
- }
- }
-
- /**
- * 把服务器信息转成消息
- *
- * @param name 昵称
- * @return [Message]
- */
- private fun ServerInfo.toMessage(name: String): Message {
- var sampleName = ""
- playerNameList.forEach { sampleName += "[${it}] " }
-
- var serverList = ""
- serverMap.forEach { serverList += "[${it.key}] " }
-
- return PlainText(
- "服务器信息如下:\n" +
- "名 称: $name\n" +
- "版 本: ${versionName}\n" +
- "描 述: ${descriptionColourHandle(description)}\n" +
- "在线人数: ${playerOnline}/${playerMax}\n" +
- "$sampleName\n" +
- "mod个数: ${modNumber}\n" +
- "服务器列表:$serverList"
- )
- }
-
-
- suspend fun getTPS(name: String, group: Group, sender: String) {
- val serverInfo = serverMap[name] ?: return
- if (serverInfo.loginInfo == null) {
- group.sendMessage("服务器[$name]未配置登陆信息...")
- return
- }
-
- val client = MinecraftClient.builder()
- .user(serverInfo.loginInfo.username, serverInfo.loginInfo.password)
- .authServerUrl(serverInfo.loginInfo.authServerUrl)
- .sessionServerUrl(serverInfo.loginInfo.sessionServerUrl)
- .plugin(AutoVersionForgePlugin())
- .plugin(TpsPlugin())
- .plugin(PluginFix())
- .build()
-
- if (!client.start(serverInfo.address, serverInfo.port, 5000)) {
- group.sendImage(createSubtitlesImage("$sender,好像获取失败了哦!!!", "9.jpg"))
- return
- }
-
- client.oncePacket {
- runBlocking {
- launch {
- group.sendMessage("登陆[$name]成功,开始发送 forge tps 指令")
- }
- }
- }.onPacket {
- runBlocking {
- launch {
- val forgeTps = client.tpsTools.getTpsSuspend()
- var outMsg = "[$name]低于20TPS的维度如下:\n"
- forgeTps.forEach { tpsEntity ->
- val dim = tpsEntity.dim.substringBetween("Dim", "(").trim()
- outMsg += when {
- tpsEntity.dim == "Overall" -> "\n全局TPS:${tpsEntity.tps} Tick时间:${tpsEntity.tickTime}"
- tpsEntity.tps < 20 -> "TPS:%-4.4s 维度:%s\n".format(tpsEntity.tps, dim)
- else -> ""
- }
- }
- group.sendMessage(outMsg)
- client.stop()
- }
- }
- }
- }
-
- /**
- * ### 获取服务器列表
- */
- fun getServerList(): String {
- if (serverMap.isEmpty())
- return "无服务器列表..."
- var names = ""
- for ((name, address) in serverMap) {
- names += "[$name]${address.address}:${address.port}\n"
- }
- return "服务器列表为:\n$names"
- }
-
- /**
- * 服务器描述颜色处理
- */
- private fun descriptionColourHandle(description: String): String {
- return description.replace("""§[\da-z]""".toRegex(), "")
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/utlis/ForgeUtils.kt b/src/main/kotlin/utlis/ForgeUtils.kt
new file mode 100644
index 0000000..4fb0c68
--- /dev/null
+++ b/src/main/kotlin/utlis/ForgeUtils.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2023 limbang and contributors.
+ *
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
+ */
+
+package top.limbang.minecraft.utlis
+
+import kotlinx.serialization.json.*
+import top.limbang.minecraft.entity.Channel
+import top.limbang.minecraft.entity.ForgeData
+import top.limbang.minecraft.entity.Mod
+import top.limbang.minecraft.network.MinecraftInputStream
+import top.limbang.minecraft.network.MinecraftOutputStream
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.DataInputStream
+import java.io.DataOutputStream
+
+
+/**
+ * 将经过优化的字符串解码为字节数组。
+ *
+ * @param s 经过优化的字符串
+ * @return 解码后的字节数组
+ */
+private fun decodeOptimized(s: String): ByteArray {
+ val byteArray = ByteArrayOutputStream()
+ val output = MinecraftOutputStream(DataOutputStream(byteArray))
+ val size0 = s[0].code
+ val size1 = s[1].code
+ val size = size0 or (size1 shl 15)
+
+ var stringIndex = 2
+ var buffer = 0 // 我们最多需要 8 + 14 = 22 位的缓冲区,因此使用 int 类型足够
+ var bitsInBuf = 0
+ while (stringIndex < s.length) {
+ while (bitsInBuf >= 8) {
+ output.writeByte(buffer)
+ buffer = buffer ushr 8
+ bitsInBuf -= 8
+ }
+ val c = s[stringIndex]
+ buffer = buffer or (c.code and 0x7FFF shl bitsInBuf)
+ bitsInBuf += 15
+ stringIndex++
+ }
+
+ // 写入剩余的数据
+ while (output.size() < size) {
+ output.writeByte(buffer)
+ buffer = buffer ushr 8
+ bitsInBuf -= 8
+ }
+
+ return byteArray.toByteArray()
+}
+
+/**
+ * 忽略服务器端标记的版本标志
+ */
+private const val VERSION_FLAG_IGNORE_SERVER_ONLY = 0b1
+private const val IGNORE_SERVER_ONLY = "SERVER_ONLY"
+
+/**
+ * 将解码后的字节数组反序列化为 ForgeData 对象。
+ *
+ * @param fmlNetworkVersion Forge 网络版本号
+ * @param decodedData 解码后的字节数组
+ * @return 反序列化后的 ForgeData 对象
+ */
+private fun deserializeOptimized(fmlNetworkVersion: Int, decodedData: ByteArray): ForgeData {
+ val input = MinecraftInputStream(DataInputStream(ByteArrayInputStream(decodedData)))
+ val truncated = input.readBoolean()
+ val mods = mutableListOf()
+ val channels = mutableListOf()
+ val modsSize = input.readUnsignedShort()
+ // 解析 mod 的id和版本
+ for (i in 0.. 383 -> forgeDataObject.jsonObject["fmlNetworkVersion"]!!.jsonPrimitive.int
+ element.jsonObject["modinfo"] != null && versionNumber <= 340 -> 1
+ else -> 0
+ }
+ return when (fmlNetworkVersion) {
+ 3 -> forgeNetwork3(forgeDataObject!!)
+ 2 -> forgeNetwork2(forgeDataObject!!)
+ 1 -> forgeNetwork1(element.jsonObject["modinfo"]!!)
+ else -> ForgeData(-1, false, listOf(), listOf())
+ }
+}
+
+/**
+ * 解析 fmlNetworkVersion 为 3 的 ForgeData 对象。
+ * - 1.18.x - 1.20.2 forge 使用的此方法
+ *
+ * @param forgeDataObject fmlNetworkVersion 为 3 的 ForgeData JSON 元素
+ * @return 对应的 [ForgeData] 对象
+ */
+private fun forgeNetwork3(forgeDataObject: JsonElement): ForgeData {
+ val decodedData = forgeDataObject.jsonObject["d"]!!.jsonPrimitive.content
+ return deserializeOptimized(3, decodeOptimized(decodedData))
+}
+
+/**
+ * 解析 fmlNetworkVersion 为 2 的 ForgeData 对象。
+ * - 1.16.x - 1.17,x forge 使用的此方法
+ * - 1.13.x - 1.15.x 兼容
+ *
+ * @param forgeDataObject fmlNetworkVersion 为 2 的 ForgeData JSON 元素
+ * @return 对应的 [ForgeData] 对象
+ */
+private fun forgeNetwork2(forgeDataObject: JsonElement): ForgeData {
+ val channels = forgeDataObject.jsonObject["channels"]!!.jsonArray.map {
+ val res = it.jsonObject["res"]!!.jsonPrimitive.content
+ val version = it.jsonObject["version"]!!.jsonPrimitive.content
+ val required = it.jsonObject["required"]!!.jsonPrimitive.boolean
+ Channel(name = res, version = version, requiredOnClient = required)
+ }
+
+ val mods = forgeDataObject.jsonObject["mods"]!!.jsonArray.map {
+ val modId = it.jsonObject["modId"]!!.jsonPrimitive.content
+ val modmarker = it.jsonObject["modmarker"]!!.jsonPrimitive.content
+ Mod(id = modId, version = modmarker)
+ }
+
+ // 兼容 1.13.x - 1.15.x 找不到就默认 false
+ val truncated = forgeDataObject.jsonObject["truncated"]?.jsonPrimitive?.boolean ?: false
+ return ForgeData(fmlNetworkVersion = 2, truncated = truncated, mods = mods, channels = channels)
+}
+
+/**
+ * 解析 fmlNetworkVersion 为 1 的 ForgeData 对象。
+ *
+ * - 1.8.8 - 1.12.x forge 使用的此方法
+ * - 1.7.10 也兼容
+ *
+ * @param forgeDataObject fmlNetworkVersion 为 1 的 ForgeData JSON 元素
+ * @return 对应的 [ForgeData] 对象
+ */
+private fun forgeNetwork1(forgeDataObject: JsonElement): ForgeData {
+ val mods = forgeDataObject.jsonObject["modList"]!!.jsonArray.map {
+ val modId = it.jsonObject["modid"]!!.jsonPrimitive.content
+ val version = it.jsonObject["version"]!!.jsonPrimitive.content
+ Mod(id = modId, version = version)
+ }
+ return ForgeData(fmlNetworkVersion = 2, truncated = false, mods = mods, channels = listOf())
+}
\ No newline at end of file
diff --git a/src/main/kotlin/extension/ImageExtension.kt b/src/main/kotlin/utlis/ImageUtils.kt
similarity index 68%
rename from src/main/kotlin/extension/ImageExtension.kt
rename to src/main/kotlin/utlis/ImageUtils.kt
index f10e2d5..92bbbe9 100644
--- a/src/main/kotlin/extension/ImageExtension.kt
+++ b/src/main/kotlin/utlis/ImageUtils.kt
@@ -1,24 +1,63 @@
/*
- * Copyright 2020-2022 limbang and contributors.
+ * Copyright (c) 2023 limbang and contributors.
*
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
+ * 此源代码的使用受 GNU AGPLv3 许可证的约束,该许可证可在"LICENSE"文件中找到。
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found in the "LICENSE" file.
*/
-package top.limbang.minecraft.extension
-
+package top.limbang.minecraft.utlis
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.util.*
import javax.imageio.ImageIO
+
+/**
+ * 解码 Minecraft 服务器图标
+ *
+ * @param favicon
+ */
+fun decodeFavicon(favicon: String): ByteArray {
+ // 如果图标为空读取默认图标
+ if (favicon.isEmpty()) {
+ val imageStream = object {}::class.java.classLoader.getResourceAsStream("images/defaultFavicon.png")!!
+ return imageStream.use { inputStream ->
+ ByteArrayOutputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ outputStream.toByteArray()
+ }
+ }
+ }
+ val prefix = "data:image/png;base64,"
+ if (!favicon.startsWith(prefix)) throw RuntimeException("Unknown format")
+ val faviconBase64 = favicon.substring(prefix.length).replace("\n", "")
+ val byte = try {
+ Base64.getDecoder().decode(faviconBase64.toByteArray(StandardCharsets.UTF_8))
+ } catch (e: Exception) {
+ throw RuntimeException("Malformed base64 server icon")
+ }
+ return byte
+}
+
+/**
+ * 编码 Minecraft 服务器图标
+ *
+ * @param byte
+ */
+fun encodeFavicon(byte: ByteArray): String {
+ val prefix = "data:image/png;base64,"
+ val faviconBase64 = Base64.getEncoder().encodeToString(byte)
+ return prefix + faviconBase64
+}
+
/**
* ### 添加字幕
* @param subtitles 字幕
@@ -110,4 +149,12 @@ private fun textToImage(text: String): ByteArrayOutputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
ImageIO.write(newImage, "png", byteArrayOutputStream)
return byteArrayOutputStream
-}
\ No newline at end of file
+}
+
+/**
+ * ### ByteArrayOutputStream 转 ByteArrayInputStream
+ *
+ */
+fun ByteArrayOutputStream.toInput(): ByteArrayInputStream {
+ return ByteArrayInputStream(this.toByteArray())
+}
diff --git a/src/main/resources/0.jpg b/src/main/resources/0.jpg
deleted file mode 100644
index e726437..0000000
Binary files a/src/main/resources/0.jpg and /dev/null differ
diff --git a/src/main/resources/1.jpg b/src/main/resources/1.jpg
deleted file mode 100644
index 252d48c..0000000
Binary files a/src/main/resources/1.jpg and /dev/null differ
diff --git a/src/main/resources/2.jpg b/src/main/resources/2.jpg
deleted file mode 100644
index cc7138f..0000000
Binary files a/src/main/resources/2.jpg and /dev/null differ
diff --git a/src/main/resources/3.jpg b/src/main/resources/3.jpg
deleted file mode 100644
index 200af9f..0000000
Binary files a/src/main/resources/3.jpg and /dev/null differ
diff --git a/src/main/resources/4.jpg b/src/main/resources/4.jpg
deleted file mode 100644
index 1107c9d..0000000
Binary files a/src/main/resources/4.jpg and /dev/null differ
diff --git a/src/main/resources/5.jpg b/src/main/resources/5.jpg
deleted file mode 100644
index bbc7a99..0000000
Binary files a/src/main/resources/5.jpg and /dev/null differ
diff --git a/src/main/resources/9.jpg b/src/main/resources/9.jpg
deleted file mode 100644
index 9b5a33f..0000000
Binary files a/src/main/resources/9.jpg and /dev/null differ
diff --git a/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin
index 256b54e..d227d09 100644
--- a/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin
+++ b/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin
@@ -7,4 +7,4 @@
# https://github.com/limbang/mirai-console-minecraft-plugin/blob/master/LICENSE
#
-top.limbang.minecraft.Minecraft
\ No newline at end of file
+top.limbang.minecraft.mirai.Minecraft
\ No newline at end of file