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.
@@ -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) - 客户端发送更新后的卡组