ygopro-msg-encode 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc.js +25 -0
  3. package/.prettierrc +4 -0
  4. package/AGENTS.md +12 -0
  5. package/CHANGES.md +270 -0
  6. package/CTOS_STOC_IMPLEMENTATION.md +279 -0
  7. package/FINAL_SUMMARY.md +357 -0
  8. package/FULL_PAYLOAD_SUMMARY.md +491 -0
  9. package/FULL_PAYLOAD_UPDATE.md +598 -0
  10. package/IMPLEMENTATION_SUMMARY.md +294 -0
  11. package/LICENSE +22 -0
  12. package/MSG_IMPLEMENTATION_SUMMARY.md +358 -0
  13. package/OPPONENT_VIEW_SUMMARY.md +387 -0
  14. package/PROJECT_COMPLETE.md +565 -0
  15. package/QUICK_REFERENCE.md +352 -0
  16. package/README.md +494 -0
  17. package/REAL_IP_STRING_UPDATE.md +289 -0
  18. package/TESTS_MIGRATION.md +342 -0
  19. package/VARIABLE_LENGTH_STRINGS.md +224 -0
  20. package/VARIABLE_LENGTH_UPDATE.md +229 -0
  21. package/dist/index.cjs +5131 -0
  22. package/dist/index.cjs.map +7 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.mjs +5185 -0
  25. package/dist/index.mjs.map +7 -0
  26. package/dist/src/binary/binary-meta.d.ts +18 -0
  27. package/dist/src/binary/fill-binary-fields.d.ts +2 -0
  28. package/dist/src/metadata.d.ts +10 -0
  29. package/dist/src/proto-base/payload-base.d.ts +8 -0
  30. package/dist/src/proto-base/registry-base.d.ts +13 -0
  31. package/dist/src/protos/common/host-info.d.ts +12 -0
  32. package/dist/src/protos/common/index.d.ts +1 -0
  33. package/dist/src/protos/ctos/base.d.ts +17 -0
  34. package/dist/src/protos/ctos/index.d.ts +3 -0
  35. package/dist/src/protos/ctos/proto/chat.d.ts +9 -0
  36. package/dist/src/protos/ctos/proto/create-game.d.ts +8 -0
  37. package/dist/src/protos/ctos/proto/external-address.d.ts +12 -0
  38. package/dist/src/protos/ctos/proto/hand-result.d.ts +5 -0
  39. package/dist/src/protos/ctos/proto/hs-notready.d.ts +4 -0
  40. package/dist/src/protos/ctos/proto/hs-ready.d.ts +4 -0
  41. package/dist/src/protos/ctos/proto/hs-start.d.ts +4 -0
  42. package/dist/src/protos/ctos/proto/hs-toduelist.d.ts +4 -0
  43. package/dist/src/protos/ctos/proto/hs-toobserver.d.ts +4 -0
  44. package/dist/src/protos/ctos/proto/index.d.ts +19 -0
  45. package/dist/src/protos/ctos/proto/join-game.d.ts +7 -0
  46. package/dist/src/protos/ctos/proto/kick.d.ts +5 -0
  47. package/dist/src/protos/ctos/proto/leave-game.d.ts +4 -0
  48. package/dist/src/protos/ctos/proto/player-info.d.ts +5 -0
  49. package/dist/src/protos/ctos/proto/request-field.d.ts +4 -0
  50. package/dist/src/protos/ctos/proto/response.d.ts +7 -0
  51. package/dist/src/protos/ctos/proto/surrender.d.ts +4 -0
  52. package/dist/src/protos/ctos/proto/time-confirm.d.ts +4 -0
  53. package/dist/src/protos/ctos/proto/tp-result.d.ts +5 -0
  54. package/dist/src/protos/ctos/proto/update-deck.d.ts +11 -0
  55. package/dist/src/protos/ctos/registry.d.ts +3 -0
  56. package/dist/src/protos/msg/base.d.ts +9 -0
  57. package/dist/src/protos/msg/index.d.ts +3 -0
  58. package/dist/src/protos/msg/proto/add-counter.d.ts +9 -0
  59. package/dist/src/protos/msg/proto/announce-attrib.d.ts +7 -0
  60. package/dist/src/protos/msg/proto/announce-card.d.ts +7 -0
  61. package/dist/src/protos/msg/proto/announce-number.d.ts +7 -0
  62. package/dist/src/protos/msg/proto/announce-race.d.ts +7 -0
  63. package/dist/src/protos/msg/proto/attack-disabled.d.ts +4 -0
  64. package/dist/src/protos/msg/proto/attack.d.ts +12 -0
  65. package/dist/src/protos/msg/proto/battle.d.ts +18 -0
  66. package/dist/src/protos/msg/proto/become-target.d.ts +6 -0
  67. package/dist/src/protos/msg/proto/cancel-target.d.ts +11 -0
  68. package/dist/src/protos/msg/proto/card-hint.d.ts +10 -0
  69. package/dist/src/protos/msg/proto/card-query.d.ts +38 -0
  70. package/dist/src/protos/msg/proto/card-target.d.ts +11 -0
  71. package/dist/src/protos/msg/proto/chain-disabled.d.ts +5 -0
  72. package/dist/src/protos/msg/proto/chain-end.d.ts +4 -0
  73. package/dist/src/protos/msg/proto/chain-negated.d.ts +5 -0
  74. package/dist/src/protos/msg/proto/chain-solved.d.ts +5 -0
  75. package/dist/src/protos/msg/proto/chain-solving.d.ts +5 -0
  76. package/dist/src/protos/msg/proto/chained.d.ts +5 -0
  77. package/dist/src/protos/msg/proto/chaining.d.ts +12 -0
  78. package/dist/src/protos/msg/proto/confirm-cards.d.ts +16 -0
  79. package/dist/src/protos/msg/proto/confirm-decktop.d.ts +14 -0
  80. package/dist/src/protos/msg/proto/confirm-extratop.d.ts +14 -0
  81. package/dist/src/protos/msg/proto/damage-step-end.d.ts +4 -0
  82. package/dist/src/protos/msg/proto/damage-step-start.d.ts +4 -0
  83. package/dist/src/protos/msg/proto/damage.d.ts +6 -0
  84. package/dist/src/protos/msg/proto/deck-top.d.ts +8 -0
  85. package/dist/src/protos/msg/proto/draw.d.ts +8 -0
  86. package/dist/src/protos/msg/proto/equip.d.ts +12 -0
  87. package/dist/src/protos/msg/proto/field-disabled.d.ts +5 -0
  88. package/dist/src/protos/msg/proto/flipsummoned.d.ts +4 -0
  89. package/dist/src/protos/msg/proto/flipsummoning.d.ts +9 -0
  90. package/dist/src/protos/msg/proto/hand-res.d.ts +5 -0
  91. package/dist/src/protos/msg/proto/hint.d.ts +7 -0
  92. package/dist/src/protos/msg/proto/index.d.ts +85 -0
  93. package/dist/src/protos/msg/proto/lpupdate.d.ts +6 -0
  94. package/dist/src/protos/msg/proto/match-kill.d.ts +5 -0
  95. package/dist/src/protos/msg/proto/missed-effect.d.ts +7 -0
  96. package/dist/src/protos/msg/proto/move.d.ts +14 -0
  97. package/dist/src/protos/msg/proto/new-phase.d.ts +5 -0
  98. package/dist/src/protos/msg/proto/new-turn.d.ts +5 -0
  99. package/dist/src/protos/msg/proto/pay-lpcost.d.ts +6 -0
  100. package/dist/src/protos/msg/proto/player-hint.d.ts +7 -0
  101. package/dist/src/protos/msg/proto/pos-change.d.ts +12 -0
  102. package/dist/src/protos/msg/proto/random-selected.d.ts +7 -0
  103. package/dist/src/protos/msg/proto/recover.d.ts +6 -0
  104. package/dist/src/protos/msg/proto/reload-field.d.ts +36 -0
  105. package/dist/src/protos/msg/proto/remove-counter.d.ts +9 -0
  106. package/dist/src/protos/msg/proto/reset-time.d.ts +6 -0
  107. package/dist/src/protos/msg/proto/retry.d.ts +4 -0
  108. package/dist/src/protos/msg/proto/reverse-deck.d.ts +4 -0
  109. package/dist/src/protos/msg/proto/rock-paper-scissors.d.ts +5 -0
  110. package/dist/src/protos/msg/proto/select-battlecmd.d.ts +25 -0
  111. package/dist/src/protos/msg/proto/select-card.d.ts +17 -0
  112. package/dist/src/protos/msg/proto/select-chain.d.ts +19 -0
  113. package/dist/src/protos/msg/proto/select-counter.d.ts +17 -0
  114. package/dist/src/protos/msg/proto/select-disfield.d.ts +7 -0
  115. package/dist/src/protos/msg/proto/select-effectyn.d.ts +11 -0
  116. package/dist/src/protos/msg/proto/select-idlecmd.d.ts +37 -0
  117. package/dist/src/protos/msg/proto/select-option.d.ts +7 -0
  118. package/dist/src/protos/msg/proto/select-place.d.ts +7 -0
  119. package/dist/src/protos/msg/proto/select-position.d.ts +7 -0
  120. package/dist/src/protos/msg/proto/select-sum.d.ts +20 -0
  121. package/dist/src/protos/msg/proto/select-tribute.d.ts +18 -0
  122. package/dist/src/protos/msg/proto/select-unselect-card.d.ts +20 -0
  123. package/dist/src/protos/msg/proto/select-yesno.d.ts +6 -0
  124. package/dist/src/protos/msg/proto/set.d.ts +5 -0
  125. package/dist/src/protos/msg/proto/shuffle-deck.d.ts +5 -0
  126. package/dist/src/protos/msg/proto/shuffle-extra.d.ts +8 -0
  127. package/dist/src/protos/msg/proto/shuffle-hand.d.ts +8 -0
  128. package/dist/src/protos/msg/proto/shuffle-set-card.d.ts +17 -0
  129. package/dist/src/protos/msg/proto/sort-card.d.ts +13 -0
  130. package/dist/src/protos/msg/proto/spsummoned.d.ts +4 -0
  131. package/dist/src/protos/msg/proto/spsummoning.d.ts +9 -0
  132. package/dist/src/protos/msg/proto/start.d.ts +14 -0
  133. package/dist/src/protos/msg/proto/summoned.d.ts +4 -0
  134. package/dist/src/protos/msg/proto/summoning.d.ts +9 -0
  135. package/dist/src/protos/msg/proto/swap-grave-deck.d.ts +5 -0
  136. package/dist/src/protos/msg/proto/swap.d.ts +12 -0
  137. package/dist/src/protos/msg/proto/tag-swap.d.ts +14 -0
  138. package/dist/src/protos/msg/proto/toss-coin.d.ts +7 -0
  139. package/dist/src/protos/msg/proto/toss-dice.d.ts +7 -0
  140. package/dist/src/protos/msg/proto/update-card.d.ts +14 -0
  141. package/dist/src/protos/msg/proto/update-data.d.ts +12 -0
  142. package/dist/src/protos/msg/proto/waiting.d.ts +4 -0
  143. package/dist/src/protos/msg/proto/win.d.ts +6 -0
  144. package/dist/src/protos/msg/registry.d.ts +3 -0
  145. package/dist/src/protos/stoc/base.d.ts +17 -0
  146. package/dist/src/protos/stoc/index.d.ts +3 -0
  147. package/dist/src/protos/stoc/proto/change-side.d.ts +4 -0
  148. package/dist/src/protos/stoc/proto/chat.d.ts +10 -0
  149. package/dist/src/protos/stoc/proto/create-game.d.ts +5 -0
  150. package/dist/src/protos/stoc/proto/deck-count.d.ts +5 -0
  151. package/dist/src/protos/stoc/proto/duel-end.d.ts +4 -0
  152. package/dist/src/protos/stoc/proto/duel-start.d.ts +4 -0
  153. package/dist/src/protos/stoc/proto/error-msg.d.ts +6 -0
  154. package/dist/src/protos/stoc/proto/field-finish.d.ts +4 -0
  155. package/dist/src/protos/stoc/proto/game-msg.d.ts +9 -0
  156. package/dist/src/protos/stoc/proto/hand-result.d.ts +6 -0
  157. package/dist/src/protos/stoc/proto/hs-player-change.d.ts +5 -0
  158. package/dist/src/protos/stoc/proto/hs-player-enter.d.ts +6 -0
  159. package/dist/src/protos/stoc/proto/hs-watch-change.d.ts +5 -0
  160. package/dist/src/protos/stoc/proto/index.d.ts +24 -0
  161. package/dist/src/protos/stoc/proto/join-game.d.ts +6 -0
  162. package/dist/src/protos/stoc/proto/leave-game.d.ts +5 -0
  163. package/dist/src/protos/stoc/proto/replay.d.ts +11 -0
  164. package/dist/src/protos/stoc/proto/select-hand.d.ts +4 -0
  165. package/dist/src/protos/stoc/proto/select-tp.d.ts +4 -0
  166. package/dist/src/protos/stoc/proto/srvpro-roomlist.d.ts +18 -0
  167. package/dist/src/protos/stoc/proto/teammate-surrender.d.ts +4 -0
  168. package/dist/src/protos/stoc/proto/time-limit.d.ts +6 -0
  169. package/dist/src/protos/stoc/proto/tp-result.d.ts +4 -0
  170. package/dist/src/protos/stoc/proto/type-change.d.ts +5 -0
  171. package/dist/src/protos/stoc/proto/waiting-side.d.ts +4 -0
  172. package/dist/src/protos/stoc/registry.d.ts +3 -0
  173. package/dist/src/vendor/ocgcore-constants.d.ts +360 -0
  174. package/dist/src/vendor/script-constants.d.ts +836 -0
  175. package/index.ts +6 -0
  176. package/package.json +74 -0
  177. package/scripts/gen-constants.js +156 -0
  178. package/tsconfig.json +20 -0
@@ -0,0 +1,598 @@
1
+ # toFullPayload / fromFullPayload 方法添加
2
+
3
+ ## 更新日期
4
+ 2026-02-02
5
+
6
+ ## 更新概述
7
+
8
+ 在 `YGOProCtosBase` 和 `YGOProStocBase` 基类中添加了 `toFullPayload()` 和 `fromFullPayload()` 方法,用于处理包含完整 header(length + identifier)的数据包。
9
+
10
+ ## 动机
11
+
12
+ ### 之前的方式
13
+
14
+ 在添加这些方法之前,测试代码需要使用辅助函数手动构建完整数据包:
15
+
16
+ ```typescript
17
+ // 旧方式:使用辅助函数
18
+ function createCtosPacket(protocol: YGOProCtosBase): Uint8Array {
19
+ const body = protocol.toPayload();
20
+ const length = 1 + body.length;
21
+ const fullPayload = new Uint8Array(3 + body.length);
22
+ fullPayload[0] = length & 0xff;
23
+ fullPayload[1] = (length >> 8) & 0xff;
24
+ fullPayload[2] = protocol.identifier;
25
+ fullPayload.set(body, 3);
26
+ return fullPayload;
27
+ }
28
+
29
+ const playerInfo = new YGOProCtosPlayerInfo();
30
+ const fullPayload = createCtosPacket(playerInfo); // 需要辅助函数
31
+ ```
32
+
33
+ ### 现在的方式
34
+
35
+ 现在可以直接调用基类方法:
36
+
37
+ ```typescript
38
+ // 新方式:直接调用方法
39
+ const playerInfo = new YGOProCtosPlayerInfo();
40
+ const fullPayload = playerInfo.toFullPayload(); // 一行搞定!
41
+ ```
42
+
43
+ ## 实现细节
44
+
45
+ ### 协议格式
46
+
47
+ ```
48
+ CTOS/STOC 完整数据包格式:
49
+ ┌────────────┬────────────────┬──────────────┐
50
+ │ Length │ Identifier │ Body │
51
+ │ 2 bytes LE │ 1 byte │ Variable │
52
+ └────────────┴────────────────┴──────────────┘
53
+
54
+ 其中 Length = 1 (identifier) + body.length
55
+ ```
56
+
57
+ ### 方法签名
58
+
59
+ #### toFullPayload()
60
+
61
+ ```typescript
62
+ /**
63
+ * Serialize to full payload including header (length + identifier + body)
64
+ * Format: [length 2 bytes LE][identifier 1 byte][body]
65
+ * Length = 1 (identifier) + body.length
66
+ */
67
+ toFullPayload(): Uint8Array
68
+ ```
69
+
70
+ **功能**:
71
+ 1. 调用 `toPayload()` 获取 body
72
+ 2. 计算 length = 1 + body.length
73
+ 3. 创建完整数据包:[length(2), identifier(1), body]
74
+ 4. 返回 `Uint8Array`
75
+
76
+ **示例**:
77
+ ```typescript
78
+ const chat = new YGOProCtosChat();
79
+ chat.msg = "Hello";
80
+ const fullPayload = chat.toFullPayload();
81
+ // fullPayload = [15, 0, 0x16, ...body bytes...]
82
+ // ^^^ length (13 = 1 + 12)
83
+ // ^^^^ identifier
84
+ ```
85
+
86
+ #### fromFullPayload(data)
87
+
88
+ ```typescript
89
+ /**
90
+ * Deserialize from full payload including header (length + identifier + body)
91
+ * Format: [length 2 bytes LE][identifier 1 byte][body]
92
+ * @param data - Full payload data
93
+ * @returns this instance
94
+ * @throws Error if data is too short or identifier mismatch
95
+ */
96
+ fromFullPayload(data: Uint8Array): this
97
+ ```
98
+
99
+ **功能**:
100
+ 1. 验证数据至少 3 字节
101
+ 2. 读取 length(2 字节,小端序)
102
+ 3. 读取 identifier(1 字节)
103
+ 4. 验证 identifier 是否匹配
104
+ 5. 如果数据长度 > 声明长度:截断到声明长度
105
+ 6. 如果数据长度 < 声明长度:抛出错误
106
+ 7. 调用 `fromPayload()` 解析 body
107
+
108
+ **示例**:
109
+ ```typescript
110
+ const fullPayload = new Uint8Array([5, 0, 0x03, 0x01, 0x00, 0x00, 0x00]);
111
+ // ^^^^ ^^^^ ^^^^^^^^^^^^^^^^^
112
+ // len id body
113
+
114
+ const handResult = new YGOProCtosHandResult();
115
+ handResult.fromFullPayload(fullPayload);
116
+ console.log(handResult.res); // 1
117
+ ```
118
+
119
+ ### 错误处理
120
+
121
+ #### 1. 数据太短
122
+ ```typescript
123
+ const shortPayload = new Uint8Array([5, 0]); // 只有 2 字节
124
+ const protocol = new YGOProCtosHandResult();
125
+ protocol.fromFullPayload(shortPayload);
126
+ // ❌ 抛出: CTOS payload too short: expected at least 3 bytes, got 2
127
+ ```
128
+
129
+ #### 2. identifier 不匹配
130
+ ```typescript
131
+ const wrongId = new Uint8Array([5, 0, 0xFF, 0x01, 0x00, 0x00, 0x00]);
132
+ // ^^^^ 错误的 identifier
133
+ const protocol = new YGOProCtosHandResult(); // 期望 0x03
134
+ protocol.fromFullPayload(wrongId);
135
+ // ❌ 抛出: CTOS identifier mismatch: expected 0x3, got 0xff
136
+ ```
137
+
138
+ #### 3. 声明长度不足
139
+ ```typescript
140
+ const fullPayload = new Uint8Array([10, 0, 0x03, 0x01]); // 声明 10 字节但只有 4 字节
141
+ const protocol = new YGOProCtosHandResult();
142
+ protocol.fromFullPayload(fullPayload);
143
+ // ❌ 抛出: CTOS payload too short: declared length 10 requires 12 bytes total, got 4
144
+ ```
145
+
146
+ #### 4. 数据超长(自动截断)
147
+ ```typescript
148
+ const extended = new Uint8Array([5, 0, 0x03, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF]);
149
+ // ^^^^^^^^^^
150
+ // 额外数据
151
+ const protocol = new YGOProCtosHandResult();
152
+ protocol.fromFullPayload(extended);
153
+ // ✅ 成功:自动截断额外数据,只解析前 5+3 字节
154
+ console.log(protocol.res); // 1
155
+ ```
156
+
157
+ ## 实现代码
158
+
159
+ ### YGOProCtosBase
160
+
161
+ ```typescript
162
+ export class YGOProCtosBase extends PayloadBase {
163
+ get identifier(): number {
164
+ return (this.constructor as typeof YGOProCtosBase).identifier;
165
+ }
166
+
167
+ static identifier: number;
168
+
169
+ toFullPayload(): Uint8Array {
170
+ const body = this.toPayload();
171
+ const length = 1 + body.length;
172
+ const fullPayload = new Uint8Array(3 + body.length);
173
+
174
+ fullPayload[0] = length & 0xff;
175
+ fullPayload[1] = (length >> 8) & 0xff;
176
+ fullPayload[2] = this.identifier;
177
+ fullPayload.set(body, 3);
178
+
179
+ return fullPayload;
180
+ }
181
+
182
+ fromFullPayload(data: Uint8Array): this {
183
+ if (data.length < 3) {
184
+ throw new Error(
185
+ `CTOS payload too short: expected at least 3 bytes, got ${data.length}`,
186
+ );
187
+ }
188
+
189
+ const declaredLength = data[0] | (data[1] << 8);
190
+ const identifier = data[2];
191
+
192
+ if (identifier !== this.identifier) {
193
+ throw new Error(
194
+ `CTOS identifier mismatch: expected 0x${this.identifier.toString(16)}, got 0x${identifier.toString(16)}`,
195
+ );
196
+ }
197
+
198
+ const expectedTotalLength = 3 + declaredLength - 1;
199
+
200
+ if (data.length < expectedTotalLength) {
201
+ throw new Error(
202
+ `CTOS payload too short: declared length ${declaredLength} requires ${expectedTotalLength} bytes total, got ${data.length}`,
203
+ );
204
+ }
205
+
206
+ const bodyData =
207
+ data.length > expectedTotalLength
208
+ ? data.slice(3, expectedTotalLength)
209
+ : data.slice(3);
210
+
211
+ return this.fromPayload(bodyData);
212
+ }
213
+ }
214
+ ```
215
+
216
+ ### YGOProStocBase
217
+
218
+ ```typescript
219
+ export class YGOProStocBase extends PayloadBase {
220
+ get identifier(): number {
221
+ return (this.constructor as typeof YGOProStocBase).identifier;
222
+ }
223
+
224
+ static identifier: number;
225
+
226
+ toFullPayload(): Uint8Array {
227
+ const body = this.toPayload();
228
+ const length = 1 + body.length;
229
+ const fullPayload = new Uint8Array(3 + body.length);
230
+
231
+ fullPayload[0] = length & 0xff;
232
+ fullPayload[1] = (length >> 8) & 0xff;
233
+ fullPayload[2] = this.identifier;
234
+ fullPayload.set(body, 3);
235
+
236
+ return fullPayload;
237
+ }
238
+
239
+ fromFullPayload(data: Uint8Array): this {
240
+ if (data.length < 3) {
241
+ throw new Error(
242
+ `STOC payload too short: expected at least 3 bytes, got ${data.length}`,
243
+ );
244
+ }
245
+
246
+ const declaredLength = data[0] | (data[1] << 8);
247
+ const identifier = data[2];
248
+
249
+ if (identifier !== this.identifier) {
250
+ throw new Error(
251
+ `STOC identifier mismatch: expected 0x${this.identifier.toString(16)}, got 0x${identifier.toString(16)}`,
252
+ );
253
+ }
254
+
255
+ const expectedTotalLength = 3 + declaredLength - 1;
256
+
257
+ if (data.length < expectedTotalLength) {
258
+ throw new Error(
259
+ `STOC payload too short: declared length ${declaredLength} requires ${expectedTotalLength} bytes total, got ${data.length}`,
260
+ );
261
+ }
262
+
263
+ const bodyData =
264
+ data.length > expectedTotalLength
265
+ ? data.slice(3, expectedTotalLength)
266
+ : data.slice(3);
267
+
268
+ return this.fromPayload(bodyData);
269
+ }
270
+ }
271
+ ```
272
+
273
+ ## 测试更新
274
+
275
+ ### 测试统计
276
+
277
+ | 文件 | 之前 | 现在 | 增加 |
278
+ |------|------|------|------|
279
+ | `ctos-stoc.spec.ts` | 9 | 13 | +4 |
280
+ | `srvpro-roomlist.spec.ts` | 6 | 6 | 0 |
281
+ | `chat-protocols.spec.ts` | 14 | 14 | 0 |
282
+ | **总计** | **96** | **101** | **+5** |
283
+
284
+ ### 新增测试
285
+
286
+ #### 1. fromFullPayload 基础测试
287
+ ```typescript
288
+ it('should use fromFullPayload correctly', () => {
289
+ const playerInfo = new YGOProCtosPlayerInfo();
290
+ playerInfo.name = Array.from({ length: 20 }, (_, i) =>
291
+ i < 4 ? 0x0041 + i : 0,
292
+ );
293
+
294
+ const fullPayload = playerInfo.toFullPayload();
295
+
296
+ const parsed = new YGOProCtosPlayerInfo();
297
+ parsed.fromFullPayload(fullPayload);
298
+
299
+ expect(parsed.name).toEqual(playerInfo.name);
300
+ });
301
+ ```
302
+
303
+ #### 2. 截断测试
304
+ ```typescript
305
+ it('should truncate extra data in fromFullPayload', () => {
306
+ const handResult = new YGOProCtosHandResult();
307
+ handResult.res = 1;
308
+
309
+ const fullPayload = handResult.toFullPayload();
310
+ // 添加额外字节
311
+ const extendedPayload = new Uint8Array(fullPayload.length + 10);
312
+ extendedPayload.set(fullPayload);
313
+ for (let i = fullPayload.length; i < extendedPayload.length; i++) {
314
+ extendedPayload[i] = 0xff;
315
+ }
316
+
317
+ const parsed = new YGOProCtosHandResult();
318
+ parsed.fromFullPayload(extendedPayload);
319
+
320
+ expect(parsed.res).toBe(1); // ✅ 自动截断,正确解析
321
+ });
322
+ ```
323
+
324
+ #### 3. 数据太短错误测试
325
+ ```typescript
326
+ it('should throw error if data too short', () => {
327
+ const handResult = new YGOProCtosHandResult();
328
+ handResult.res = 1;
329
+
330
+ const fullPayload = handResult.toFullPayload();
331
+ const shortPayload = fullPayload.slice(0, fullPayload.length - 1);
332
+
333
+ const parsed = new YGOProCtosHandResult();
334
+ expect(() => parsed.fromFullPayload(shortPayload)).toThrow(
335
+ /too short/i,
336
+ );
337
+ });
338
+ ```
339
+
340
+ #### 4. identifier 不匹配错误测试
341
+ ```typescript
342
+ it('should throw error on identifier mismatch', () => {
343
+ const handResult = new YGOProStocHandResult();
344
+ handResult.res1 = 1;
345
+ handResult.res2 = 2;
346
+
347
+ const fullPayload = handResult.toFullPayload();
348
+ fullPayload[2] = 0xff; // 修改 identifier
349
+
350
+ const parsed = new YGOProStocHandResult();
351
+ expect(() => parsed.fromFullPayload(fullPayload)).toThrow(
352
+ /identifier mismatch/i,
353
+ );
354
+ });
355
+ ```
356
+
357
+ #### 5. STOC fromFullPayload 测试
358
+ ```typescript
359
+ it('should use fromFullPayload correctly', () => {
360
+ const errorMsg = new YGOProStocErrorMsg();
361
+ errorMsg.msg = 3;
362
+ errorMsg.code = 0xabcdef01;
363
+
364
+ const fullPayload = errorMsg.toFullPayload();
365
+
366
+ const parsed = new YGOProStocErrorMsg();
367
+ parsed.fromFullPayload(fullPayload);
368
+
369
+ expect(parsed.msg).toBe(3);
370
+ expect(parsed.code).toBe(0xabcdef01);
371
+ });
372
+ ```
373
+
374
+ ### 测试覆盖
375
+
376
+ ✅ **基本功能**
377
+ - toFullPayload 正确生成完整数据包
378
+ - fromFullPayload 正确解析完整数据包
379
+ - 往返测试(serialize → deserialize)
380
+
381
+ ✅ **边界情况**
382
+ - 空消息
383
+ - 最大长度消息
384
+ - 超长数据自动截断
385
+
386
+ ✅ **错误处理**
387
+ - 数据太短抛出错误
388
+ - identifier 不匹配抛出错误
389
+ - 声明长度不足抛出错误
390
+
391
+ ✅ **兼容性**
392
+ - 与 Registry 系统兼容
393
+ - 所有现有测试继续通过
394
+
395
+ ## 测试结果
396
+
397
+ ```bash
398
+ ✅ Test Suites: 7 passed, 7 total
399
+ ✅ Tests: 101 passed, 101 total (+5 new)
400
+ ✅ Time: 11.556 s
401
+ ```
402
+
403
+ ## 使用示例
404
+
405
+ ### 基础用法
406
+
407
+ ```typescript
408
+ import { YGOProCtosPlayerInfo } from 'ygopro-msg-encode';
409
+
410
+ // 序列化
411
+ const playerInfo = new YGOProCtosPlayerInfo();
412
+ playerInfo.name = [0x0041, 0x0042, 0x0043, ...]; // "ABC"
413
+ const fullPayload = playerInfo.toFullPayload();
414
+
415
+ // 反序列化
416
+ const parsed = new YGOProCtosPlayerInfo();
417
+ parsed.fromFullPayload(fullPayload);
418
+ console.log(parsed.name); // [0x0041, 0x0042, 0x0043, ...]
419
+ ```
420
+
421
+ ### 可变长度字符串
422
+
423
+ ```typescript
424
+ import { YGOProCtosChat } from 'ygopro-msg-encode';
425
+
426
+ // 序列化聊天消息
427
+ const chat = new YGOProCtosChat();
428
+ chat.msg = "Hello, world!";
429
+ const fullPayload = chat.toFullPayload();
430
+
431
+ // 发送到服务器...
432
+ // send(fullPayload);
433
+
434
+ // 在服务器端反序列化
435
+ const received = new YGOProCtosChat();
436
+ received.fromFullPayload(fullPayload);
437
+ console.log(received.msg); // "Hello, world!"
438
+ ```
439
+
440
+ ### 错误处理
441
+
442
+ ```typescript
443
+ import { YGOProStocErrorMsg } from 'ygopro-msg-encode';
444
+
445
+ try {
446
+ const errorMsg = new YGOProStocErrorMsg();
447
+ errorMsg.fromFullPayload(receivedData);
448
+
449
+ console.log('Error code:', errorMsg.code);
450
+ console.log('Error message:', errorMsg.msg);
451
+ } catch (error) {
452
+ if (error.message.includes('too short')) {
453
+ console.error('数据包不完整');
454
+ } else if (error.message.includes('identifier mismatch')) {
455
+ console.error('协议类型不匹配');
456
+ } else {
457
+ console.error('解析失败:', error);
458
+ }
459
+ }
460
+ ```
461
+
462
+ ### 与 Registry 配合使用
463
+
464
+ ```typescript
465
+ import { YGOProCtos, YGOProCtosPlayerInfo } from 'ygopro-msg-encode';
466
+
467
+ // 方式 1: 使用 Registry 自动识别
468
+ const playerInfo = new YGOProCtosPlayerInfo();
469
+ playerInfo.name = Array(20).fill(0x0041);
470
+ const fullPayload = playerInfo.toFullPayload();
471
+
472
+ const parsed = YGOProCtos.getInstanceFromPayload(fullPayload);
473
+ if (parsed instanceof YGOProCtosPlayerInfo) {
474
+ console.log('Received player info:', parsed.name);
475
+ }
476
+
477
+ // 方式 2: 直接使用特定类解析
478
+ const playerInfo2 = new YGOProCtosPlayerInfo();
479
+ playerInfo2.fromFullPayload(fullPayload);
480
+ console.log('Direct parse:', playerInfo2.name);
481
+ ```
482
+
483
+ ## 迁移指南
484
+
485
+ ### 从辅助函数迁移
486
+
487
+ **之前**:
488
+ ```typescript
489
+ // 定义辅助函数
490
+ function createCtosPacket(protocol: YGOProCtosBase): Uint8Array {
491
+ const body = protocol.toPayload();
492
+ const length = 1 + body.length;
493
+ const fullPayload = new Uint8Array(3 + body.length);
494
+ fullPayload[0] = length & 0xff;
495
+ fullPayload[1] = (length >> 8) & 0xff;
496
+ fullPayload[2] = protocol.identifier;
497
+ fullPayload.set(body, 3);
498
+ return fullPayload;
499
+ }
500
+
501
+ // 使用辅助函数
502
+ const fullPayload = createCtosPacket(playerInfo);
503
+ ```
504
+
505
+ **之后**:
506
+ ```typescript
507
+ // 直接调用方法
508
+ const fullPayload = playerInfo.toFullPayload();
509
+ ```
510
+
511
+ ### 兼容性
512
+
513
+ 这是一个**向后兼容**的更新:
514
+ - ✅ 原有的 `toPayload()` 和 `fromPayload()` 方法不受影响
515
+ - ✅ Registry 系统继续正常工作
516
+ - ✅ 所有现有代码无需修改
517
+ - ✅ 新方法是可选的,可以渐进式采用
518
+
519
+ ## 性能考虑
520
+
521
+ ### 内存分配
522
+
523
+ ```typescript
524
+ // toFullPayload 分配一次内存
525
+ const fullPayload = protocol.toFullPayload();
526
+ // 内部: new Uint8Array(3 + body.length)
527
+
528
+ // fromFullPayload 可能有额外的 slice 操作
529
+ protocol.fromFullPayload(data);
530
+ // 如果需要截断: data.slice(3, expectedTotalLength)
531
+ ```
532
+
533
+ ### 优化建议
534
+
535
+ 1. **重用对象**: 避免每次都创建新实例
536
+ ```typescript
537
+ // ✅ 好
538
+ const protocol = new YGOProCtosChat();
539
+ protocol.msg = "message1";
540
+ const payload1 = protocol.toFullPayload();
541
+
542
+ protocol.msg = "message2";
543
+ const payload2 = protocol.toFullPayload();
544
+
545
+ // ❌ 不好
546
+ const payload1 = new YGOProCtosChat().toFullPayload();
547
+ const payload2 = new YGOProCtosChat().toFullPayload();
548
+ ```
549
+
550
+ 2. **批量处理**: 对多个消息使用数组
551
+ ```typescript
552
+ const messages = [msg1, msg2, msg3];
553
+ const payloads = messages.map(m => m.toFullPayload());
554
+ ```
555
+
556
+ ## 优势总结
557
+
558
+ ### 1. 简化 API ✨
559
+ - 一行代码完成完整数据包的序列化/反序列化
560
+ - 不需要手动处理 header
561
+ - 代码更简洁、更易读
562
+
563
+ ### 2. 类型安全 🔒
564
+ - 基类方法,所有协议自动继承
565
+ - TypeScript 类型检查
566
+ - IDE 自动完成
567
+
568
+ ### 3. 错误处理 🛡️
569
+ - 自动验证数据长度
570
+ - 自动验证 identifier
571
+ - 清晰的错误消息
572
+
573
+ ### 4. 灵活性 🎯
574
+ - 支持数据截断(处理超长数据)
575
+ - 与现有 API 完全兼容
576
+ - 可选使用,渐进式采用
577
+
578
+ ### 5. 测试友好 🧪
579
+ - 更容易编写单元测试
580
+ - 不需要重复的辅助函数
581
+ - 测试代码更简洁
582
+
583
+ ## 文档链接
584
+
585
+ 相关文档:
586
+ - `CTOS_STOC_IMPLEMENTATION.md` - 协议实现详细文档
587
+ - `TESTS_MIGRATION.md` - 测试迁移总结
588
+ - `PROJECT_COMPLETE.md` - 项目完成报告
589
+
590
+ ## 总结
591
+
592
+ ✅ **实现完成**: `toFullPayload()` 和 `fromFullPayload()` 方法已添加到基类
593
+ ✅ **测试通过**: 101 个测试全部通过(新增 5 个测试)
594
+ ✅ **向后兼容**: 所有现有代码继续工作
595
+ ✅ **文档完整**: 包含详细说明和示例
596
+ ✅ **Production Ready**: 可以直接用于生产环境
597
+
598
+ 这次更新大大简化了 CTOS/STOC 协议的使用,让开发者可以更专注于业务逻辑!🎉