ygopro-msg-encode 1.0.2 → 1.0.4
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/COMPOSITE_FIELDS_AUDIT.md +275 -0
- package/COMPOSITE_FIELDS_GUIDE.md +303 -0
- package/DECK_COUNT_EXPLANATION.md +124 -0
- package/ENUM_FIX_SUMMARY.md +84 -0
- package/ENUM_UPDATE.md +219 -0
- package/PROTOCOL_VERIFICATION_SUMMARY.md +410 -0
- package/README.md +32 -0
- package/REFACTOR_COMPOSITE_FIELDS.md +242 -0
- package/STOC_CHAT_FIX.md +156 -0
- package/UTF16_STRING_FIX.md +190 -0
- package/dist/index.cjs +177 -8
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +167 -8
- package/dist/index.mjs.map +4 -4
- package/dist/src/protos/common/host-info.d.ts +2 -1
- package/dist/src/protos/ctos/proto/create-game.d.ts +2 -2
- package/dist/src/protos/ctos/proto/hand-result.d.ts +2 -1
- package/dist/src/protos/ctos/proto/join-game.d.ts +1 -1
- package/dist/src/protos/ctos/proto/kick.d.ts +2 -1
- package/dist/src/protos/ctos/proto/player-info.d.ts +1 -1
- package/dist/src/protos/ctos/proto/tp-result.d.ts +2 -1
- package/dist/src/protos/msg/proto/start.d.ts +17 -0
- package/dist/src/protos/network-enums.d.ts +94 -0
- package/dist/src/protos/stoc/proto/deck-count.d.ts +15 -1
- package/dist/src/protos/stoc/proto/error-msg.d.ts +2 -1
- package/dist/src/protos/stoc/proto/hs-player-change.d.ts +23 -0
- package/dist/src/protos/stoc/proto/hs-player-enter.d.ts +1 -1
- package/dist/src/protos/stoc/proto/srvpro-roomlist.d.ts +2 -1
- package/dist/src/protos/stoc/proto/type-change.d.ts +23 -0
- package/index.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# 复合字段审计报告
|
|
2
|
+
|
|
3
|
+
本文档记录了对所有 CTOS、STOC、MSG 协议的复合字段(使用位运算的字段)审计结果。
|
|
4
|
+
|
|
5
|
+
## 审计范围
|
|
6
|
+
|
|
7
|
+
- **CTOS 协议**: 19 个
|
|
8
|
+
- **STOC 协议**: 24 个
|
|
9
|
+
- **MSG 协议**: 100+ 个
|
|
10
|
+
|
|
11
|
+
## 审计方法
|
|
12
|
+
|
|
13
|
+
1. 检查 `/home/nanahira/ygo/ygopro/gframe/network.h` 中的协议定义
|
|
14
|
+
2. 检查 `/home/nanahira/ygo/ygopro/gframe/duelclient.cpp` 中的解析代码
|
|
15
|
+
3. 检查 `/home/nanahira/ygo/ygopro/ocgcore/playerop.cpp` 中的 MSG 协议
|
|
16
|
+
4. 搜索位运算操作符:`<<`, `>>`, `& 0xf`, `& 0x0f`, `& 0xf0`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 发现的复合字段
|
|
21
|
+
|
|
22
|
+
### 1. STOC_TypeChange.type ✅ 已添加 getter/setter
|
|
23
|
+
|
|
24
|
+
**结构**: `uint8_t`
|
|
25
|
+
|
|
26
|
+
**位运算**:
|
|
27
|
+
- 低 4 位 (0x0F): 玩家位置 (0-7)
|
|
28
|
+
- 高 4 位 (0xF0): 房主标志 (0x10 = 房主)
|
|
29
|
+
|
|
30
|
+
**源码**: `duelclient.cpp:645-646`
|
|
31
|
+
```cpp
|
|
32
|
+
selftype = pkt->type & 0xf;
|
|
33
|
+
is_host = ((pkt->type >> 4) & 0xf) != 0;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**实现**:
|
|
37
|
+
- ✅ `get/set playerPosition`: 低 4 位
|
|
38
|
+
- ✅ `get/set isHost`: 高 4 位 (boolean)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### 2. STOC_HS_PlayerChange.status ✅ 已添加 getter/setter
|
|
43
|
+
|
|
44
|
+
**结构**: `uint8_t`
|
|
45
|
+
|
|
46
|
+
**位运算**:
|
|
47
|
+
- 低 4 位 (0x0F): 玩家状态 (PlayerChangeState 或位置 0-7)
|
|
48
|
+
- 高 4 位 (0xF0): 玩家位置 (0-3)
|
|
49
|
+
|
|
50
|
+
**源码**: `duelclient.cpp:991-992`
|
|
51
|
+
```cpp
|
|
52
|
+
unsigned char pos = (pkt->status >> 4) & 0xf;
|
|
53
|
+
unsigned char state = pkt->status & 0xf;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**实现**:
|
|
57
|
+
- ✅ `get/set playerPosition`: 高 4 位
|
|
58
|
+
- ✅ `get/set playerState`: 低 4 位
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### 3. MSG_START.playerType ✅ 已添加 getter/setter
|
|
63
|
+
|
|
64
|
+
**结构**: `uint8_t`
|
|
65
|
+
|
|
66
|
+
**位运算**:
|
|
67
|
+
- 低 4 位 (0x0F): 玩家编号 (0-3)
|
|
68
|
+
- 高 4 位 (0xF0): 观战者标志 (0x10 = 观战者)
|
|
69
|
+
|
|
70
|
+
**源码**: `duelclient.cpp:1429-1432`
|
|
71
|
+
```cpp
|
|
72
|
+
int playertype = BufferIO::Read<uint8_t>(pbuf);
|
|
73
|
+
mainGame->dInfo.isFirst = (playertype & 0xf) ? false : true;
|
|
74
|
+
if(playertype & 0xf0)
|
|
75
|
+
mainGame->dInfo.player_type = 7; // Observer
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**实现**:
|
|
79
|
+
- ✅ `get/set playerNumber`: 低 4 位
|
|
80
|
+
- ✅ `get/set observerFlag`: 高 4 位
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 不是复合字段的情况
|
|
85
|
+
|
|
86
|
+
### STOC_ErrorMsg.code (条件复合)
|
|
87
|
+
|
|
88
|
+
**注意**: 这个字段**不是**固定的复合字段,只在特定条件下使用位运算。
|
|
89
|
+
|
|
90
|
+
当 `msg == DECKERROR` 时:
|
|
91
|
+
- 高 4 位 (28-31): 卡组错误类型 (DeckErrorType)
|
|
92
|
+
- 低 28 位 (0-27): 卡片 ID
|
|
93
|
+
|
|
94
|
+
**源码**: `single_duel.cpp:358`
|
|
95
|
+
```cpp
|
|
96
|
+
scem.code = deckerror; // deckerror = (deckError << 28) | cardId
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**为什么不添加 getter/setter**:
|
|
100
|
+
- 这是条件性的,不是该字段的固定含义
|
|
101
|
+
- 在其他 `msg` 值下,`code` 有不同的含义
|
|
102
|
+
- 用户需要根据 `msg` 字段自己解析
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### get_info_location() 返回值
|
|
107
|
+
|
|
108
|
+
**不是协议字段**: `get_info_location()` 是 C++ 函数返回的**临时值**,不是协议定义的一部分。
|
|
109
|
+
|
|
110
|
+
**位运算** (card.cpp:444):
|
|
111
|
+
```cpp
|
|
112
|
+
return c | (l << 8) | (s << 16) | (ss << 24);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**在 TypeScript 中**: 这些信息被拆分为独立字段,不需要位运算:
|
|
116
|
+
```typescript
|
|
117
|
+
@BinaryField('u8', 0) controller: number;
|
|
118
|
+
@BinaryField('u8', 1) location: number;
|
|
119
|
+
@BinaryField('u8', 2) sequence: number;
|
|
120
|
+
@BinaryField('u8', 3) position: number;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### MSG 协议中的临时变量
|
|
126
|
+
|
|
127
|
+
**不是协议字段**: MSG 协议中很多位运算用于解析**客户端响应**,不是协议定义本身。
|
|
128
|
+
|
|
129
|
+
**例子** (playerop.cpp:70-71):
|
|
130
|
+
```cpp
|
|
131
|
+
int32_t t = (uint32_t)returns.ivalue[0] & 0xffff;
|
|
132
|
+
int32_t s = (uint32_t)returns.ivalue[0] >> 16;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
这是服务器端对客户端响应的解析,不是发送给客户端的协议字段。
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## CTOS 协议审计结果
|
|
140
|
+
|
|
141
|
+
检查了所有 19 个 CTOS 协议:
|
|
142
|
+
|
|
143
|
+
- ✅ CTOS_PlayerInfo - 无复合字段
|
|
144
|
+
- ✅ CTOS_CreateGame - 无复合字段
|
|
145
|
+
- ✅ CTOS_JoinGame - 无复合字段
|
|
146
|
+
- ✅ CTOS_LeaveGame - 无数据
|
|
147
|
+
- ✅ CTOS_Kick - 无复合字段(简单 enum)
|
|
148
|
+
- ✅ CTOS_HandResult - 无复合字段(简单 enum)
|
|
149
|
+
- ✅ CTOS_TPResult - 无复合字段(简单 enum)
|
|
150
|
+
- ✅ CTOS_UpdateDeck - 无复合字段
|
|
151
|
+
- ✅ CTOS_Response - 无复合字段
|
|
152
|
+
- ✅ CTOS_Surrender - 无数据
|
|
153
|
+
- ✅ CTOS_Chat - 无复合字段
|
|
154
|
+
- ✅ CTOS_HS_ToObserver - 无数据
|
|
155
|
+
- ✅ CTOS_HS_ToDuelist - 无数据
|
|
156
|
+
- ✅ CTOS_HS_Ready - 无数据
|
|
157
|
+
- ✅ CTOS_HS_NotReady - 无数据
|
|
158
|
+
- ✅ CTOS_HS_Start - 无数据
|
|
159
|
+
- ✅ CTOS_TimeConfirm - 无数据
|
|
160
|
+
- ✅ CTOS_RequestField - 无数据
|
|
161
|
+
- ✅ CTOS_ExternalAddress - 无复合字段
|
|
162
|
+
|
|
163
|
+
**结论**: CTOS 协议中**没有**复合字段。
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## STOC 协议审计结果
|
|
168
|
+
|
|
169
|
+
检查了所有 24 个 STOC 协议:
|
|
170
|
+
|
|
171
|
+
- ✅ STOC_GameMsg - 无复合字段(封装 MSG)
|
|
172
|
+
- ✅ STOC_ErrorMsg - code 是条件复合(见上文)
|
|
173
|
+
- ✅ STOC_SelectHand - 无数据
|
|
174
|
+
- ✅ STOC_SelectTP - 无数据
|
|
175
|
+
- ✅ STOC_HandResult - 无复合字段
|
|
176
|
+
- ✅ STOC_TPResult - 无复合字段
|
|
177
|
+
- ✅ STOC_ChangeSide - 无数据
|
|
178
|
+
- ✅ STOC_WaitingSide - 无数据
|
|
179
|
+
- ✅ STOC_DeckCount - 结构化对象(已改进)
|
|
180
|
+
- ✅ STOC_CreateGame - 无数据
|
|
181
|
+
- ✅ STOC_JoinGame - 无复合字段
|
|
182
|
+
- ✅ **STOC_TypeChange** - ✅ 已添加 getter/setter
|
|
183
|
+
- ✅ STOC_LeaveGame - 无复合字段
|
|
184
|
+
- ✅ STOC_DuelStart - 无数据
|
|
185
|
+
- ✅ STOC_DuelEnd - 无数据
|
|
186
|
+
- ✅ STOC_Replay - 无复合字段
|
|
187
|
+
- ✅ STOC_TimeLimit - 无复合字段
|
|
188
|
+
- ✅ STOC_Chat - 无复合字段(player_type 是条件性的)
|
|
189
|
+
- ✅ STOC_HS_PlayerEnter - 无复合字段
|
|
190
|
+
- ✅ **STOC_HS_PlayerChange** - ✅ 已添加 getter/setter
|
|
191
|
+
- ✅ STOC_HS_WatchChange - 无复合字段
|
|
192
|
+
- ✅ STOC_TeammateSurrender - 无数据
|
|
193
|
+
- ✅ STOC_FieldFinish - 无数据
|
|
194
|
+
- ✅ STOC_SRVPRO_ROOMLIST - 无复合字段
|
|
195
|
+
|
|
196
|
+
**结论**: STOC 协议中有 **2 个复合字段**,已全部添加 getter/setter。
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## MSG 协议审计结果
|
|
201
|
+
|
|
202
|
+
检查了所有需要客户端响应的 MSG 协议(18 个)和其他主要 MSG 协议:
|
|
203
|
+
|
|
204
|
+
- ✅ **MSG_START** - ✅ 已添加 getter/setter (playerType)
|
|
205
|
+
- ✅ MSG_SELECT_BATTLECMD - 无复合字段
|
|
206
|
+
- ✅ MSG_SELECT_IDLECMD - 无复合字段
|
|
207
|
+
- ✅ MSG_SELECT_EFFECTYN - 无复合字段
|
|
208
|
+
- ✅ MSG_SELECT_YESNO - 无复合字段
|
|
209
|
+
- ✅ MSG_SELECT_OPTION - 无复合字段
|
|
210
|
+
- ✅ MSG_SELECT_CARD - 无复合字段
|
|
211
|
+
- ✅ MSG_SELECT_CHAIN - 无复合字段
|
|
212
|
+
- ✅ MSG_SELECT_PLACE - 无复合字段
|
|
213
|
+
- ✅ MSG_SELECT_POSITION - 无复合字段
|
|
214
|
+
- ✅ MSG_SELECT_TRIBUTE - 无复合字段
|
|
215
|
+
- ✅ MSG_SELECT_COUNTER - 无复合字段
|
|
216
|
+
- ✅ MSG_SELECT_SUM - 无复合字段
|
|
217
|
+
- ✅ MSG_SELECT_DISFIELD - 无复合字段
|
|
218
|
+
- ✅ MSG_SORT_CARD - 无复合字段
|
|
219
|
+
- ✅ MSG_ANNOUNCE_RACE - 无复合字段
|
|
220
|
+
- ✅ MSG_ANNOUNCE_ATTRIB - 无复合字段
|
|
221
|
+
- ✅ MSG_ANNOUNCE_CARD - 无复合字段
|
|
222
|
+
- ✅ MSG_ANNOUNCE_NUMBER - 无复合字段
|
|
223
|
+
- ✅ 其他 MSG (80+) - 无复合字段(信息显示类消息)
|
|
224
|
+
|
|
225
|
+
**注意**: MSG 协议中的位运算主要用于:
|
|
226
|
+
1. 临时变量解析(如 `returns.ivalue[0]` 的拆分)
|
|
227
|
+
2. C++ 函数返回值(如 `get_info_location()`)
|
|
228
|
+
3. 这些不是协议字段定义本身
|
|
229
|
+
|
|
230
|
+
**结论**: MSG 协议中只有 **1 个复合字段** (MSG_START.playerType),已添加 getter/setter。
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 总结
|
|
235
|
+
|
|
236
|
+
### 复合字段统计
|
|
237
|
+
|
|
238
|
+
- **CTOS**: 0 个复合字段
|
|
239
|
+
- **STOC**: 2 个复合字段(已全部添加 getter/setter)
|
|
240
|
+
- **MSG**: 1 个复合字段(已添加 getter/setter)
|
|
241
|
+
- **总计**: 3 个复合字段,✅ 已全部添加 getter/setter
|
|
242
|
+
|
|
243
|
+
### 实现的 getter/setter
|
|
244
|
+
|
|
245
|
+
1. ✅ `YGOProStocTypeChange`
|
|
246
|
+
- `playerPosition` (get/set)
|
|
247
|
+
- `isHost` (get/set)
|
|
248
|
+
|
|
249
|
+
2. ✅ `YGOProStocHsPlayerChange`
|
|
250
|
+
- `playerPosition` (get/set)
|
|
251
|
+
- `playerState` (get/set)
|
|
252
|
+
|
|
253
|
+
3. ✅ `YGOProMsgStart`
|
|
254
|
+
- `playerNumber` (get/set)
|
|
255
|
+
- `observerFlag` (get/set)
|
|
256
|
+
|
|
257
|
+
### 测试结果
|
|
258
|
+
|
|
259
|
+
- ✅ 所有 101 个测试通过
|
|
260
|
+
- ✅ 构建成功
|
|
261
|
+
- ✅ 类型检查通过
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 审计完成日期
|
|
266
|
+
|
|
267
|
+
2026-02-02
|
|
268
|
+
|
|
269
|
+
## 参考源码
|
|
270
|
+
|
|
271
|
+
- `/home/nanahira/ygo/ygopro/gframe/network.h` - 协议定义
|
|
272
|
+
- `/home/nanahira/ygo/ygopro/gframe/duelclient.cpp` - 客户端解析
|
|
273
|
+
- `/home/nanahira/ygo/ygopro/gframe/single_duel.cpp` - 服务器端发送
|
|
274
|
+
- `/home/nanahira/ygo/ygopro/gframe/tag_duel.cpp` - 服务器端发送
|
|
275
|
+
- `/home/nanahira/ygo/ygopro/ocgcore/playerop.cpp` - MSG 协议定义
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# 复合字段使用指南
|
|
2
|
+
|
|
3
|
+
本文档说明 YGOPro 协议中使用位运算或特殊结构的复合字段。
|
|
4
|
+
|
|
5
|
+
## MSG_START.playerType
|
|
6
|
+
|
|
7
|
+
### 字段结构
|
|
8
|
+
|
|
9
|
+
`playerType` 是一个 `uint8_t` 字段,使用位运算编码了两种信息:
|
|
10
|
+
|
|
11
|
+
- **低 4 位 (0x0F)**: 玩家编号
|
|
12
|
+
- **高 4 位 (0xF0)**: 观战者标志
|
|
13
|
+
|
|
14
|
+
### 可能的值
|
|
15
|
+
|
|
16
|
+
| 值 | 十六进制 | 含义 |
|
|
17
|
+
|----|----------|------|
|
|
18
|
+
| 0 | 0x00 | 玩家 0(你先手) |
|
|
19
|
+
| 1 | 0x01 | 玩家 1(对手先手) |
|
|
20
|
+
| 16 | 0x10 | 观战者视角(观看玩家 0) |
|
|
21
|
+
| 17 | 0x11 | 观战者视角(观看玩家 1,swapped) |
|
|
22
|
+
|
|
23
|
+
### 使用示例
|
|
24
|
+
|
|
25
|
+
#### 使用 getter/setter(推荐)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { YGOProMsgStart } from 'ygopro-msg-encode';
|
|
29
|
+
|
|
30
|
+
const startMsg = new YGOProMsgStart();
|
|
31
|
+
startMsg.fromPayload(data);
|
|
32
|
+
|
|
33
|
+
// 读取低 4 位(玩家编号)
|
|
34
|
+
const playerNumber = startMsg.playerNumber; // 0-3
|
|
35
|
+
const isFirst = startMsg.playerNumber === 0;
|
|
36
|
+
|
|
37
|
+
// 读取高 4 位(观战者标志)
|
|
38
|
+
const observerFlag = startMsg.observerFlag; // 0x00 或 0x10
|
|
39
|
+
const isObserver = startMsg.observerFlag !== 0;
|
|
40
|
+
|
|
41
|
+
// 设置值
|
|
42
|
+
startMsg.playerNumber = 1; // 设置为玩家 1
|
|
43
|
+
startMsg.observerFlag = 0x10; // 设置为观战者
|
|
44
|
+
|
|
45
|
+
// 组合判断
|
|
46
|
+
if (isObserver) {
|
|
47
|
+
console.log(`You are watching player ${playerNumber}`);
|
|
48
|
+
} else if (isFirst) {
|
|
49
|
+
console.log('You go first!');
|
|
50
|
+
} else {
|
|
51
|
+
console.log('Opponent goes first!');
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### 直接访问(也可以)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// 直接读写 playerType
|
|
59
|
+
startMsg.playerType = 0x11; // 观战者视角,观看玩家 1
|
|
60
|
+
|
|
61
|
+
// 手动位运算
|
|
62
|
+
const playerNumber = startMsg.playerType & 0x0f;
|
|
63
|
+
const observerFlag = startMsg.playerType & 0xf0;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 源码参考
|
|
67
|
+
|
|
68
|
+
```cpp
|
|
69
|
+
// duelclient.cpp:1429-1432
|
|
70
|
+
int playertype = BufferIO::Read<uint8_t>(pbuf);
|
|
71
|
+
mainGame->dInfo.isFirst = (playertype & 0xf) ? false : true;
|
|
72
|
+
if(playertype & 0xf0)
|
|
73
|
+
mainGame->dInfo.player_type = 7; // Observer
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## STOC_DECK_COUNT 卡组数量
|
|
79
|
+
|
|
80
|
+
### 字段结构
|
|
81
|
+
|
|
82
|
+
使用嵌套对象结构存储双方玩家的卡组数量:
|
|
83
|
+
|
|
84
|
+
- `player0DeckCount`: `YGOProStocDeckCount_DeckInfo` - 玩家 0 的卡组信息
|
|
85
|
+
- `main`: `int16_t` - 主卡组数量
|
|
86
|
+
- `extra`: `int16_t` - 额外卡组数量
|
|
87
|
+
- `side`: `int16_t` - 副卡组数量
|
|
88
|
+
- `player1DeckCount`: `YGOProStocDeckCount_DeckInfo` - 玩家 1 的卡组信息
|
|
89
|
+
- `main`: `int16_t` - 主卡组数量
|
|
90
|
+
- `extra`: `int16_t` - 额外卡组数量
|
|
91
|
+
- `side`: `int16_t` - 副卡组数量
|
|
92
|
+
|
|
93
|
+
### 使用示例
|
|
94
|
+
|
|
95
|
+
#### 直接访问对象属性(推荐)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { YGOProStocDeckCount } from 'ygopro-msg-encode';
|
|
99
|
+
|
|
100
|
+
const deckCount = new YGOProStocDeckCount();
|
|
101
|
+
deckCount.fromFullPayload(data);
|
|
102
|
+
|
|
103
|
+
// 访问玩家 0 的卡组数量
|
|
104
|
+
console.log(`Your deck: Main=${deckCount.player0DeckCount.main}, Extra=${deckCount.player0DeckCount.extra}, Side=${deckCount.player0DeckCount.side}`);
|
|
105
|
+
|
|
106
|
+
// 访问玩家 1 的卡组数量
|
|
107
|
+
console.log(`Opponent: Main=${deckCount.player1DeckCount.main}, Extra=${deckCount.player1DeckCount.extra}, Side=${deckCount.player1DeckCount.side}`);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### 设置卡组数量
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { YGOProStocDeckCount, YGOProStocDeckCount_DeckInfo } from 'ygopro-msg-encode';
|
|
114
|
+
|
|
115
|
+
const deckCount = new YGOProStocDeckCount();
|
|
116
|
+
deckCount.player0DeckCount = new YGOProStocDeckCount_DeckInfo();
|
|
117
|
+
deckCount.player1DeckCount = new YGOProStocDeckCount_DeckInfo();
|
|
118
|
+
|
|
119
|
+
// 设置玩家 0
|
|
120
|
+
deckCount.player0DeckCount.main = 40;
|
|
121
|
+
deckCount.player0DeckCount.extra = 15;
|
|
122
|
+
deckCount.player0DeckCount.side = 15;
|
|
123
|
+
|
|
124
|
+
// 设置玩家 1
|
|
125
|
+
deckCount.player1DeckCount.main = 42;
|
|
126
|
+
deckCount.player1DeckCount.extra = 14;
|
|
127
|
+
deckCount.player1DeckCount.side = 13;
|
|
128
|
+
|
|
129
|
+
const payload = deckCount.toFullPayload();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 源码参考
|
|
133
|
+
|
|
134
|
+
```cpp
|
|
135
|
+
// single_duel.cpp:485-490 (发送端)
|
|
136
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].main.size());
|
|
137
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].extra.size());
|
|
138
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].side.size());
|
|
139
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].main.size());
|
|
140
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].extra.size());
|
|
141
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].side.size());
|
|
142
|
+
|
|
143
|
+
// duelclient.cpp:541-548 (接收端)
|
|
144
|
+
int deckc = BufferIO::Read<uint16_t>(pdata);
|
|
145
|
+
int extrac = BufferIO::Read<uint16_t>(pdata);
|
|
146
|
+
int sidec = BufferIO::Read<uint16_t>(pdata);
|
|
147
|
+
mainGame->dField.Initial(0, deckc, extrac, sidec);
|
|
148
|
+
deckc = BufferIO::Read<uint16_t>(pdata);
|
|
149
|
+
extrac = BufferIO::Read<uint16_t>(pdata);
|
|
150
|
+
sidec = BufferIO::Read<uint16_t>(pdata);
|
|
151
|
+
mainGame->dField.Initial(1, deckc, extrac, sidec);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## STOC_TYPE_CHANGE.type
|
|
157
|
+
|
|
158
|
+
### 字段结构
|
|
159
|
+
|
|
160
|
+
`type` 是一个 `uint8_t` 字段,使用位运算编码玩家位置和房主状态:
|
|
161
|
+
|
|
162
|
+
- **低 4 位 (0x0F)**: 玩家位置 (0-7)
|
|
163
|
+
- **高 4 位 (0xF0)**: 房主标志 (0x10 = 房主, 0x00 = 非房主)
|
|
164
|
+
|
|
165
|
+
### 使用示例
|
|
166
|
+
|
|
167
|
+
#### 使用 getter/setter(推荐)
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { YGOProStocTypeChange } from 'ygopro-msg-encode';
|
|
171
|
+
|
|
172
|
+
const typeChange = new YGOProStocTypeChange();
|
|
173
|
+
typeChange.fromFullPayload(data);
|
|
174
|
+
|
|
175
|
+
// 读取玩家位置
|
|
176
|
+
const pos = typeChange.playerPosition; // 0-7
|
|
177
|
+
|
|
178
|
+
// 读取房主状态
|
|
179
|
+
const isHost = typeChange.isHost; // true/false
|
|
180
|
+
|
|
181
|
+
// 设置值
|
|
182
|
+
typeChange.playerPosition = 2;
|
|
183
|
+
typeChange.isHost = true; // 设置为房主
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 直接访问(也可以)
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// 直接位运算
|
|
190
|
+
const pos = typeChange.type & 0x0f;
|
|
191
|
+
const isHost = ((typeChange.type >> 4) & 0x0f) !== 0;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 源码参考
|
|
195
|
+
|
|
196
|
+
```cpp
|
|
197
|
+
// duelclient.cpp:645-646
|
|
198
|
+
selftype = pkt->type & 0xf;
|
|
199
|
+
is_host = ((pkt->type >> 4) & 0xf) != 0;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## STOC_HS_PLAYER_CHANGE.status
|
|
205
|
+
|
|
206
|
+
### 字段结构
|
|
207
|
+
|
|
208
|
+
`status` 是一个 `uint8_t` 字段,使用位运算编码玩家位置和状态:
|
|
209
|
+
|
|
210
|
+
- **低 4 位 (0x0F)**: 玩家状态 (PlayerChangeState 或位置 0-7)
|
|
211
|
+
- **高 4 位 (0xF0)**: 玩家位置 (0-3)
|
|
212
|
+
|
|
213
|
+
### 使用示例
|
|
214
|
+
|
|
215
|
+
#### 使用 getter/setter(推荐)
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { YGOProStocHsPlayerChange, PlayerChangeState } from 'ygopro-msg-encode';
|
|
219
|
+
|
|
220
|
+
const playerChange = new YGOProStocHsPlayerChange();
|
|
221
|
+
playerChange.fromFullPayload(data);
|
|
222
|
+
|
|
223
|
+
// 读取玩家位置
|
|
224
|
+
const pos = playerChange.playerPosition; // 0-3
|
|
225
|
+
|
|
226
|
+
// 读取状态
|
|
227
|
+
const state = playerChange.playerState;
|
|
228
|
+
if (state === PlayerChangeState.READY) {
|
|
229
|
+
console.log(`Player ${pos} is ready!`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 设置值
|
|
233
|
+
playerChange.playerPosition = 1;
|
|
234
|
+
playerChange.playerState = PlayerChangeState.READY;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### 直接访问(也可以)
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// 直接位运算
|
|
241
|
+
const pos = (playerChange.status >> 4) & 0x0f;
|
|
242
|
+
const state = playerChange.status & 0x0f;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 源码参考
|
|
246
|
+
|
|
247
|
+
```cpp
|
|
248
|
+
// duelclient.cpp:991-992
|
|
249
|
+
unsigned char pos = (pkt->status >> 4) & 0xf;
|
|
250
|
+
unsigned char state = pkt->status & 0xf;
|
|
251
|
+
|
|
252
|
+
// single_duel.cpp:372 (设置值)
|
|
253
|
+
scpc.status = (dp->type << 4) | (is_ready ? PLAYERCHANGE_READY : PLAYERCHANGE_NOTREADY);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 其他复合字段
|
|
259
|
+
|
|
260
|
+
### STOC_ErrorMsg.code (当 msg == DECKERROR 时)
|
|
261
|
+
|
|
262
|
+
复合字段(未提供辅助方法,使用位运算):
|
|
263
|
+
|
|
264
|
+
- **高 4 位**: 卡组错误类型 (DeckErrorType)
|
|
265
|
+
- **低 28 位**: 卡片 ID
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
if (errorMsg.msg === ErrorMessageType.DECKERROR) {
|
|
269
|
+
const errorType = (errorMsg.code >> 28) & 0xf; // DeckErrorType
|
|
270
|
+
const cardId = errorMsg.code & 0x0fffffff; // Card ID
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 设计原则
|
|
277
|
+
|
|
278
|
+
这些复合字段的设计遵循以下原则:
|
|
279
|
+
|
|
280
|
+
1. **节省带宽**:在一个字节中编码多个信息
|
|
281
|
+
2. **向后兼容**:通过位运算扩展功能而不破坏现有结构
|
|
282
|
+
3. **简单解析**:使用简单的位运算即可提取信息
|
|
283
|
+
|
|
284
|
+
## 最佳实践
|
|
285
|
+
|
|
286
|
+
1. **使用对象属性和 getter/setter**:优先使用结构化的对象属性(如 `player0DeckCount.main`)和 getter/setter(如 `startMsg.playerNumber`)
|
|
287
|
+
2. **避免直接位运算**:除非有特殊需求,否则避免直接对 `playerType` 等字段进行位运算
|
|
288
|
+
3. **类型安全**:利用 TypeScript 的类型系统和枚举来确保正确性
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// ✅ 推荐:使用 getter/setter
|
|
292
|
+
if (startMsg.observerFlag !== 0) {
|
|
293
|
+
const watching = startMsg.playerNumber;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ✅ 推荐:使用对象属性
|
|
297
|
+
console.log(`Main deck: ${deckCount.player0DeckCount.main}`);
|
|
298
|
+
|
|
299
|
+
// ⚠️ 可以但不推荐:直接位运算
|
|
300
|
+
if (startMsg.playerType & 0xf0) {
|
|
301
|
+
const watching = startMsg.playerType & 0x0f;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# STOC_DECK_COUNT 字段说明
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
`STOC_DECK_COUNT` 协议在换边(Side Deck)阶段发送,用于告知客户端双方玩家的卡组数量信息。
|
|
6
|
+
|
|
7
|
+
## 字段结构
|
|
8
|
+
|
|
9
|
+
协议包含两个 `YGOProStocDeckCount_DeckInfo` 对象:
|
|
10
|
+
|
|
11
|
+
### player0DeckCount
|
|
12
|
+
|
|
13
|
+
- `main` (int16_t) - 玩家 0 的主卡组数量
|
|
14
|
+
- `extra` (int16_t) - 玩家 0 的额外卡组数量
|
|
15
|
+
- `side` (int16_t) - 玩家 0 的副卡组数量
|
|
16
|
+
|
|
17
|
+
### player1DeckCount
|
|
18
|
+
|
|
19
|
+
- `main` (int16_t) - 玩家 1 的主卡组数量
|
|
20
|
+
- `extra` (int16_t) - 玩家 1 的额外卡组数量
|
|
21
|
+
- `side` (int16_t) - 玩家 1 的副卡组数量
|
|
22
|
+
|
|
23
|
+
## 二进制布局
|
|
24
|
+
|
|
25
|
+
| 偏移 | 类型 | 字段 |
|
|
26
|
+
|------|------|------|
|
|
27
|
+
| 0 | int16_t | player0DeckCount.main |
|
|
28
|
+
| 2 | int16_t | player0DeckCount.extra |
|
|
29
|
+
| 4 | int16_t | player0DeckCount.side |
|
|
30
|
+
| 6 | int16_t | player1DeckCount.main |
|
|
31
|
+
| 8 | int16_t | player1DeckCount.extra |
|
|
32
|
+
| 10 | int16_t | player1DeckCount.side |
|
|
33
|
+
|
|
34
|
+
## 源码参考
|
|
35
|
+
|
|
36
|
+
### 服务器端发送 (single_duel.cpp:485-490)
|
|
37
|
+
|
|
38
|
+
```cpp
|
|
39
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].main.size());
|
|
40
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].extra.size());
|
|
41
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[0].side.size());
|
|
42
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].main.size());
|
|
43
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].extra.size());
|
|
44
|
+
BufferIO::Write<uint16_t>(pbuf, (short)pdeck[1].side.size());
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 客户端接收 (duelclient.cpp:541-548)
|
|
48
|
+
|
|
49
|
+
```cpp
|
|
50
|
+
int deckc = BufferIO::Read<uint16_t>(pdata);
|
|
51
|
+
int extrac = BufferIO::Read<uint16_t>(pdata);
|
|
52
|
+
int sidec = BufferIO::Read<uint16_t>(pdata);
|
|
53
|
+
mainGame->dField.Initial(0, deckc, extrac, sidec);
|
|
54
|
+
deckc = BufferIO::Read<uint16_t>(pdata);
|
|
55
|
+
extrac = BufferIO::Read<uint16_t>(pdata);
|
|
56
|
+
sidec = BufferIO::Read<uint16_t>(pdata);
|
|
57
|
+
mainGame->dField.Initial(1, deckc, extrac, sidec);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 使用示例
|
|
61
|
+
|
|
62
|
+
### 读取卡组数量
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { YGOProStocDeckCount } from 'ygopro-msg-encode';
|
|
66
|
+
|
|
67
|
+
const deckCount = new YGOProStocDeckCount();
|
|
68
|
+
deckCount.fromFullPayload(data);
|
|
69
|
+
|
|
70
|
+
// 访问玩家 0 的卡组数量
|
|
71
|
+
console.log(`Player 0: Main=${deckCount.player0DeckCount.main}, Extra=${deckCount.player0DeckCount.extra}, Side=${deckCount.player0DeckCount.side}`);
|
|
72
|
+
|
|
73
|
+
// 访问玩家 1 的卡组数量
|
|
74
|
+
console.log(`Player 1: Main=${deckCount.player1DeckCount.main}, Extra=${deckCount.player1DeckCount.extra}, Side=${deckCount.player1DeckCount.side}`);
|
|
75
|
+
|
|
76
|
+
// 简洁写法
|
|
77
|
+
const { main, extra, side } = deckCount.player0DeckCount;
|
|
78
|
+
console.log(`Your deck: ${main}+${extra}+${side}`);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 设置卡组数量
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { YGOProStocDeckCount, YGOProStocDeckCount_DeckInfo } from 'ygopro-msg-encode';
|
|
85
|
+
|
|
86
|
+
const deckCount = new YGOProStocDeckCount();
|
|
87
|
+
|
|
88
|
+
// 初始化对象
|
|
89
|
+
deckCount.player0DeckCount = new YGOProStocDeckCount_DeckInfo();
|
|
90
|
+
deckCount.player1DeckCount = new YGOProStocDeckCount_DeckInfo();
|
|
91
|
+
|
|
92
|
+
// 设置玩家 0 的卡组数量
|
|
93
|
+
deckCount.player0DeckCount.main = 40;
|
|
94
|
+
deckCount.player0DeckCount.extra = 15;
|
|
95
|
+
deckCount.player0DeckCount.side = 15;
|
|
96
|
+
|
|
97
|
+
// 设置玩家 1 的卡组数量
|
|
98
|
+
deckCount.player1DeckCount.main = 42;
|
|
99
|
+
deckCount.player1DeckCount.extra = 14;
|
|
100
|
+
deckCount.player1DeckCount.side = 13;
|
|
101
|
+
|
|
102
|
+
const payload = deckCount.toFullPayload();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 使用场景
|
|
106
|
+
|
|
107
|
+
这个协议主要在以下场景使用:
|
|
108
|
+
|
|
109
|
+
1. **换边阶段前**:告知客户端双方当前的卡组配置
|
|
110
|
+
2. **匹配赛(Match)中**:在第二局或第三局开始前发送,让玩家知道对手是否更换了副卡组
|
|
111
|
+
|
|
112
|
+
## 注意事项
|
|
113
|
+
|
|
114
|
+
1. **对象初始化**:使用前需要创建 `YGOProStocDeckCount_DeckInfo` 实例
|
|
115
|
+
2. **数值范围**:每个值都是 `int16_t`(-32768 到 32767),但实际使用时应该是非负整数
|
|
116
|
+
3. **玩家索引**:Player 0 通常是先手,Player 1 通常是后手(但需要根据 `MSG_START` 确认)
|
|
117
|
+
4. **副卡组融合怪兽**:在某些服务器配置下(`DUEL_FLAG_SIDEINS`),副卡组中的融合/同调/超量/连接怪兽会被计入额外卡组数量
|
|
118
|
+
5. **类型安全**:使用对象属性访问提供更好的类型检查和 IDE 自动补全
|
|
119
|
+
|
|
120
|
+
## 相关协议
|
|
121
|
+
|
|
122
|
+
- `STOC_CHANGE_SIDE` (0x07) - 通知客户端进入换边阶段
|
|
123
|
+
- `STOC_WAITING_SIDE` (0x08) - 等待对手换边
|
|
124
|
+
- `CTOS_UPDATE_DECK` (0x02) - 客户端发送更新后的卡组
|