v0版本完成:新增 4 篇详细文档(README 精简内容的落点)
README 为了防冗余做了精简,被移出的详细内容写成文档放这里:
- 功能票使用说明.md 买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md reincarnate 完整流程、分批规则(避开 2 首)、
续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md 二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
This commit is contained in:
+173
@@ -0,0 +1,173 @@
|
||||
# 功能票使用说明(购买 / 消耗)
|
||||
|
||||
> 本文是 [`README.md`](https://git.fragrance.moe/Txiaochen/wahappy) 里功能票部分的**详细版**。
|
||||
> 购买接口的完整排查过程见 [购买功能票流程.md](购买功能票流程.md) 与
|
||||
> [购买功能票_存档与交接.md](购买功能票_存档与交接.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一、票的种类
|
||||
|
||||
机台**可购买**的是付费票(`ticketKind=1`,`api.PAID_TICKET_IDS = (2,3,4,5,6)`):
|
||||
|
||||
| chargeId | 票 | 上报的 `price`(= 票的 `creditNum`) |
|
||||
|---|---|---|
|
||||
| 2 | 2倍チケット | 1 |
|
||||
| 3 | 3倍チケット | 2 |
|
||||
| 5 | 5倍チケット | 4 |
|
||||
|
||||
> 完整表(含 4/6 倍与活动票)见 `api/upsert_user_chargelog.py` 的 `TICKET_CREDIT_NUM`。
|
||||
>
|
||||
> ⚠️ **不同店铺上架的票不一样。** 买之前先 `--list` 看一眼 ——
|
||||
> 服务端对未上架的票会直接 `returnCode=0`(实测过)。
|
||||
|
||||
---
|
||||
|
||||
## 二、购买:`buy_ticket.py`
|
||||
|
||||
```bash
|
||||
python buy_ticket.py "<二维码>" --list # 只列当前票券和可买的票
|
||||
python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文
|
||||
python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买
|
||||
```
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `--charge-id N` | **5** | 买哪张票 |
|
||||
| `--list` | — | 只列票券与可购买清单,不做任何写操作 |
|
||||
| `--send` | 关 | ★ 真实发包 |
|
||||
| `--debug` | 关 | 日志里带完整收发报文 |
|
||||
| `--skip-entry` | 关 | 跳过登录后的「入场全量下载」 |
|
||||
| `--wait-before N` | **30** | 买票前等待秒数(真机 login→买票是 26 秒) |
|
||||
|
||||
### 三道安全检查
|
||||
|
||||
1. **买之前**先查 `GetGameChargeApi` —— 目标票**不在当前可购买清单里就直接退出**,
|
||||
连登录都不做(不浪费二维码)。
|
||||
依据 `source/Assembly-CSharp/Manager/TicketManager.cs:137-150`。
|
||||
2. **跑前查一次** `GetUserChargeApi`:**账号已经持有功能票时直接拒绝购买**
|
||||
(重复购买有封号风险)。想清掉手里的票用 `use_ticket.py`。
|
||||
3. **跑后再查一次**,确认票券状态符合预期。
|
||||
|
||||
### 报文形态
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": <uid>,
|
||||
"userChargelog": {
|
||||
"chargeId": 2, "price": 1,
|
||||
"purchaseDate": "2026-10-06 22:23:00.0",
|
||||
"playCount": 1268, "playerRating": 15534,
|
||||
"placeId": 1007, "regionId": 8, "clientId": "A63E01C3047"
|
||||
},
|
||||
"userCharge": {
|
||||
"chargeId": 2, "stock": 1,
|
||||
"purchaseDate": "2026-10-06 22:23:00.0",
|
||||
"validDate": "2027-01-04 04:00:00"
|
||||
},
|
||||
"loginDateTime": 1791325354
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 依据 |
|
||||
|---|---|
|
||||
| `price` | = `ticket.creditNum`(**不是** `GetGameChargeApi` 里的 money price)。`VOExtensions.cs:410` |
|
||||
| `stock` | **购买后**的库存(原值 + 1) |
|
||||
| `purchaseDate` | `TimeManager.DateTimeFormat` = `yyyy-MM-dd HH:mm:ss.f`,**带一位小数** |
|
||||
| `validDate` | `TicketManager.cs:562-565`:`Now.AddHours(-4).AddDays(90).ToString("yyyy-MM-dd 04:00:00")`,**不带小数** |
|
||||
|
||||
> ⚠️ 这两个日期格式不一样 —— `purchaseDate` 带 `.f` 而 `validDate` 不带。
|
||||
> 少写 `.0` 服务端直接 `returnCode=0`(踩过)。
|
||||
|
||||
---
|
||||
|
||||
## 三、消耗:`use_ticket.py`
|
||||
|
||||
```bash
|
||||
python use_ticket.py "<二维码>" --list # 只列当前持有的票
|
||||
python use_ticket.py "<二维码>" --ticket-id 2 # 预检:只看报文
|
||||
python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗
|
||||
```
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `--ticket-id N` | 第一张持有的 | 消耗哪一张 |
|
||||
| `--list` | — | 只列票券,不做任何写操作 |
|
||||
| `--send` | 关 | ★ 真实发包 |
|
||||
| `--music-id N` / `--level N` | `config.PLAY_MUSIC_ID` / `PLAY_MUSIC_LEVEL` | 「这一局」打的歌 |
|
||||
| `--skip-entry` | 关 | 跳过入场全量下载 |
|
||||
| `--debug` | 关 | 日志里带完整收发报文 |
|
||||
|
||||
### 原理
|
||||
|
||||
`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList`
|
||||
**格式完全相同**(`UserCharge.cs`:`chargeId` / `stock` / `purchaseDate` / `validDate`),
|
||||
所以只要把该节点里目标票的 `stock` 减 1,再让 playlog 带上
|
||||
`useTicketId` / `playCredit`,服务端就认为这张票被这一局用掉了。
|
||||
|
||||
**真机实证**(2026-10-06 22:23 那局正好用了 2 倍券):
|
||||
|
||||
```
|
||||
userChargeList: chargeId=2 stock 1 -> 0 ← 票被消耗
|
||||
userGamePlaylogList[0].useTicketId = 2 ← 用了哪张票
|
||||
userGamePlaylogList[0].playCredit = 2
|
||||
userPlaylogList[0].playCredit = 2
|
||||
userData.lastPlayCredit = 2
|
||||
(报文里**没有** isNewChargeList 这个节点)
|
||||
```
|
||||
|
||||
`playCredit` 的算法(`VOExtensions.cs:838-845`):
|
||||
|
||||
```csharp
|
||||
int num = Credit.GameCostEnoughPlay(); // 投币数,普通局 = 1
|
||||
int useTicketId = TicketManager.GetUseTicketId(playerId);
|
||||
int ticketCredit= TicketManager.GetTicketCredit(useTicketId); // = 票的 creditNum
|
||||
int playCredit = num + ticketCredit;
|
||||
```
|
||||
|
||||
2 倍券的 `creditNum = 1` → `playCredit = 1 + 1 = 2`,与抓包吻合。
|
||||
|
||||
### 实测记录(2026-10-07 00:24)
|
||||
|
||||
```
|
||||
[消耗前] chargeId=2 2倍チケット stock=1 ← 持有中
|
||||
选定要消耗的票:chargeId=2 creditNum=1 -> playCredit=1+1=2
|
||||
userChargeList 里 chargeId=2 的 stock:1 -> 0
|
||||
★ 上传成绩成功:returnCode=1
|
||||
落库回查:
|
||||
playCount 1269 -> 1269 (PLAY_USER_PLAY_COUNT_GAIN=0,不再 +1)
|
||||
currentPlayCount 197 -> 197
|
||||
lastPlayDate 2026-10-06 22:23:56 -> 2026-10-07 00:24:07 ← 落库成功
|
||||
musicId=143 level=4 playCount -> 11 (曲目次数不变)
|
||||
★ 票券已消耗:chargeId=2 不再持有 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、典型用法:票挂在手里,想再买一张
|
||||
|
||||
`buy_ticket.py` 检测到**已持票**会拒绝购买。想再买一张:
|
||||
|
||||
1. 用 `use_ticket.py --send` 把手里那张消耗掉,或
|
||||
2. 直接在机台上打一局并选用它
|
||||
|
||||
然后再跑 `buy_ticket.py`。
|
||||
|
||||
---
|
||||
|
||||
## 五、购买接口曾经的坑(摘要)
|
||||
|
||||
`UpsertUserChargelogApi` 曾经一直返回 `returnCode=0`,报文与真机**逐字节一致**也照样失败。
|
||||
|
||||
**根因:请求头多发了 `Accept` 与 `Connection`。**
|
||||
|
||||
真机(Unity Mono 的 `HttpWebRequest`)实际只发这几个头:
|
||||
|
||||
```
|
||||
number / Content-Type / User-Agent / charset / Mai-Encoding / Content-Encoding / Cookie
|
||||
```
|
||||
|
||||
删掉这两个头之后一次通过(`stock` 0 → 1,落库确认)。
|
||||
|
||||
> 完整排查过程(12 项已排除假设、账号对照实验、真机抓包对比)见
|
||||
> [购买功能票流程.md](购买功能票流程.md) 与 [购买功能票_存档与交接.md](购买功能票_存档与交接.md)。
|
||||
Reference in New Issue
Block a user