README 为了防冗余做了精简,被移出的详细内容写成文档放这里:
- 功能票使用说明.md 买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md reincarnate 完整流程、分批规则(避开 2 首)、
续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md 二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
7.0 KiB
账号转生说明(reincarnate.py)
本文是
README.md里转生部分的详细版。
一、它干了什么
把一个账号(源 A)的数据克隆到另一个账号(目标 B):
源账号 A:二维码 -> ParseQRCode -> UserLogin -> GetNeedUserData
或:直接读 data/<A>.json(离线复用)
↓ 节点数据(19 个节点)
目标账号 B:二维码 -> ParseQRCode -> UserPreview -> UserLogin
循环:
等待 -> UpsertUserAll(一包曲目)
-> UserLogout(type=1)
-> UserLogin(isContinue=true) 续关,拿新 loginId
收尾:最后一包登出后结束(不再续关)
二、用法
# 源数据来自二维码(会联网抓 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):
- 前一批 >= 4 → 从它借 1 首过来(前一批变 >=3,本批变 3)
- 借不到(前一批只有 3,或自己就是第一批)→ 把这个 2 拆成 1 + 1
★ 只借位是不够的:前一批是 3 时借完自己变 2,等于把 2 挪了个位置。
自检里有 1..299 首 × per∈{1,3,4} 的全量扫描,保证永不产生 2 首包。
四、★ 为什么要「续关登录」
UpsertUserAll 的 playlogId 必须等于本次登录的 loginId
(PacketUpsertUserAll.cs:31 / UserAllRequestVO)。
一局结束后这个会话就被消费掉了,要继续上传必须重新登录 —— 真机走的就是「续关」:
// 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:
public enum LogoutType { None=0, Logout=1, Cancel=2, Error=3, TestIn=4, Quit=5 }
真机抓包实证 UserLogoutApi 用的是 type=1:
{"userId":12570152,"accessCode":"","regionId":8,"placeId":1007,
"clientId":"A63E01C3047","loginDateTime":1791325354,"type":1,"delayLog":{...}}
源码侧的正常会话结束也是 type=1:
// 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。
八、⚠️ 注意事项
- 未实跑过。 建议先用
--plan看计划,再--limit 8小批量试,确认无误再全量。 - 会真实写入账号 B(不可撤销)。 不传
--send时只构造报文。 - 耗时:曲目多的话包数很多(每包最多 4 首),加上
--wait的等待, 几百首会跑很久。想快可以调小--wait。 - 二维码有效期 10 分钟,但转生过程中只用一次(登录时)—— 续关用的是 token,不需要新二维码。
- 基线快照只在第一次登录后抓一次,后续包沿用同一份做差分。
曲目节点由
extra_data整体覆盖,位串也单独算,不受影响。