v0版本完成:新增 4 篇详细文档(README 精简内容的落点)
README 为了防冗余做了精简,被移出的详细内容写成文档放这里:
- 功能票使用说明.md 买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md reincarnate 完整流程、分批规则(避开 2 首)、
续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md 二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
# 账号转生说明(`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` 整体覆盖,位串也单独算,不受影响。
|
||||
Reference in New Issue
Block a user