Files
Astral/docs/账号转生说明.md
T
Txiaochen 3e30dffb31 v0版本完成:新增 4 篇详细文档(README 精简内容的落点)
README 为了防冗余做了精简,被移出的详细内容写成文档放这里:

- 功能票使用说明.md      买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md        reincarnate 完整流程、分批规则(避开 2 首)、
                          续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md    二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
                          不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md     DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
2026-10-07 11:23:47 +08:00

187 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 账号转生说明(`reincarnate.py`)
> 本文是 [`README.md`](https://git.fragrance.moe/Txiaochen/wahappy) 里转生部分的**详细版**。
---
## 一、它干了什么
把一个账号(源 **A**)的数据克隆到另一个账号(目标 **B**):
```
源账号 A:二维码 -> ParseQRCode -> UserLogin -> GetNeedUserData
或:直接读 data/<A>.json(离线复用)
↓ 节点数据(19 个节点)
目标账号 B:二维码 -> ParseQRCode -> UserPreview -> UserLogin
循环:
等待 -> UpsertUserAll(一包曲目)
-> UserLogout(type=1)
-> UserLogin(isContinue=true) 续关,拿新 loginId
收尾:最后一包登出后结束(不再续关)
```
---
## 二、用法
```bash
# 源数据来自二维码(会联网抓 A 的数据,并落盘到 data/<A>.json)
python reincarnate.py --dst-qr <B的二维码> --src-qr <A的二维码>
# 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码)
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json
# ★ 真实上传
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --send
# 只看计划,不发包(★ 建议先跑这个)
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --plan
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `--dst-qr` | 必填 | 目标账号 B 的二维码 |
| `--src-qr` / `--src-json` | 二选一 | 源数据来源 |
| `--plan` | — | 只列分批计划,不发包 |
| `--send` | 关 | ★ 真实发包 |
| `--limit N` | 0(全部) | 只迁前 N 首(试跑用) |
| `--per-packet N` | 4 | 每包曲目数,**只能是 1 / 3 / 4** |
| `--wait N` | 5.0 | 每次上传前的等待秒数(0 = 不等) |
| `--clone-identity` | 关 | 连 `userName` / `accessCode` 一起克隆(默认**保留 B 自己的**) |
| `--no-continue` | 关 | 不续关,只发一包(调试用) |
| `--debug` | 关 | 日志里带完整收发报文 |
---
## 三、★ 每包几首:1 / 3 / 4 可以,**恰好 2 首会被服务端拒绝**
真机一局(一个 credit)最多打 4 首,所以一条 `UpsertUserAll` 报文里的
`userPlaylogList` / `userGamePlaylogList` **最多 4 条**。
但实测:**每包恰好 2 首会被服务端拒绝**(1 / 3 / 4 首都正常)。
所以本脚本的分批器会**主动避开 2 首包**:
| 总量 | `--per-packet` | 原始切分 | 修正后 |
|---|---|---|---|
| 10 首 | 4 | `[4,4,2]` | `[4,3,3]` |
| 6 首 | 4 | `[4,2]` | `[3,3]` |
| 2 首 | 4 | `[2]` | `[1,1]` |
| 5 首 | 3 | `[3,2]` | `[3,1,1]` |
| 8 首 | 3 | `[3,3,2]` | `[3,3,1,1]` |
| 5 首 | 4 | `[4,1]` | 本来就合法,不动 |
修正规则(`reincarnate.split_batches`):
1. 前一批 **>= 4** → 从它借 1 首过来(前一批变 >=3,本批变 3)
2. 借不到(前一批只有 3,或自己就是第一批)→ 把这个 2 **拆成 1 + 1**
> ★ 只借位是不够的:前一批是 3 时借完自己变 2,等于把 2 挪了个位置。
自检里有 `1..299 首 × per∈{1,3,4}` 的全量扫描,保证永不产生 2 首包。
---
## 四、★ 为什么要「续关登录」
`UpsertUserAll` 的 `playlogId` 必须等于**本次登录**的 `loginId`
(`PacketUpsertUserAll.cs:31` / `UserAllRequestVO`)。
一局结束后这个会话就被消费掉了,要继续上传必须重新登录 —— 真机走的就是「续关」:
```csharp
// source/Assembly-CSharp/Process/ContinueProcess.cs:403
new PacketUserLogin(userID, accessCode, isContinue: true, GameManager.IsEventMode ? 1 : 0, ...)
```
对应本工程:`UserLogin(user_id, token, is_continue=True)`
—— 和普通登录的唯一区别就是 `UserLoginRequestVO.isContinue = true`,
会返回**新的 `loginId` / `loginDateTime`**。
---
## 五、★ 登出必须用 type=1(Logout),不要用 4(TestIn)
`Net.VO.Mai2/LogoutType.cs`:
```csharp
public enum LogoutType { None=0, Logout=1, Cancel=2, Error=3, TestIn=4, Quit=5 }
```
**真机抓包实证** `UserLogoutApi` 用的是 **type=1**:
```json
{"userId":12570152,"accessCode":"","regionId":8,"placeId":1007,
"clientId":"A63E01C3047","loginDateTime":1791325354,"type":1,"delayLog":{...}}
```
源码侧的正常会话结束也是 type=1:
```csharp
// source/Assembly-CSharp/Process.UserDataNet.State.UserDataULState/StateULUserLogout.cs:24
PacketHelper.StartPacket(new PacketUserLogout(_index, _userId, LogoutType.Logout, ...));
```
**续关场景下用 4(TestIn)会出问题** —— 那是「测试登入 / 顶号」的语义
(`GameMain.cs:104` 退出游戏时才用)。
`UserLogout` 对 type=1 会自动带上完整 `delayLog`
(`PacketUserLogout.cs:21`:`logoutType == LogoutType.Logout` 时才收集会话统计)。
---
## 六、playlog 从哪来
`GetNeedUserData` 抓不到 `userPlaylogList`(那是逐局记录,没有对应的 Get 接口),
所以本脚本**从曲目记录反推合成**:
| `UserMusicDetail`(9 字段) | → | `UserPlaylog`(110 字段) |
|---|---|---|
| `musicId` / `level` | | `musicId` / `level` |
| `achievement` | | `achievement` |
| `deluxscoreMax` | | `deluxscore` |
| `scoreRank` | | `scoreRank` / `isClear` |
| `comboStatus` / `syncStatus` | | 同名 |
| 其余 | | `config.PLAY_*` 默认值 |
`UserMusicDetail` 的全部字段(`Net.VO.Mai2/UserMusicDetail.cs`):
`musicId` / `level` / `playCount` / `achievement` / `comboStatus` /
`syncStatus` / `deluxscoreMax` / `scoreRank` / `extNum1`。
---
## 七、迁移范围
默认迁**全部节点**(整账号克隆):
- **第 1 包**带全部非曲目节点(`userData` / `userExtend` / `userOption` /
`userCharacterList` / `userMapList` / …)
- **之后每包**只带这一批曲目 + 合成的 `userGamePlaylogList`
- 位串 `isNew*` 按 **B 的基线**逐条算(B 没有 → `"1"`,已有 → `"0"`),
主键表见 `api/upsert_user_all.py` 的 `DIFF_PRIMARY_KEYS`
**不迁的节点**(`SKIP_NODES`):
| 节点 | 原因 |
|---|---|
| `userMusicDetailList` | 单独分批 |
| `userGamePlaylogList` | 本脚本按曲目合成 |
| `user2pPlaylog` | 2P 记录,迁移无意义 |
| `userGetPointList` | 里程/奖励,迁过去会凭空发点 |
| `userTradeItemList` | 无 Get 来源 |
| `userFriendSeasonRankingList` | 无 Get 来源 |
**默认不克隆 `userName` / `accessCode`**(避免把 B 改名);要连身份一起搬就加
`--clone-identity`。
---
## 八、⚠️ 注意事项
1. **未实跑过。** 建议先用 `--plan` 看计划,再 `--limit 8` 小批量试,确认无误再全量。
2. **会真实写入账号 B(不可撤销)。** 不传 `--send` 时只构造报文。
3. **耗时**:曲目多的话包数很多(每包最多 4 首),加上 `--wait` 的等待,
几百首会跑很久。想快可以调小 `--wait`。
4. **二维码有效期 10 分钟**,但转生过程中只用一次(登录时)——
续关用的是 token,不需要新二维码。
5. 基线快照只在**第一次登录后抓一次**,后续包沿用同一份做差分。
曲目节点由 `extra_data` 整体覆盖,位串也单独算,不受影响。