# 账号转生说明(`reincarnate.py`) > 本文是 [`README.md`](https://git.fragrance.moe/Txiaochen/wahappy) 里转生部分的**详细版**。 --- ## 一、它干了什么 把一个账号(源 **A**)的数据克隆到另一个账号(目标 **B**): ``` 源账号 A:二维码 -> ParseQRCode -> UserLogin -> GetNeedUserData 或:直接读 data/.json(离线复用) ↓ 节点数据(19 个节点) 目标账号 B:二维码 -> ParseQRCode -> UserPreview -> UserLogin 循环: 等待 -> UpsertUserAll(一包曲目) -> UserLogout(type=1) -> UserLogin(isContinue=true) 续关,拿新 loginId 收尾:最后一包登出后结束(不再续关) ``` --- ## 二、用法 ```bash # 源数据来自二维码(会联网抓 A 的数据,并落盘到 data/.json) python reincarnate.py --dst-qr --src-qr # 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码) python reincarnate.py --dst-qr --src-json data/12345678.json # ★ 真实上传 python reincarnate.py --dst-qr --src-json data/12345678.json --send # 只看计划,不发包(★ 建议先跑这个) python reincarnate.py --dst-qr --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` 整体覆盖,位串也单独算,不受影响。