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