diff --git a/docs/Messages.md b/docs/Messages.md
index f7425784e..3703492e7 100644
--- a/docs/Messages.md
+++ b/docs/Messages.md
@@ -89,6 +89,7 @@ Mirai 支持多种消息类型。
 [`MusicShare`]: ../mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt
 [`Dice`]: ../mirai-core-api/src/commonMain/kotlin/message/data/Dice.kt
 [`FileMessage`]: ../mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt
+[`RockPaperScissors`]: ../mirai-core-api/src/commonMain/kotlin/message/data/RockPaperScissors.kt
 
 [`MessageSource`]: ../mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
 [`QuoteReply`]: ../mirai-core-api/src/commonMain/kotlin/message/data/QuoteReply.kt
@@ -119,6 +120,7 @@ Mirai 支持多种消息类型。
 | [`SimpleServiceMessage`] | (不稳定)服务消息      | `$content`              |          2.0          |
 |      [`MusicShare`]      | 音乐分享              | `[分享]曲名`             |          2.1          |
 |         [`Dice`]         | 魔法表情骰子           | `[骰子:$value]`         |          2.5          |
+| [`RockPaperScissors`]    | 魔法表情猜拳           | `[石头]`/`[剪刀]`/`[布]` |         2.14          |
 |     [`FileMessage`]      | 文件消息              | `[文件]文件名称`          |          2.5          |
 |        [`Audio`]         | 语音                 | `[语音消息]`              |          2.7          |
 
@@ -572,6 +574,7 @@ at.serializeToMiraiCode() // 结果为 `[mirai:at:123]`
 |         [`Dice`]         | `[mirai:dice:$value]`                            |
 |      [`MusicShare`]      | `[mirai:musicshare:$args]`                       |
 |     [`FileMessage`]      | `[mirai:file:$id,$internalId,$name,$size]`       |
+| [`RockPaperScissors`]    | `[mirai:rps:$name]`                              |
 
 ### 由 mirai 码字符串取得 `MessageChain` 实例
 
diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api
index 55316d25e..6588a8d4e 100644
--- a/mirai-core-api/compatibility-validation/android/api/android.api
+++ b/mirai-core-api/compatibility-validation/android/api/android.api
@@ -5112,6 +5112,32 @@ public final class net/mamoe/mirai/message/data/RichMessageOrigin$Key : net/mamo
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public final class net/mamoe/mirai/message/data/RockPaperScissors : java/lang/Enum, net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/MarketFace {
+	public static final field Key Lnet/mamoe/mirai/message/data/RockPaperScissors$Key;
+	public static final field PAPER Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field ROCK Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field SCISSORS Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field SERIAL_NAME Ljava/lang/String;
+	public fun contentToString ()Ljava/lang/String;
+	public final fun eliminates (Lnet/mamoe/mirai/message/data/RockPaperScissors;)Ljava/lang/Boolean;
+	public final fun getContent ()Ljava/lang/String;
+	public fun getId ()I
+	public final fun getInternalId ()B
+	public synthetic fun getName ()Ljava/lang/String;
+	public static final fun random ()Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public fun toString ()Ljava/lang/String;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static fun values ()[Lnet/mamoe/mirai/message/data/RockPaperScissors;
+}
+
+public final class net/mamoe/mirai/message/data/RockPaperScissors$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public final fun random ()Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static synthetic fun random$default (Lnet/mamoe/mirai/message/data/RockPaperScissors$Key;Lkotlin/random/Random;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
 public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage {
 	public static final field Key Lnet/mamoe/mirai/message/data/ServiceMessage$Key;
 	public abstract fun getServiceId ()I
diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api
index 1d69ea44e..719a03766 100644
--- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api
+++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api
@@ -5112,6 +5112,32 @@ public final class net/mamoe/mirai/message/data/RichMessageOrigin$Key : net/mamo
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public final class net/mamoe/mirai/message/data/RockPaperScissors : java/lang/Enum, net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/MarketFace {
+	public static final field Key Lnet/mamoe/mirai/message/data/RockPaperScissors$Key;
+	public static final field PAPER Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field ROCK Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field SCISSORS Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final field SERIAL_NAME Ljava/lang/String;
+	public fun contentToString ()Ljava/lang/String;
+	public final fun eliminates (Lnet/mamoe/mirai/message/data/RockPaperScissors;)Ljava/lang/Boolean;
+	public final fun getContent ()Ljava/lang/String;
+	public fun getId ()I
+	public final fun getInternalId ()B
+	public synthetic fun getName ()Ljava/lang/String;
+	public static final fun random ()Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public fun toString ()Ljava/lang/String;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static fun values ()[Lnet/mamoe/mirai/message/data/RockPaperScissors;
+}
+
+public final class net/mamoe/mirai/message/data/RockPaperScissors$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public final fun random ()Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public final fun random (Lkotlin/random/Random;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public static synthetic fun random$default (Lnet/mamoe/mirai/message/data/RockPaperScissors$Key;Lkotlin/random/Random;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/RockPaperScissors;
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
 public abstract interface class net/mamoe/mirai/message/data/ServiceMessage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/RichMessage {
 	public static final field Key Lnet/mamoe/mirai/message/data/ServiceMessage$Key;
 	public abstract fun getServiceId ()I
diff --git a/mirai-core-api/src/commonMain/kotlin/message/code/internal/impl.kt b/mirai-core-api/src/commonMain/kotlin/message/code/internal/impl.kt
index c44ebeb85..b8834f062 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/code/internal/impl.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/code/internal/impl.kt
@@ -123,6 +123,9 @@ private object MiraiCodeParsers : AbstractMap<String, MiraiCodeParser>(), Map<St
     "dice" to MiraiCodeParser(Regex("""([1-6])""")) { (value) ->
         Dice(value.toInt())
     },
+    "rps" to MiraiCodeParser(Regex("""(\w+)""")) { (value) ->
+        RockPaperScissors.valueOf(value.uppercase())
+    },
     "musicshare" to MiraiCodeParser.DynamicParser(7) { args ->
         val (kind, title, summary, jumpUrl, pictureUrl) = args
         val musicUrl = args[5]
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/RockPaperScissors.kt b/mirai-core-api/src/commonMain/kotlin/message/data/RockPaperScissors.kt
new file mode 100644
index 000000000..29d8a2a8b
--- /dev/null
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/RockPaperScissors.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2019-2022 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
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("MessageUtils")
+
+package net.mamoe.mirai.message.data
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.message.code.CodableMessage
+import net.mamoe.mirai.message.data.RockPaperScissors.*
+import net.mamoe.mirai.message.data.visitor.MessageVisitor
+import net.mamoe.mirai.utils.*
+import kotlin.jvm.*
+import kotlin.random.Random
+
+/**
+ * 石头剪刀布.
+ *
+ * 可以通过 [RockPaperScissors.random] 获得一个随机手势的实例.
+ *
+ * @property ROCK 石头 `[mirai:rps:rock]`
+ * @property SCISSORS 剪刀 `[mirai:rps:scissors]`
+ * @property PAPER 布(纸)`[mirai:rps:paper]`
+ *
+ * @since 2.14
+ */
+@kotlin.Suppress("RemoveRedundantQualifierName")
+@Serializable(RockPaperScissors.Serializer::class)
+@SerialName(RockPaperScissors.SERIAL_NAME)
+public enum class RockPaperScissors(
+    public val content: String,
+
+    internalId: Int,
+) : MarketFace, CodableMessage {
+    ROCK("[石头]", 48),
+    SCISSORS("[剪刀]", 49),
+    PAPER("[布]", 50)
+    ;
+
+    @MiraiExperimentalApi
+    override val id: Int
+        get() = 11415
+
+    @MiraiInternalApi
+    @JvmSynthetic
+    public val internalId: Byte = internalId.toByte()
+
+    @MiraiExperimentalApi
+    override fun appendMiraiCodeTo(builder: StringBuilder) {
+        builder.append("[mirai:rps:").append(name.lowercase()).append(']')
+    }
+
+    override fun toString(): String = serializeToMiraiCode()
+
+
+    override fun contentToString(): String = content
+
+    @MiraiInternalApi
+    override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
+        return visitor.visitRockPaperScissors(this, data)
+    }
+
+    /**
+     * 判断 当前手势 (`this`) 能否淘汰对手 ([other])
+     *
+     * @return 赢返回 `true`,输返回 `false`,平局时返回 `null`
+     */
+    public infix fun eliminates(other: RockPaperScissors): Boolean? {
+        return when {
+            this == other -> null
+            this == ROCK && other == SCISSORS -> true
+            this == SCISSORS && other == PAPER -> true
+            this == PAPER && other == ROCK -> true
+            else -> false
+        }
+    }
+
+    public companion object Key :
+        AbstractPolymorphicMessageKey<MarketFace, RockPaperScissors>(MarketFace, { it.safeCast() }) {
+        public const val SERIAL_NAME: String = "RockPaperScissors"
+
+        private val values = values()
+
+        /**
+         * 获取随机手势的 [石头剪刀布][RockPaperScissors]
+         *
+         * Java 可通过 `kotlin.random.PlatformRandomKt.asKotlinRandom()` 来传入一个 random
+         */
+        @JvmStatic
+        @JvmOverloads
+        public fun random(random: Random = Random): RockPaperScissors = RockPaperScissors.values.random(random)
+
+    }
+
+    internal object Serializer : KSerializer<RockPaperScissors> by Surrogate.serializer().map(
+        resultantDescriptor = Surrogate.serializer().descriptor.copy(SERIAL_NAME),
+        deserialize = { valueOf(it.name) },
+        serialize = { Surrogate(name) },
+    ) {
+
+        @Serializable
+        @SerialName(RockPaperScissors.SERIAL_NAME)
+        private class Surrogate(
+            val name: String,
+        )
+    }
+}
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt b/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt
index 234a05885..9c072fc2a 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/visitor/MessageVisitor.kt
@@ -48,6 +48,7 @@ public interface MessageVisitor<in D, out R> {
     // region MarketFace
     public fun visitMarketFace(message: MarketFace, data: D): R
     public fun visitDice(message: Dice, data: D): R
+    public fun visitRockPaperScissors(message: RockPaperScissors, data: D): R
 
     // endregion
     // endregion
@@ -184,6 +185,10 @@ public abstract class AbstractMessageVisitor<in D, out R> : MessageVisitor<D, R>
         return visitMarketFace(message, data)
     }
 
+    public override fun visitRockPaperScissors(message: RockPaperScissors, data: D): R {
+        return visitMarketFace(message, data)
+    }
+
     public override fun visitFace(message: Face, data: D): R {
         return visitMessageContent(message, data)
     }
diff --git a/mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt b/mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt
index 5fbb1d1ce..97d0b4d8c 100644
--- a/mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt
+++ b/mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt
@@ -107,6 +107,10 @@ internal class MessageVisitorTest {
             return arrayOf("visitDice") + super.visitDice(message, data)
         }
 
+        override fun visitRockPaperScissors(message: RockPaperScissors, data: Unit): Array<String> {
+            return arrayOf("visitRockPaperScissors") + super.visitRockPaperScissors(message, data)
+        }
+
         override fun visitFace(message: Face, data: Unit): Array<String> {
             return arrayOf("visitFace") + super.visitFace(message, data)
         }
@@ -335,6 +339,18 @@ internal class MessageVisitorTest {
             Dice(1).accept(GetCalledMethodNames)
         )
 
+        assertContentEquals(
+            arrayOf(
+                "visitRockPaperScissors",
+                "visitMarketFace",
+                "visitHummerMessage",
+                "visitMessageContent",
+                "visitSingleMessage",
+                "visitMessage",
+            ),
+            RockPaperScissors.PAPER.accept(GetCalledMethodNames)
+        )
+
 
         assertContentEquals(
             arrayOf(
diff --git a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
index 4e914a5c8..b04f046e9 100644
--- a/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
+++ b/mirai-core/src/commonMain/kotlin/message/protocol/impl/MarketFaceProtocol.kt
@@ -28,6 +28,7 @@ import net.mamoe.mirai.utils.map
 internal class MarketFaceProtocol : MessageProtocol() {
     override fun ProcessorCollector.collectProcessorsImpl() {
         add(DiceEncoder())
+        add(RockPaperScissorsEncoder())
         add(MarketFaceImplEncoder())
 
         add(MarketFaceDecoder())
@@ -46,11 +47,12 @@ internal class MarketFaceProtocol : MessageProtocol() {
                 MarketFace::class, MarketFaceImpl.serializer().map(
                     resultantDescriptor = MarketFaceImpl.serializer().descriptor.copy(MarketFace.SERIAL_NAME),
                     deserialize = {
-                        it.delegate.toDiceOrNull() ?: it
+                        it.delegate.toDiceOrNull() ?: it.delegate.toRockPaperScissorsOrNull() ?: it
                     },
                     serialize = {
                         when (it) {
                             is Dice -> MarketFaceImpl(it.toJceStruct())
+                            is RockPaperScissors -> MarketFaceImpl(it.toJceStruct())
                             is MarketFaceImpl -> it
                             else -> {
                                 error("Unsupported MarketFace type ${it::class.qualifiedName}")
@@ -64,6 +66,7 @@ internal class MarketFaceProtocol : MessageProtocol() {
         MessageSerializer.superclassesScope(MarketFace::class, MessageContent::class, SingleMessage::class) {
             add(MessageSerializer(MarketFaceImpl::class, MarketFaceImpl.serializer()))
             add(MessageSerializer(Dice::class, Dice.serializer()))
+            add(MessageSerializer(RockPaperScissors::class, RockPaperScissors.serializer()))
         }
     }
 
@@ -90,6 +93,13 @@ internal class MarketFaceProtocol : MessageProtocol() {
         }
     }
 
+    private class RockPaperScissorsEncoder : MessageEncoder<RockPaperScissors> {
+        override suspend fun MessageEncoderContext.process(data: RockPaperScissors) {
+            markAsConsumed()
+            processAlso(MarketFaceImpl(data.toJceStruct()))
+        }
+    }
+
     private class MarketFaceDecoder : MessageDecoder {
         override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
             val proto = data.marketFace ?: return
@@ -99,6 +109,11 @@ internal class MarketFaceProtocol : MessageProtocol() {
                 return
             }
 
+            proto.toRockPaperScissorsOrNull()?.let {
+                collect(it)
+                return
+            }
+
             collect(MarketFaceImpl(proto))
         }
     }
@@ -118,6 +133,12 @@ internal class MarketFaceProtocol : MessageProtocol() {
             6 to "7A2303AD80755FCB6BBFAC38327E0C01".hexToBytes(),
         )
 
+        private val RPS_PC_FACE_IDS = mapOf(
+            48 to "E5D889F1DF79B2B45183F625584465D3".hexToBytes(),
+            49 to "628FA4AB7B6C2BCCFCDCD0C2DAF7A60C".hexToBytes(),
+            50 to "457CDE420F598EB424CED2E905D38D8B".hexToBytes(),
+        )
+
         private fun ImMsgBody.MarketFace.toDiceOrNull(): Dice? {
             if (this.tabId != 11464) return null
             val value = when {
@@ -130,6 +151,26 @@ internal class MarketFaceProtocol : MessageProtocol() {
             return null
         }
 
+        private fun ImMsgBody.MarketFace.toRockPaperScissorsOrNull(): RockPaperScissors? {
+            if (tabId != 11415) return null
+
+            val value = when {
+                mobileParam.isNotEmpty() -> {
+                    val theLast = mobileParam.lastOrNull() ?: return null
+                    theLast.toInt().and(0xff)
+                }
+                else -> RPS_PC_FACE_IDS.entries.find { it.value.contentEquals(faceId) }?.key ?: return null
+            }
+
+            return when (value) {
+                48 -> RockPaperScissors.ROCK
+                49 -> RockPaperScissors.SCISSORS
+                50 -> RockPaperScissors.PAPER
+
+                else -> null
+            }
+        }
+
         // From https://github.com/mamoe/mirai/issues/1012
         private fun Dice.toJceStruct(): ImMsgBody.MarketFace {
             return ImMsgBody.MarketFace(
@@ -161,5 +202,31 @@ internal class MarketFaceProtocol : MessageProtocol() {
                 )
             )
         }
+
+        private fun RockPaperScissors.toJceStruct(): ImMsgBody.MarketFace {
+            return ImMsgBody.MarketFace(
+                faceName = byteArrayOf(91, -25, -116, -100, -26, -117, -77, 93),
+                itemType = 6,
+                faceInfo = 1,
+                faceId = byteArrayOf(
+                    -125, -56, -94, -109, -82,
+                    101, -54, 20, 15, 52,
+                    -127, 32, -89, 116, 72, -18
+                ),
+                tabId = 11415,
+                subType = 3,
+                key = byteArrayOf(55, 100, 101, 51, 57, 102, 101, 98, 99, 102, 52, 53, 101, 54, 100, 98),
+                mediaType = 0,
+                imageWidth = 200,
+                imageHeight = 200,
+                mobileParam = byteArrayOf(
+                    114, 115, 99, 84, 121, 112, 101,
+                    63, 49, 59, 118, 97, 108, 117,
+                    101, 61,
+                    internalId
+                ),
+                pbReserve = byteArrayOf(10, 6, 8, -56, 1, 16, -56, 1, 64, 1)
+            )
+        }
     }
 }
diff --git a/mirai-core/src/commonTest/kotlin/message/code/TestMiraiCode.kt b/mirai-core/src/commonTest/kotlin/message/code/TestMiraiCode.kt
index 403d00a63..e9b97646e 100644
--- a/mirai-core/src/commonTest/kotlin/message/code/TestMiraiCode.kt
+++ b/mirai-core/src/commonTest/kotlin/message/code/TestMiraiCode.kt
@@ -82,5 +82,18 @@ internal class TestMiraiCode : AbstractTest() {
             brief = "",
         )
         assertEquals(musicShare.toMessageChain(), musicShare.serializeToMiraiCode().deserializeMiraiCode())
+
+        assertEquals(
+            messageChainOf(RockPaperScissors.ROCK),
+            "[mirai:rps:rock]".deserializeMiraiCode()
+        )
+        assertEquals(
+            messageChainOf(RockPaperScissors.SCISSORS),
+            "[mirai:rps:scissors]".deserializeMiraiCode()
+        )
+        assertEquals(
+            messageChainOf(RockPaperScissors.PAPER),
+            "[mirai:rps:paper]".deserializeMiraiCode()
+        )
     }
 }
\ No newline at end of file
diff --git a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
index 5e4e952a8..ecd71ae48 100644
--- a/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/data/MessageSerializationTest.kt
@@ -71,6 +71,7 @@ internal class MessageSerializationTest : AbstractTest() {
         AtAll,
         image,
         Face(Face.AI_NI),
+        RockPaperScissors.PAPER,
         UnsupportedMessageImpl(ImMsgBody.Elem())
     )
 
diff --git a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
index c947c114e..260805ba9 100644
--- a/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
+++ b/mirai-core/src/commonTest/kotlin/message/protocol/impl/MarketFaceProtocolTest.kt
@@ -21,6 +21,7 @@ import net.mamoe.mirai.internal.testFramework.dynamicTest
 import net.mamoe.mirai.internal.testFramework.runDynamicTests
 import net.mamoe.mirai.message.data.Dice
 import net.mamoe.mirai.message.data.MarketFace
+import net.mamoe.mirai.message.data.RockPaperScissors
 import net.mamoe.mirai.utils.hexToBytes
 import kotlin.test.BeforeTest
 import kotlin.test.Test
@@ -104,6 +105,354 @@ internal class MarketFaceProtocolTest : AbstractMessageProtocolTest() {
         }.doEncoderChecks()
     }
 
+    @Test
+    fun `decode RockPaperScissors`() {
+        // region WinQQ PC
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "E5 D8 89 F1 DF 79 B2 B4 51 83 F6 25 58 44 65 D3".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 100,
+                        imageHeight = 100,
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                        attr7Buf = "01".hexToBytes(),
+                    ),
+                ),
+            )
+
+            message(RockPaperScissors.ROCK)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "62 8F A4 AB 7B 6C 2B CC FC DC D0 C2 DA F7 A6 0C".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 100,
+                        imageHeight = 100,
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                        attr7Buf = "01".hexToBytes(),
+                    ),
+                ),
+            )
+
+            message(RockPaperScissors.SCISSORS)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "45 7C DE 42 0F 59 8E B4 24 CE D2 E9 05 D3 8D 8B".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 100,
+                        imageHeight = 100,
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                        attr7Buf = "01".hexToBytes(),
+                    ),
+                ),
+            )
+
+            message(RockPaperScissors.PAPER)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        // endregion
+
+        // region AndroidQQ 8.4.18.49145
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=2".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 32 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+            message(RockPaperScissors.PAPER)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=0".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 30 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+            message(RockPaperScissors.ROCK)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=1".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 31 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+            message(RockPaperScissors.SCISSORS)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        // endregion
+
+        // region MacOS
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=0".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 30 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+
+            message(RockPaperScissors.ROCK)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        // endregion
+
+        // region iOS
+        buildCodingChecks {
+            elem(
+                // ROCK
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=0".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 30 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+            message(RockPaperScissors.ROCK)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        buildCodingChecks { // paper
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=2".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 32 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+            )
+            message(RockPaperScissors.PAPER)
+            useOrdinaryEquality()
+        }.doDecoderChecks()
+        // endregion
+    }
+
+    @Test
+    fun `encode RockPaperScissors`() {
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=0".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 30 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo(
+                        flags = 8,
+                        groupMask = 1,
+                    ),
+                ),
+            )
+            message(RockPaperScissors.ROCK)
+        }.doBothChecks()
+
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=1".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 31 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ),
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo(
+                        flags = 8,
+                        groupMask = 1,
+                    ),
+                ),
+            )
+            message(RockPaperScissors.SCISSORS)
+        }.doBothChecks()
+
+        buildCodingChecks {
+            elem(
+                net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    marketFace = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MarketFace(
+                        faceName = "[猜拳]".toByteArray(), /* 5B E7 8C 9C E6 8B B3 5D */
+                        itemType = 6,
+                        faceInfo = 1,
+                        faceId = "83 C8 A2 93 AE 65 CA 14 0F 34 81 20 A7 74 48 EE".hexToBytes(),
+                        tabId = 11415,
+                        subType = 3,
+                        key = "7de39febcf45e6db".toByteArray(), /* 37 64 65 33 39 66 65 62 63 66 34 35 65 36 64 62 */
+                        imageWidth = 200,
+                        imageHeight = 200,
+                        mobileParam = "rscType?1;value=2".toByteArray(), /* 72 73 63 54 79 70 65 3F 31 3B 76 61 6C 75 65 3D 32 */
+                        pbReserve = "0A 06 08 C8 01 10 C8 01 40 01".hexToBytes(),
+                    ),
+                ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text(
+                        str = "[猜拳]",
+                    ),
+                ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
+                    extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo(
+                        flags = 8,
+                        groupMask = 1,
+                    ),
+                )
+            )
+            message(RockPaperScissors.PAPER)
+        }.doBothChecks()
+
+    }
 
     @Test
     fun `encode decode MarketFace from Android`() {