Browse Source

Move `_contentToString` outside mirai-core main sourceSets, and rename it to `structureToString`

Him188 4 years ago
parent
commit
bf98ab7858

+ 2 - 2
mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt

@@ -31,7 +31,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn
 import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
 import net.mamoe.mirai.internal.utils.AtomicIntSeq
 import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache
-import net.mamoe.mirai.internal.utils._miraiContentToString
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.*
@@ -112,7 +112,7 @@ internal sealed class AbstractUser(
                             } else {
                                 throw contextualBugReportException(
                                     "Failed to compute friend image image from resourceId: ${resp.resourceId}",
-                                    resp._miraiContentToString(),
+                                    resp.structureToString(),
                                     additional = "并附加此时正在上传的文件"
                                 )
                             }

+ 2 - 2
mirai-core/src/commonMain/kotlin/message/imagesImpl.kt

@@ -21,9 +21,9 @@ import net.mamoe.mirai.contact.ContactOrBot
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.User
 import net.mamoe.mirai.internal.network.protocol.data.proto.*
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX
 import net.mamoe.mirai.utils.*
@@ -109,7 +109,7 @@ OnlineFriendImage() {
                     Image.logger.warning(
                         contextualBugReportException(
                             "Failed to compute friend imageId: resId=${delegate.resId}",
-                            delegate._miraiContentToString(),
+                            delegate.structureToString(),
                             additional = "并描述此时 Bot 是否正在从好友或群接受消息, 尽量附加该图片原文件"
                         )
                     )

+ 4 - 4
mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt

@@ -25,8 +25,8 @@ import net.mamoe.mirai.internal.getGroupByUinOrCodeOrFail
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
 import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSourceKind
 import net.mamoe.mirai.message.data.OnlineMessageSource
@@ -177,10 +177,10 @@ internal class OnlineMessageSourceFromGroupImpl(
 
     override val subject: GroupImpl by lazy {
         val groupCode = msg.first().msgHead.groupInfo?.groupCode
-            ?: error("cannot find groupCode for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
+            ?: error("cannot find groupCode for OnlineMessageSourceFromGroupImpl. msg=${msg.structureToString()}")
 
         val group = bot.getGroup(groupCode)?.checkIsGroupImpl()
-            ?: error("cannot find group for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
+            ?: error("cannot find group for OnlineMessageSourceFromGroupImpl. msg=${msg.structureToString()}")
 
         group
     }
@@ -192,7 +192,7 @@ internal class OnlineMessageSourceFromGroupImpl(
         if (member != null) return@lazy member
 
         val anonymousInfo = msg.first().msgBody.richText.elems.firstOrNull { it.anonGroupMsg != null }
-            ?: error("cannot find member for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}")
+            ?: error("cannot find member for OnlineMessageSourceFromGroupImpl. msg=${msg.structureToString()}")
 
         anonymousInfo.run {
             group.newAnonymous(anonGroupMsg!!.anonNick.decodeToString(), anonGroupMsg.anonId.encodeBase64())

+ 4 - 4
mirai-core/src/commonMain/kotlin/network/notice/UnconsumedNoticesAlerter.kt

@@ -20,7 +20,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush
 import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans
 import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg
 import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact
-import net.mamoe.mirai.internal.utils._miraiContentToString
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.utils.*
 
 internal class UnconsumedNoticesAlerter(
@@ -81,7 +81,7 @@ internal class UnconsumedNoticesAlerter(
             logger.debug(
                 contextualBugReportException(
                     "解析 OnlinePush.PbPushTransMsg, msgType=${data.msgType}",
-                    data._miraiContentToString(),
+                    data.structureToString(),
                     null,
                     "并描述此时机器人是否被踢出, 或是否有成员列表变更等动作.",
                 )
@@ -127,7 +127,7 @@ internal class UnconsumedNoticesAlerter(
             data.msg?.context {
                 throw contextualBugReportException(
                     "解析 NewContact.SystemMsgNewGroup, subType=$subType, groupMsgType=$groupMsgType",
-                    forDebug = this._miraiContentToString(),
+                    forDebug = this.structureToString(),
                     additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群",
                 )
             }
@@ -139,7 +139,7 @@ internal class UnconsumedNoticesAlerter(
         if (logger.isEnabled && logger.isDebugEnabled) {
             throw contextualBugReportException(
                 "decode SvcRequestPushStatus (PC Client status change)",
-                data._miraiContentToString(),
+                data.structureToString(),
                 additional = "unknown status=${data.status}",
             )
         }

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/notice/group/GroupNotificationProcessor.kt

@@ -28,9 +28,9 @@ import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210
 import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x122
 import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x27
 import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.parseToMessageDataList
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.utils.*
 
 internal class GroupNotificationProcessor(
@@ -362,7 +362,7 @@ internal class GroupNotificationProcessor(
             else -> {
                 markNotConsumed()
                 logger.debug {
-                    "Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?._miraiContentToString()}"
+                    "Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?.structureToString()}"
                 }
             }
         }

+ 4 - 4
mirai-core/src/commonMain/kotlin/network/notice/group/GroupOrMemberListNoticeProcessor.kt

@@ -33,9 +33,9 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
 import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans
 import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg
 import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x44
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.parseToMessageDataList
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.internal.utils.toMemberInfo
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.context
@@ -212,7 +212,7 @@ internal class GroupOrMemberListNoticeProcessor(
                 } else {
                     throw contextualBugReportException(
                         "解析 NewContact.SystemMsgNewGroup, subType=5, groupMsgType=$groupMsgType",
-                        data._miraiContentToString(),
+                        data.structureToString(),
                         null,
                         "并描述此时机器人是否被邀请加入群等其他",
                     )
@@ -244,7 +244,7 @@ internal class GroupOrMemberListNoticeProcessor(
                 }
                 else -> throw contextualBugReportException(
                     "parse SystemMsgNewGroup, subType=1",
-                    this._miraiContentToString(),
+                    this.structureToString(),
                     additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群"
                 )
             }
@@ -287,7 +287,7 @@ internal class GroupOrMemberListNoticeProcessor(
                     else -> {
                         throw contextualBugReportException(
                             "解析 NewContact.SystemMsgNewGroup, subType=5, groupMsgType=$groupMsgType",
-                            this._miraiContentToString(),
+                            this.structureToString(),
                             null,
                             "并描述此时机器人是否被踢出群等",
                         )

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/notice/priv/FriendNoticeProcessor.kt

@@ -36,9 +36,9 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0xb3.SubMs
 import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact
 import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList.GetFriendGroupList
 import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.utils.*
 
 /**
@@ -216,7 +216,7 @@ internal class FriendNoticeProcessor(
             }
         }
         if (body.msgProfileInfos.isEmpty() || containsUnknown) {
-            logger.debug { "Transformers528 0x27L: ProfileChanged new data: ${body._miraiContentToString()}" }
+            logger.debug { "Transformers528 0x27L: ProfileChanged new data: ${body.structureToString()}" }
         }
     }
 

+ 3 - 3
mirai-core/src/commonMain/kotlin/network/notice/priv/OtherClientNoticeProcessor.kt

@@ -31,8 +31,8 @@ import net.mamoe.mirai.internal.network.handler.logger
 import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushStatus
 import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
 import net.mamoe.mirai.internal.network.protocol.data.proto.SubMsgType0x7
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.PlainText
 import net.mamoe.mirai.message.data.buildMessageChain
 import net.mamoe.mirai.utils.context
@@ -70,9 +70,9 @@ internal class OtherClientNoticeProcessor : MixedNoticeProcessor() {
                             bot.network.logger.warning(
                                 contextualBugReportException(
                                     "SvcRequestPushStatus (OtherClient online)",
-                                    "packet: \n" + data._miraiContentToString() +
+                                    "packet: \n" + data.structureToString() +
                                             "\n\nquery: \n" +
-                                            Mirai.getOnlineOtherClientsList(bot)._miraiContentToString(),
+                                            Mirai.getOnlineOtherClientsList(bot).structureToString(),
                                     additional = "Failed to find corresponding instanceInfo.",
                                 ),
                             )

+ 4 - 4
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MultiMsg.kt

@@ -26,10 +26,10 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit
 import net.mamoe.mirai.internal.network.protocol.data.proto.MultiMsg
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.ForwardMessage
 import net.mamoe.mirai.message.data.MessageSource
 import net.mamoe.mirai.message.data.toMessageChain
@@ -120,7 +120,7 @@ internal class MultiMsg {
             ) : Response() {
                 override fun toString(): String {
                     if (PacketCodec.PacketLogger.isEnabled) {
-                        return _miraiContentToString()
+                        return structureToString()
                     }
                     return "MultiMsg.ApplyUp.Response.RequireUpload"
                 }
@@ -168,7 +168,7 @@ internal class MultiMsg {
                 //1 -> Response.OK(resId = response.msgResid)
                 else -> {
                     error(kotlin.run {
-                        println(response._miraiContentToString())
+                        println(response.structureToString())
                     }.let { "Protocol error: MultiMsg.ApplyUp failed with result ${response.result}" })
                 }
             }
@@ -221,7 +221,7 @@ internal class MultiMsg {
                 //1 -> Response.OK(resId = response.msgResid)
                 else -> throw contextualBugReportException(
                     "MultiMsg.ApplyDown",
-                    response._miraiContentToString(),
+                    response.structureToString(),
                     additional = "Decode failure result=${response.result}"
                 )
             }

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/StatSvc.kt

@@ -41,8 +41,8 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcGetOnline
 import net.mamoe.mirai.internal.network.protocol.data.proto.StatSvcSimpleGet
 import net.mamoe.mirai.internal.network.protocol.packet.*
 import net.mamoe.mirai.internal.utils.NetworkType
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.*
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.internal.utils.toIpV4Long
 import net.mamoe.mirai.utils.*
 
@@ -369,7 +369,7 @@ internal class StatSvc {
 
                     else -> throw contextualBugReportException(
                         "decode SvcReqMSFLoginNotify (OtherClient status change)",
-                        notify._miraiContentToString(),
+                        notify.structureToString(),
                         additional = "unknown notify.status=${notify.status}"
                     )
                 }

+ 7 - 7
mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt

@@ -22,9 +22,9 @@ import net.mamoe.mirai.internal.network.protocol.packet.*
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLoginExt
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.analysisTlv0x531
 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.orEmpty
-import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.crypto.TEA
-import net.mamoe.mirai.internal.utils.soutv
+import net.mamoe.mirai.internal.utils.printStructurally
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.utils.*
 
 internal class WtLogin {
@@ -163,7 +163,7 @@ internal class WtLogin {
             val tlvMap: TlvMap = this._readTLVMap()
 
             if (SHOW_TLV_MAP_ON_LOGIN_SUCCESS) {
-                tlvMap.smartToString().soutv("tlvMap outer")
+                tlvMap.smartToString().printStructurally("tlvMap outer")
             }
 
             // tlvMap.printTLVMap()
@@ -185,7 +185,7 @@ internal class WtLogin {
                 // 1, 15 -> onErrorMessage(tlvMap) ?: error("Cannot find error message")
                 else -> {
                     onErrorMessage(type.toInt(), tlvMap, bot)
-                        ?: error("Cannot find error message, unknown login result type: $type, TLVMap = ${tlvMap._miraiContentToString()}")
+                        ?: error("Cannot find error message, unknown login result type: $type, TLVMap = ${tlvMap.structureToString()}")
                 }
             }
         }
@@ -242,7 +242,7 @@ internal class WtLogin {
                 // } else error("UNKNOWN CAPTCHA QUESTION: ${question.toUHexString()}, tlvMap=" + tlvMap.contentToString())
             }
 
-            error("UNKNOWN CAPTCHA, tlvMap=" + tlvMap._miraiContentToString())
+            error("UNKNOWN CAPTCHA, tlvMap=" + tlvMap.structureToString())
         }
 
         fun onLoginSuccess(subCommand: Int, tlvMap: TlvMap, bot: QQAndroidBot): LoginPacketResponse.Success {
@@ -266,7 +266,7 @@ internal class WtLogin {
                     val tlvMap119 = this._readTLVMap()
 
                     if (SHOW_TLV_MAP_ON_LOGIN_SUCCESS) {
-                        tlvMap119.smartToString().soutv("TlvMap119")
+                        tlvMap119.smartToString().printStructurally("TlvMap119")
                     }
 
                     tlvMap119[0x106]?.let { client.analyzeTlv106(it) }
@@ -366,7 +366,7 @@ internal class WtLogin {
                     } ?: emptyMap()
 
                     if (SHOW_TLV_MAP_ON_LOGIN_SUCCESS) {
-                        changeTokenTimeMap._miraiContentToString().soutv("tokenChangeTime")
+                        changeTokenTimeMap.structureToString().printStructurally("tokenChangeTime")
                     }
 
                     val outPSKeyMap: PSKeyMap?

+ 34 - 186
mirai-core/src/commonMain/kotlin/utils/contentToString.kt

@@ -11,205 +11,53 @@
 
 package net.mamoe.mirai.internal.utils
 
-import kotlinx.serialization.Transient
-import net.mamoe.mirai.IMirai
+import net.mamoe.mirai.utils.DeprecatedSinceMirai
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.debug
-import net.mamoe.mirai.utils.toUHexString
-import java.lang.reflect.Modifier
-import kotlin.reflect.KClass
-import kotlin.reflect.KProperty
-import kotlin.reflect.KProperty1
-import kotlin.reflect.full.hasAnnotation
-import kotlin.reflect.jvm.javaField
+import net.mamoe.mirai.utils.loadService
 
 
-private val indent: String = " ".repeat(4)
+internal fun Any?.structureToString(): String = StructureToStringTransformer.instance.transform(this)
 
-/**
- * 将所有元素加入转换为多行的字符串表示.
- */
-private fun <T> Sequence<T>.joinToStringPrefixed(prefix: String, transform: (T) -> CharSequence): String {
-    return this.joinToString(prefix = "$prefix$indent", separator = "\n$prefix$indent", transform = transform)
-}
+@Suppress("FunctionName")
+@Deprecated(
+    "",
+    ReplaceWith("this.structureToString()", "net.mamoe.mirai.internal.utils.structureToString"),
+    level = DeprecationLevel.ERROR
+) // kept for local developers for some time
+@DeprecatedSinceMirai(errorSince = "2.10")
+internal fun Any?._miraiContentToString(): String = this.structureToString()
 
-private val SoutvLogger: MiraiLogger by lazy { MiraiLogger.Factory.create(IMirai::class, "soutv") }
-internal fun Any?.soutv(name: String = "unnamed") {
-    @Suppress("DEPRECATION")
-    SoutvLogger.debug { "$name = ${this._miraiContentToString()}" }
+private val SoutvLogger: MiraiLogger by lazy {
+    MiraiLogger.Factory.create(
+        StructureToStringTransformer::class,
+        "printStructurally"
+    )
 }
 
-/**
- * 将内容格式化为较可读的字符串输出.
- *
- * 各数字类型及其无符号类型: 十六进制表示 + 十进制表示. e.g. `0x1000(4096)`
- * [ByteArray] 和 [UByteArray]: 十六进制表示, 通过 [ByteArray.toUHexString]
- * [Iterable], [Iterator], [Sequence]: 调用各自的 joinToString.
- * [Map]: 多行输出. 每行显示一个值. 递归调用 [_miraiContentToString]. 嵌套结构将会以缩进表示
- * `data class`: 调用其 [toString]
- * 其他类型: 反射获取它和它的所有来自 Mirai 的 super 类型的所有自有属性并递归调用 [_miraiContentToString]. 嵌套结构将会以缩进表示
- */
-@Suppress("FunctionName") // 这样就不容易被 IDE 提示
-internal fun Any?._miraiContentToString(prefix: String = ""): String = when (this) {
-    is Unit -> "Unit"
-    is UInt -> "0x" + this.toUHexString("") + "($this)"
-    is UByte -> "0x" + this.toUHexString() + "($this)"
-    is UShort -> "0x" + this.toUHexString("") + "($this)"
-    is ULong -> "0x" + this.toUHexString("") + "($this)"
-    is Int -> "0x" + this.toUHexString("") + "($this)"
-    is Byte -> "0x" + this.toUHexString() + "($this)"
-    is Short -> "0x" + this.toUHexString("") + "($this)"
-    is Long -> "0x" + this.toUHexString("") + "($this)"
+@Deprecated(
+    "",
+    ReplaceWith("this.printStructurally(name)", "net.mamoe.mirai.internal.utils.printStructurally"),
+    level = DeprecationLevel.ERROR
+)
+@DeprecatedSinceMirai(errorSince = "2.10")
+internal fun Any?.soutv(name: String = "unnamed") = this.printStructurally(name)
 
-    is Boolean -> if (this) "true" else "false"
+internal fun Any?.printStructurally(name: String = "unnamed") {
+    return SoutvLogger.debug { "$name = ${this.structureToString()}" }
+}
 
-    is ByteArray -> {
-        if (this.size == 0) "<Empty ByteArray>"
-        else this.toUHexString()
-    }
-    is UByteArray -> {
-        if (this.size == 0) "<Empty UByteArray>"
-        else this.toUHexString()
-    }
-    is ShortArray -> {
-        if (this.size == 0) "<Empty ShortArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is IntArray -> {
-        if (this.size == 0) "<Empty IntArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is LongArray -> {
-        if (this.size == 0) "<Empty LongArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is FloatArray -> {
-        if (this.size == 0) "<Empty FloatArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is DoubleArray -> {
-        if (this.size == 0) "<Empty DoubleArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is UShortArray -> {
-        if (this.size == 0) "<Empty ShortArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is UIntArray -> {
-        if (this.size == 0) "<Empty IntArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is ULongArray -> {
-        if (this.size == 0) "<Empty LongArray>"
-        else this.iterator()._miraiContentToString()
-    }
-    is Array<*> -> {
-        if (this.size == 0) "<Empty Array>"
-        else this.iterator()._miraiContentToString()
-    }
-    is BooleanArray -> {
-        if (this.size == 0) "<Empty BooleanArray>"
-        else this.iterator()._miraiContentToString()
-    }
+internal fun interface StructureToStringTransformer {
+    fun transform(any: Any?): String
 
-    is Iterable<*> -> this.joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
-    is Iterator<*> -> this.asSequence().joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
-    is Sequence<*> -> this.joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
-    is Map<*, *> -> this.entries.joinToString(
-        prefix = "{",
-        postfix = "}"
-    ) { it.key._miraiContentToString(prefix) + "=" + it.value._miraiContentToString(prefix) }
-    else -> {
-        if (this == null) "null"
-        else if (this::class.isData) this.toString()
-        else {
-            if (this::class.qualifiedName?.startsWith("net.mamoe.mirai.") == true) {
-                this.contentToStringReflectively(prefix + indent)
-            } else this.toString()
-            /*
-            (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + "{\n" +
-                    this::class.members.asSequence().filterIsInstance<KProperty<*>>().filter { !it.isSuspend && it.visibility == KVisibility.PUBLIC }
-                        .joinToStringPrefixed(
-                            prefix = indent
-                        ) { it.name + "=" + kotlin.runCatching { it.call(it).contentToString(indent) }.getOrElse { "<!>" } }
-             */
+    companion object {
+        private class ObjectToStringStructureToStringTransformer : StructureToStringTransformer {
+            override fun transform(any: Any?): String = any.toString()
         }
-    }
-}
 
-internal fun KProperty1<*, *>.getValueAgainstPermission(receiver: Any): Any? {
-    return this.javaField?.apply { isAccessible = true }?.get(receiver)
-}
-
-private fun Any.canBeIgnored(): Boolean {
-    return when (this) {
-        is String -> this.isEmpty()
-        is ByteArray -> this.isEmpty()
-        is Array<*> -> this.isEmpty()
-        is Number -> this == 0
-        is Int -> this == 0
-        is Float -> this == 0
-        is Double -> this == 0
-        is Byte -> this == 0
-        is Short -> this == 0
-        is Long -> this == 0
-        else -> false
+        val instance by lazy {
+            loadService(StructureToStringTransformer::class) { ObjectToStringStructureToStringTransformer() }
+        }
     }
 }
 
-private fun Any.contentToStringReflectively(
-    prefix: String,
-    filter: ((name: String, value: Any?) -> Boolean)? = null,
-): String {
-    val newPrefix = "$prefix    "
-    return (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + " {\n" +
-            this.allMembersFromSuperClassesMatching { it.qualifiedName?.startsWith("net.mamoe.mirai") == true }
-                .distinctBy { it.name }
-                .filterNot { it.name.contains("$") || it.name == "Companion" || it.isConst || it.name == "serialVersionUID" }
-                .mapNotNull {
-                    val value = it.getValueAgainstPermission(this) ?: return@mapNotNull null
-                    if (filter != null) {
-                        if (!filter(it.name, value))
-                            return@mapNotNull it.name to value
-                        else {
-                            return@mapNotNull null
-                        }
-                    }
-                    it.name to value
-                }
-                .joinToStringPrefixed(
-                    prefix = newPrefix
-                ) { (name: String, value: Any?) ->
-                    if (value.canBeIgnored()) ""
-                    else {
-                        "$name=" + kotlin.runCatching {
-                            if (value == this) "<this>"
-                            else value._miraiContentToString(newPrefix)
-                        }.getOrElse { "<!>" }
-                    }
-                }.lines().filterNot { it.isBlank() }.joinToString("\n") + "\n$prefix}"
-}
-
-private fun KClass<out Any>.thisClassAndSuperclassSequence(): Sequence<KClass<out Any>> {
-    return sequenceOf(this) +
-            this.supertypes.asSequence()
-                .mapNotNull { type ->
-                    type.classifier?.takeIf { it is KClass<*> }?.takeIf { it != Any::class } as? KClass<out Any>
-                }.flatMap { it.thisClassAndSuperclassSequence() }
-}
-
-@Suppress("UNCHECKED_CAST")
-private fun Any.allMembersFromSuperClassesMatching(classFilter: (KClass<out Any>) -> Boolean): Sequence<KProperty1<Any, *>> {
-    return this::class.thisClassAndSuperclassSequence()
-        .filter { classFilter(it) }
-        .map { it.members }
-        .flatMap { it.asSequence() }
-        .filterIsInstance<KProperty1<*, *>>()
-        .filterNot { it.hasAnnotation<Transient>() }
-        .filterNot { it.isTransient() }
-        .mapNotNull { it as KProperty1<Any, *> }
-}
-
-internal fun KProperty<*>.isTransient(): Boolean =
-    javaField?.modifiers?.and(Modifier.TRANSIENT) != 0
-

+ 2 - 2
mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt

@@ -24,7 +24,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.OidbSso
 import net.mamoe.mirai.internal.utils.io.JceStruct
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.tars.Tars
-import net.mamoe.mirai.internal.utils.soutv
+import net.mamoe.mirai.internal.utils.printStructurally
 import net.mamoe.mirai.utils.read
 import net.mamoe.mirai.utils.readPacketExact
 import kotlin.contracts.InvocationKind
@@ -171,7 +171,7 @@ internal fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrate
 internal fun <T : ProtoBuf> ByteArray.loadOidb(deserializer: DeserializationStrategy<T>, log: Boolean = false): T {
     val oidb = loadAs(OidbSso.OIDBSSOPkg.serializer())
     if (log) {
-        oidb.soutv("OIDB")
+        oidb.printStructurally("OIDB")
     }
     return oidb.bodybuffer.loadAs(deserializer)
 }

+ 2 - 2
mirai-core/src/jvmTest/kotlin/message/data/ForwardRefineTest.kt

@@ -16,7 +16,7 @@ import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
 import net.mamoe.mirai.internal.message.ForwardMessageInternal
 import net.mamoe.mirai.internal.message.SimpleRefineContext
 import net.mamoe.mirai.internal.test.runBlockingUnit
-import net.mamoe.mirai.internal.utils._miraiContentToString
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.data.*
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
@@ -49,7 +49,7 @@ internal class ForwardRefineTest : AbstractTestWithMiraiImpl() {
         })
         println(refine.size)
         println(refine.first()::class)
-        println(refine._miraiContentToString())
+        println(refine.structureToString())
         assertTrue { refine.first() is MessageOrigin }
         assertTrue { refine.drop(1).first() is ForwardMessage }
         assertEquals(

+ 2 - 2
mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt

@@ -20,7 +20,7 @@ import net.mamoe.mirai.internal.message.MarketFaceImpl
 import net.mamoe.mirai.internal.message.OnlineAudioImpl
 import net.mamoe.mirai.internal.message.UnsupportedMessageImpl
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
-import net.mamoe.mirai.internal.utils._miraiContentToString
+import net.mamoe.mirai.internal.utils.structureToString
 import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.cast
@@ -201,7 +201,7 @@ internal class MessageSerializationTest {
             ignoreUnknownKeys = true
         }
         val source = j.decodeFromString(MessageSource.Serializer, a)
-        println(source._miraiContentToString())
+        println(source.structureToString())
         assertEquals(
             expected = Mirai.buildMessageSource(692928873, MessageSourceKind.GROUP) {
                 id(44)

+ 212 - 0
mirai-core/src/jvmTest/kotlin/utils/StructureToStringLegacy.kt

@@ -0,0 +1,212 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies 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/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils
+
+import kotlinx.serialization.Transient
+import net.mamoe.mirai.IMirai
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.debug
+import net.mamoe.mirai.utils.toUHexString
+import java.lang.reflect.Modifier
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.hasAnnotation
+import kotlin.reflect.jvm.javaField
+
+// Kept for souvenir. Not used.
+internal class StructureToStringLegacy : StructureToStringTransformer {
+    override fun transform(any: Any?): String = any._miraiContentToString()
+
+    private val indent: String = " ".repeat(4)
+
+    /**
+     * 将所有元素加入转换为多行的字符串表示.
+     */
+    private fun <T> Sequence<T>.joinToStringPrefixed(prefix: String, transform: (T) -> CharSequence): String {
+        return this.joinToString(prefix = "$prefix$indent", separator = "\n$prefix$indent", transform = transform)
+    }
+
+    /**
+     * 将内容格式化为较可读的字符串输出.
+     *
+     * 各数字类型及其无符号类型: 十六进制表示 + 十进制表示. e.g. `0x1000(4096)`
+     * [ByteArray] 和 [UByteArray]: 十六进制表示, 通过 [ByteArray.toUHexString]
+     * [Iterable], [Iterator], [Sequence]: 调用各自的 joinToString.
+     * [Map]: 多行输出. 每行显示一个值. 递归调用 [_miraiContentToString]. 嵌套结构将会以缩进表示
+     * `data class`: 调用其 [toString]
+     * 其他类型: 反射获取它和它的所有来自 Mirai 的 super 类型的所有自有属性并递归调用 [_miraiContentToString]. 嵌套结构将会以缩进表示
+     */
+    @Suppress("FunctionName") // 这样就不容易被 IDE 提示
+    internal fun Any?._miraiContentToString(prefix: String = ""): String = when (this) {
+        is Unit -> "Unit"
+        is UInt -> "0x" + this.toUHexString("") + "($this)"
+        is UByte -> "0x" + this.toUHexString() + "($this)"
+        is UShort -> "0x" + this.toUHexString("") + "($this)"
+        is ULong -> "0x" + this.toUHexString("") + "($this)"
+        is Int -> "0x" + this.toUHexString("") + "($this)"
+        is Byte -> "0x" + this.toUHexString() + "($this)"
+        is Short -> "0x" + this.toUHexString("") + "($this)"
+        is Long -> "0x" + this.toUHexString("") + "($this)"
+
+        is Boolean -> if (this) "true" else "false"
+
+        is ByteArray -> {
+            if (this.size == 0) "<Empty ByteArray>"
+            else this.toUHexString()
+        }
+        is UByteArray -> {
+            if (this.size == 0) "<Empty UByteArray>"
+            else this.toUHexString()
+        }
+        is ShortArray -> {
+            if (this.size == 0) "<Empty ShortArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is IntArray -> {
+            if (this.size == 0) "<Empty IntArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is LongArray -> {
+            if (this.size == 0) "<Empty LongArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is FloatArray -> {
+            if (this.size == 0) "<Empty FloatArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is DoubleArray -> {
+            if (this.size == 0) "<Empty DoubleArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is UShortArray -> {
+            if (this.size == 0) "<Empty ShortArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is UIntArray -> {
+            if (this.size == 0) "<Empty IntArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is ULongArray -> {
+            if (this.size == 0) "<Empty LongArray>"
+            else this.iterator()._miraiContentToString()
+        }
+        is Array<*> -> {
+            if (this.size == 0) "<Empty Array>"
+            else this.iterator()._miraiContentToString()
+        }
+        is BooleanArray -> {
+            if (this.size == 0) "<Empty BooleanArray>"
+            else this.iterator()._miraiContentToString()
+        }
+
+        is Iterable<*> -> this.joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
+        is Iterator<*> -> this.asSequence()
+            .joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
+        is Sequence<*> -> this.joinToString(prefix = "[", postfix = "]") { it._miraiContentToString(prefix) }
+        is Map<*, *> -> this.entries.joinToString(
+            prefix = "{",
+            postfix = "}"
+        ) { it.key._miraiContentToString(prefix) + "=" + it.value._miraiContentToString(prefix) }
+        else -> {
+            if (this == null) "null"
+            else if (this::class.isData) this.toString()
+            else {
+                if (this::class.qualifiedName?.startsWith("net.mamoe.mirai.") == true) {
+                    this.contentToStringReflectively(prefix + indent)
+                } else this.toString()
+                /*
+                (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + "{\n" +
+                        this::class.members.asSequence().filterIsInstance<KProperty<*>>().filter { !it.isSuspend && it.visibility == KVisibility.PUBLIC }
+                            .joinToStringPrefixed(
+                                prefix = indent
+                            ) { it.name + "=" + kotlin.runCatching { it.call(it).contentToString(indent) }.getOrElse { "<!>" } }
+                 */
+            }
+        }
+    }
+
+    internal fun KProperty1<*, *>.getValueAgainstPermission(receiver: Any): Any? {
+        return this.javaField?.apply { isAccessible = true }?.get(receiver)
+    }
+
+    private fun Any.canBeIgnored(): Boolean {
+        return when (this) {
+            is String -> this.isEmpty()
+            is ByteArray -> this.isEmpty()
+            is Array<*> -> this.isEmpty()
+            is Int -> this == 0
+            is Float -> this == 0f
+            is Double -> this == 0.0
+            is Byte -> this == 0.toByte()
+            is Short -> this == 0.toShort()
+            is Long -> this == 0.toLong()
+            else -> false
+        }
+    }
+
+    private fun Any.contentToStringReflectively(
+        prefix: String,
+        filter: ((name: String, value: Any?) -> Boolean)? = null,
+    ): String {
+        val newPrefix = "$prefix    "
+        return (this::class.simpleName ?: "<UnnamedClass>") + "#" + this::class.hashCode() + " {\n" +
+                this.allMembersFromSuperClassesMatching { it.qualifiedName?.startsWith("net.mamoe.mirai") == true }
+                    .distinctBy { it.name }
+                    .filterNot { it.name.contains("$") || it.name == "Companion" || it.isConst || it.name == "serialVersionUID" }
+                    .mapNotNull {
+                        val value = it.getValueAgainstPermission(this) ?: return@mapNotNull null
+                        if (filter != null) {
+                            if (!filter(it.name, value))
+                                return@mapNotNull it.name to value
+                            else {
+                                return@mapNotNull null
+                            }
+                        }
+                        it.name to value
+                    }
+                    .joinToStringPrefixed(
+                        prefix = newPrefix
+                    ) { (name: String, value: Any?) ->
+                        if (value.canBeIgnored()) ""
+                        else {
+                            "$name=" + kotlin.runCatching {
+                                if (value == this) "<this>"
+                                else value._miraiContentToString(newPrefix)
+                            }.getOrElse { "<!>" }
+                        }
+                    }.lines().filterNot { it.isBlank() }.joinToString("\n") + "\n$prefix}"
+    }
+
+    private fun KClass<out Any>.thisClassAndSuperclassSequence(): Sequence<KClass<out Any>> {
+        return sequenceOf(this) +
+                this.supertypes.asSequence()
+                    .mapNotNull { type ->
+                        type.classifier?.takeIf { it is KClass<*> }?.takeIf { it != Any::class } as? KClass<out Any>
+                    }.flatMap { it.thisClassAndSuperclassSequence() }
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private fun Any.allMembersFromSuperClassesMatching(classFilter: (KClass<out Any>) -> Boolean): Sequence<KProperty1<Any, *>> {
+        return this::class.thisClassAndSuperclassSequence()
+            .filter { classFilter(it) }
+            .map { it.members }
+            .flatMap { it.asSequence() }
+            .filterIsInstance<KProperty1<*, *>>()
+            .filterNot { it.hasAnnotation<Transient>() }
+            .filterNot { it.isTransient() }
+            .map { it as KProperty1<Any, *> }
+    }
+
+    internal fun KProperty<*>.isTransient(): Boolean =
+        javaField?.modifiers?.and(Modifier.TRANSIENT) != 0
+
+
+}