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.
- package/.eslintignore +4 -0
- package/.eslintrc.js +25 -0
- package/.prettierrc +4 -0
- package/AGENTS.md +12 -0
- package/CHANGES.md +270 -0
- package/CTOS_STOC_IMPLEMENTATION.md +279 -0
- package/FINAL_SUMMARY.md +357 -0
- package/FULL_PAYLOAD_SUMMARY.md +491 -0
- package/FULL_PAYLOAD_UPDATE.md +598 -0
- package/IMPLEMENTATION_SUMMARY.md +294 -0
- package/LICENSE +22 -0
- package/MSG_IMPLEMENTATION_SUMMARY.md +358 -0
- package/OPPONENT_VIEW_SUMMARY.md +387 -0
- package/PROJECT_COMPLETE.md +565 -0
- package/QUICK_REFERENCE.md +352 -0
- package/README.md +494 -0
- package/REAL_IP_STRING_UPDATE.md +289 -0
- package/TESTS_MIGRATION.md +342 -0
- package/VARIABLE_LENGTH_STRINGS.md +224 -0
- package/VARIABLE_LENGTH_UPDATE.md +229 -0
- package/dist/index.cjs +5131 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +5185 -0
- package/dist/index.mjs.map +7 -0
- package/dist/src/binary/binary-meta.d.ts +18 -0
- package/dist/src/binary/fill-binary-fields.d.ts +2 -0
- package/dist/src/metadata.d.ts +10 -0
- package/dist/src/proto-base/payload-base.d.ts +8 -0
- package/dist/src/proto-base/registry-base.d.ts +13 -0
- package/dist/src/protos/common/host-info.d.ts +12 -0
- package/dist/src/protos/common/index.d.ts +1 -0
- package/dist/src/protos/ctos/base.d.ts +17 -0
- package/dist/src/protos/ctos/index.d.ts +3 -0
- package/dist/src/protos/ctos/proto/chat.d.ts +9 -0
- package/dist/src/protos/ctos/proto/create-game.d.ts +8 -0
- package/dist/src/protos/ctos/proto/external-address.d.ts +12 -0
- package/dist/src/protos/ctos/proto/hand-result.d.ts +5 -0
- package/dist/src/protos/ctos/proto/hs-notready.d.ts +4 -0
- package/dist/src/protos/ctos/proto/hs-ready.d.ts +4 -0
- package/dist/src/protos/ctos/proto/hs-start.d.ts +4 -0
- package/dist/src/protos/ctos/proto/hs-toduelist.d.ts +4 -0
- package/dist/src/protos/ctos/proto/hs-toobserver.d.ts +4 -0
- package/dist/src/protos/ctos/proto/index.d.ts +19 -0
- package/dist/src/protos/ctos/proto/join-game.d.ts +7 -0
- package/dist/src/protos/ctos/proto/kick.d.ts +5 -0
- package/dist/src/protos/ctos/proto/leave-game.d.ts +4 -0
- package/dist/src/protos/ctos/proto/player-info.d.ts +5 -0
- package/dist/src/protos/ctos/proto/request-field.d.ts +4 -0
- package/dist/src/protos/ctos/proto/response.d.ts +7 -0
- package/dist/src/protos/ctos/proto/surrender.d.ts +4 -0
- package/dist/src/protos/ctos/proto/time-confirm.d.ts +4 -0
- package/dist/src/protos/ctos/proto/tp-result.d.ts +5 -0
- package/dist/src/protos/ctos/proto/update-deck.d.ts +11 -0
- package/dist/src/protos/ctos/registry.d.ts +3 -0
- package/dist/src/protos/msg/base.d.ts +9 -0
- package/dist/src/protos/msg/index.d.ts +3 -0
- package/dist/src/protos/msg/proto/add-counter.d.ts +9 -0
- package/dist/src/protos/msg/proto/announce-attrib.d.ts +7 -0
- package/dist/src/protos/msg/proto/announce-card.d.ts +7 -0
- package/dist/src/protos/msg/proto/announce-number.d.ts +7 -0
- package/dist/src/protos/msg/proto/announce-race.d.ts +7 -0
- package/dist/src/protos/msg/proto/attack-disabled.d.ts +4 -0
- package/dist/src/protos/msg/proto/attack.d.ts +12 -0
- package/dist/src/protos/msg/proto/battle.d.ts +18 -0
- package/dist/src/protos/msg/proto/become-target.d.ts +6 -0
- package/dist/src/protos/msg/proto/cancel-target.d.ts +11 -0
- package/dist/src/protos/msg/proto/card-hint.d.ts +10 -0
- package/dist/src/protos/msg/proto/card-query.d.ts +38 -0
- package/dist/src/protos/msg/proto/card-target.d.ts +11 -0
- package/dist/src/protos/msg/proto/chain-disabled.d.ts +5 -0
- package/dist/src/protos/msg/proto/chain-end.d.ts +4 -0
- package/dist/src/protos/msg/proto/chain-negated.d.ts +5 -0
- package/dist/src/protos/msg/proto/chain-solved.d.ts +5 -0
- package/dist/src/protos/msg/proto/chain-solving.d.ts +5 -0
- package/dist/src/protos/msg/proto/chained.d.ts +5 -0
- package/dist/src/protos/msg/proto/chaining.d.ts +12 -0
- package/dist/src/protos/msg/proto/confirm-cards.d.ts +16 -0
- package/dist/src/protos/msg/proto/confirm-decktop.d.ts +14 -0
- package/dist/src/protos/msg/proto/confirm-extratop.d.ts +14 -0
- package/dist/src/protos/msg/proto/damage-step-end.d.ts +4 -0
- package/dist/src/protos/msg/proto/damage-step-start.d.ts +4 -0
- package/dist/src/protos/msg/proto/damage.d.ts +6 -0
- package/dist/src/protos/msg/proto/deck-top.d.ts +8 -0
- package/dist/src/protos/msg/proto/draw.d.ts +8 -0
- package/dist/src/protos/msg/proto/equip.d.ts +12 -0
- package/dist/src/protos/msg/proto/field-disabled.d.ts +5 -0
- package/dist/src/protos/msg/proto/flipsummoned.d.ts +4 -0
- package/dist/src/protos/msg/proto/flipsummoning.d.ts +9 -0
- package/dist/src/protos/msg/proto/hand-res.d.ts +5 -0
- package/dist/src/protos/msg/proto/hint.d.ts +7 -0
- package/dist/src/protos/msg/proto/index.d.ts +85 -0
- package/dist/src/protos/msg/proto/lpupdate.d.ts +6 -0
- package/dist/src/protos/msg/proto/match-kill.d.ts +5 -0
- package/dist/src/protos/msg/proto/missed-effect.d.ts +7 -0
- package/dist/src/protos/msg/proto/move.d.ts +14 -0
- package/dist/src/protos/msg/proto/new-phase.d.ts +5 -0
- package/dist/src/protos/msg/proto/new-turn.d.ts +5 -0
- package/dist/src/protos/msg/proto/pay-lpcost.d.ts +6 -0
- package/dist/src/protos/msg/proto/player-hint.d.ts +7 -0
- package/dist/src/protos/msg/proto/pos-change.d.ts +12 -0
- package/dist/src/protos/msg/proto/random-selected.d.ts +7 -0
- package/dist/src/protos/msg/proto/recover.d.ts +6 -0
- package/dist/src/protos/msg/proto/reload-field.d.ts +36 -0
- package/dist/src/protos/msg/proto/remove-counter.d.ts +9 -0
- package/dist/src/protos/msg/proto/reset-time.d.ts +6 -0
- package/dist/src/protos/msg/proto/retry.d.ts +4 -0
- package/dist/src/protos/msg/proto/reverse-deck.d.ts +4 -0
- package/dist/src/protos/msg/proto/rock-paper-scissors.d.ts +5 -0
- package/dist/src/protos/msg/proto/select-battlecmd.d.ts +25 -0
- package/dist/src/protos/msg/proto/select-card.d.ts +17 -0
- package/dist/src/protos/msg/proto/select-chain.d.ts +19 -0
- package/dist/src/protos/msg/proto/select-counter.d.ts +17 -0
- package/dist/src/protos/msg/proto/select-disfield.d.ts +7 -0
- package/dist/src/protos/msg/proto/select-effectyn.d.ts +11 -0
- package/dist/src/protos/msg/proto/select-idlecmd.d.ts +37 -0
- package/dist/src/protos/msg/proto/select-option.d.ts +7 -0
- package/dist/src/protos/msg/proto/select-place.d.ts +7 -0
- package/dist/src/protos/msg/proto/select-position.d.ts +7 -0
- package/dist/src/protos/msg/proto/select-sum.d.ts +20 -0
- package/dist/src/protos/msg/proto/select-tribute.d.ts +18 -0
- package/dist/src/protos/msg/proto/select-unselect-card.d.ts +20 -0
- package/dist/src/protos/msg/proto/select-yesno.d.ts +6 -0
- package/dist/src/protos/msg/proto/set.d.ts +5 -0
- package/dist/src/protos/msg/proto/shuffle-deck.d.ts +5 -0
- package/dist/src/protos/msg/proto/shuffle-extra.d.ts +8 -0
- package/dist/src/protos/msg/proto/shuffle-hand.d.ts +8 -0
- package/dist/src/protos/msg/proto/shuffle-set-card.d.ts +17 -0
- package/dist/src/protos/msg/proto/sort-card.d.ts +13 -0
- package/dist/src/protos/msg/proto/spsummoned.d.ts +4 -0
- package/dist/src/protos/msg/proto/spsummoning.d.ts +9 -0
- package/dist/src/protos/msg/proto/start.d.ts +14 -0
- package/dist/src/protos/msg/proto/summoned.d.ts +4 -0
- package/dist/src/protos/msg/proto/summoning.d.ts +9 -0
- package/dist/src/protos/msg/proto/swap-grave-deck.d.ts +5 -0
- package/dist/src/protos/msg/proto/swap.d.ts +12 -0
- package/dist/src/protos/msg/proto/tag-swap.d.ts +14 -0
- package/dist/src/protos/msg/proto/toss-coin.d.ts +7 -0
- package/dist/src/protos/msg/proto/toss-dice.d.ts +7 -0
- package/dist/src/protos/msg/proto/update-card.d.ts +14 -0
- package/dist/src/protos/msg/proto/update-data.d.ts +12 -0
- package/dist/src/protos/msg/proto/waiting.d.ts +4 -0
- package/dist/src/protos/msg/proto/win.d.ts +6 -0
- package/dist/src/protos/msg/registry.d.ts +3 -0
- package/dist/src/protos/stoc/base.d.ts +17 -0
- package/dist/src/protos/stoc/index.d.ts +3 -0
- package/dist/src/protos/stoc/proto/change-side.d.ts +4 -0
- package/dist/src/protos/stoc/proto/chat.d.ts +10 -0
- package/dist/src/protos/stoc/proto/create-game.d.ts +5 -0
- package/dist/src/protos/stoc/proto/deck-count.d.ts +5 -0
- package/dist/src/protos/stoc/proto/duel-end.d.ts +4 -0
- package/dist/src/protos/stoc/proto/duel-start.d.ts +4 -0
- package/dist/src/protos/stoc/proto/error-msg.d.ts +6 -0
- package/dist/src/protos/stoc/proto/field-finish.d.ts +4 -0
- package/dist/src/protos/stoc/proto/game-msg.d.ts +9 -0
- package/dist/src/protos/stoc/proto/hand-result.d.ts +6 -0
- package/dist/src/protos/stoc/proto/hs-player-change.d.ts +5 -0
- package/dist/src/protos/stoc/proto/hs-player-enter.d.ts +6 -0
- package/dist/src/protos/stoc/proto/hs-watch-change.d.ts +5 -0
- package/dist/src/protos/stoc/proto/index.d.ts +24 -0
- package/dist/src/protos/stoc/proto/join-game.d.ts +6 -0
- package/dist/src/protos/stoc/proto/leave-game.d.ts +5 -0
- package/dist/src/protos/stoc/proto/replay.d.ts +11 -0
- package/dist/src/protos/stoc/proto/select-hand.d.ts +4 -0
- package/dist/src/protos/stoc/proto/select-tp.d.ts +4 -0
- package/dist/src/protos/stoc/proto/srvpro-roomlist.d.ts +18 -0
- package/dist/src/protos/stoc/proto/teammate-surrender.d.ts +4 -0
- package/dist/src/protos/stoc/proto/time-limit.d.ts +6 -0
- package/dist/src/protos/stoc/proto/tp-result.d.ts +4 -0
- package/dist/src/protos/stoc/proto/type-change.d.ts +5 -0
- package/dist/src/protos/stoc/proto/waiting-side.d.ts +4 -0
- package/dist/src/protos/stoc/registry.d.ts +3 -0
- package/dist/src/vendor/ocgcore-constants.d.ts +360 -0
- package/dist/src/vendor/script-constants.d.ts +836 -0
- package/index.ts +6 -0
- package/package.json +74 -0
- package/scripts/gen-constants.js +156 -0
- 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 协议的使用,让开发者可以更专注于业务逻辑!🎉
|