From 5f082079bf7a115d32cf36827531ec68f17923f1 Mon Sep 17 00:00:00 2001
From: limbang <495071565@qq.com>
Date: Thu, 28 Dec 2023 00:08:21 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E4=B9=8B=E5=89=8D?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=9A=84=E7=99=BB=E5=BD=95=E5=8D=8F=E8=AE=AE?=
=?UTF-8?q?=E5=BA=93=20=E6=94=B9=E6=88=90=E8=87=AA=E5=B7=B1=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0=E7=AE=80=E5=8D=95=E8=8E=B7=E5=8F=96=E6=9C=8D=E5=8A=A1?=
=?UTF-8?q?=E5=99=A8=E4=BF=A1=E6=81=AF=20build:=201.2.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 33 +--
build.gradle.kts | 11 +-
src/main/kotlin/ServerListPing.kt | 169 +++++++++++++++
src/main/kotlin/entity/ServerStatus.kt | 78 +++++++
.../extension/ByteArrayStreamExtension.kt | 21 --
src/main/kotlin/{ => mirai}/Minecraft.kt | 37 +---
src/main/kotlin/mirai/MinecraftListener.kt | 153 ++++++++++++++
.../{ => mirai}/PluginCompositeCommand.kt | 66 +-----
src/main/kotlin/{ => mirai}/PluginData.kt | 27 +--
.../kotlin/network/MinecraftInputStream.kt | 68 ++++++
.../kotlin/network/MinecraftOutputStream.kt | 51 +++++
src/main/kotlin/service/ImageService.kt | 50 -----
src/main/kotlin/service/ServerService.kt | 197 ------------------
src/main/kotlin/utlis/ForgeUtils.kt | 183 ++++++++++++++++
.../ImageExtension.kt => utlis/ImageUtils.kt} | 63 +++++-
src/main/resources/0.jpg | Bin 17301 -> 0 bytes
src/main/resources/1.jpg | Bin 21576 -> 0 bytes
src/main/resources/2.jpg | Bin 23015 -> 0 bytes
src/main/resources/3.jpg | Bin 34119 -> 0 bytes
src/main/resources/4.jpg | Bin 34198 -> 0 bytes
src/main/resources/5.jpg | Bin 28002 -> 0 bytes
src/main/resources/9.jpg | Bin 35314 -> 0 bytes
...t.mamoe.mirai.console.plugin.jvm.JvmPlugin | 2 +-
23 files changed, 775 insertions(+), 434 deletions(-)
create mode 100644 src/main/kotlin/ServerListPing.kt
create mode 100644 src/main/kotlin/entity/ServerStatus.kt
delete mode 100644 src/main/kotlin/extension/ByteArrayStreamExtension.kt
rename src/main/kotlin/{ => mirai}/Minecraft.kt (62%)
create mode 100644 src/main/kotlin/mirai/MinecraftListener.kt
rename src/main/kotlin/{ => mirai}/PluginCompositeCommand.kt (56%)
rename src/main/kotlin/{ => mirai}/PluginData.kt (61%)
create mode 100644 src/main/kotlin/network/MinecraftInputStream.kt
create mode 100644 src/main/kotlin/network/MinecraftOutputStream.kt
delete mode 100644 src/main/kotlin/service/ImageService.kt
delete mode 100644 src/main/kotlin/service/ServerService.kt
create mode 100644 src/main/kotlin/utlis/ForgeUtils.kt
rename src/main/kotlin/{extension/ImageExtension.kt => utlis/ImageUtils.kt} (68%)
delete mode 100644 src/main/resources/0.jpg
delete mode 100644 src/main/resources/1.jpg
delete mode 100644 src/main/resources/2.jpg
delete mode 100644 src/main/resources/3.jpg
delete mode 100644 src/main/resources/4.jpg
delete mode 100644 src/main/resources/5.jpg
delete mode 100644 src/main/resources/9.jpg
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 e72643788db49ae29208eb735429727ddf29235d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 17301
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
diff --git a/src/main/resources/1.jpg b/src/main/resources/1.jpg
deleted file mode 100644
index 252d48c6ca61903a40e0d52a6ba7a522a7ad9e61..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 21576
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
diff --git a/src/main/resources/2.jpg b/src/main/resources/2.jpg
deleted file mode 100644
index cc7138fffadae06cfbdde431eea11a59b87a3fd4..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 23015
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