Files

500 lines
21 KiB
Markdown
Raw Permalink 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.
<p align="center">
<img src="icon/Astral.png" alt="Astral" width="220">
</p>
<h1 align="center">Astral</h1>
<p align="center">
<b>舞萌DX 的游戏助手</b><br>
宗旨:让舞萌DX的玩家能够更好地游玩游戏,获得快乐。
</p>
<p align="center">AI真的很好用你知道吗</p>
---
## 这是什么
Astral 是一个用 **Python 纯网络方式**与舞萌DX(SDGB 国服)游戏服务器交互的工具集。
它不修改游戏、不注入进程,只是把游戏客户端做的事用脚本重做一遍:
扫码登录 → 取数据 → 上传成绩 / 解锁歌曲 → 登出。
> 协议实现全部来自反编译源码与真机网络日志的双向验证,
> 每个关键常量在代码注释里都标了**源码行号**。
---
## 当前状态:v0 plus 附加功能
| 脚本 | 状态 |
|---|---|
| **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 |
| **`unlockmusic.py`** | ✅ **可用** —— 解锁(解禁)歌曲 / 难度(复用 `upsert.py` 整套流程) |
| **`buy_ticket.py`** | ✅ **可用** —— 购买功能票 |
| **`use_ticket.py`** | ✅ **可用** —— 消耗当前持有的功能票 |
| **`reincarnate.py`** | 🔧 已实现(**未实跑**)—— 账号数据迁移(转生) |
| **`netdelivery.py`** | ✅ **可用** —— 查 / 下 `.app` / `.opt` 更新包(只读) |
| `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** |
**文档**在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs),见文末[文档索引](#文档索引)。
---
## 一个教训:别把一件事分段交给 AI
v0 的开发过程中,我反复把同一个目标拆成小段丢给 AI,结果:
| 项目 | Token 消耗 |
|---|---|
| **v0 有效开发** | **473M+** |
| 含中途废弃的分支项目 | **1137.5M** |
**这 1137.5M 里有一大半是浪费掉的。**
真正的教训是:
> **不要把一个东西分段交给 AI,而是直接设定一个目标,然后让它自己跑完。**
分段交接会让 AI 反复丢失上下文、重复劳动、在已经解决的问题上打转 ——
而一个清晰的目标 + 足够长的执行窗口,反而更快也更省。
---
## 项目框架
```
Astral/
├── config.py 运行时可配置项(机台标识 / 歌曲数据 / 日志级别)★ 需自建
├── config.py.example 配置模板
├── icon/ Logo
├── upsert.py ★ 主脚本:扫码登录 → 上传成绩 → 登出
├── unlockmusic.py 主脚本:解锁(解禁)歌曲(复用 upsert.py 的 run())
├── buy_ticket.py 主脚本:购买功能票
├── use_ticket.py 主脚本:消耗当前持有的功能票
├── reincarnate.py 主脚本:账号数据迁移(转生)
├── netdelivery.py 主脚本:查 / 下 .app / .opt 更新包
├── selftest.py 零网络自检(284 项,判据 = 源码 + 真机日志)
│
├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本)
│ ├── __init__.py 统一导出 + 日志自动跟踪
│ ├── MaimaiNet.py 网络核心(AES / deflate / URL 混淆 / Cookie)
│ ├── LogSetup.py 日志(INFO / DEBUG 双模式)
│ ├── ChargeGuard.py 买票前置闸门(必须先跑过 GetUserCharge)
│ ├── UpsertUserAll.py 上传成绩 / 解锁歌曲(36 节点 / 110 字段 / 差分)
│ ├── GetNeedUserData.py 结算快照编排 + 落盘 data/<userId>.json
│ └── ... 40+ 个接口脚本
│
├── data/ 每账号一份用户数据快照(<userId>.json)★ 不入库
├── download/ netdelivery.py 下载的 .app / .opt ★ 不入库
└── logs/ 日志输出(latest.log + 日期.log)★ 不入库
```
**设计约定**:`api/` 里每个脚本 = 一个接口,对外暴露一个**能传参的函数**。
主脚本只负责编排,日志和协议细节都在 `api/` 内部。
**命名约定**:**文件名 = 函数名**,都按首字母大写的驼峰切分,一一对应:
| 服务端接口 | 脚本 | 函数 |
|---|---|---|
| `GetUserDataApi` | `api/GetUserData.py` | `GetUserData()` |
| `UpsertUserAllApi` | `api/UpsertUserAll.py` | `UpsertUserAll()` |
| `UserLoginApi` | `api/UserLogin.py` | `UserLogin()` |
服务端 API 名里带 `Get` 的,脚本名与函数名一律带 `Get`(不要模棱两可)。
### 分支说明
| 分支 | 内容 |
|---|---|
| **`mai`** | 主分支 —— 代码 |
| **`docs`** | 孤儿分支 —— 只有 `docs/`,开发文档与踩坑记录 |
两个分支**没有共同的提交历史**,各自独立。
> 🛠 **给开发者**:协议实现对照的反编译源码(`Assembly-CSharp` / `AMDaemon.NET` 等)
> 不入库,需要自己准备一份放在 `source/`;代码注释里的行号都指向它。
> 只是用工具的话**完全不需要**这个目录。
---
## 快速开始
### 1. 装依赖
```bash
pip install requests pycryptodome
```
### 2. ★ 先填 `config.py`
仓库里只有模板 `config.py.example`,先复制一份:
```bash
cp config.py.example config.py # Linux / macOS
copy config.py.example config.py # Windows
```
然后**必须**填好 **`config.py` 第二节「机台与店铺标识」**:
| 变量 | 填什么 |
|---|---|
| `CLIENT_ID` | Keychip 短值(15 位 Keychip 去掉连字符取前 11 位),形如 `A63E01ABCDE` |
| `PLACE_ID` | 你所在店铺 ID |
| `REGION_ID` | 你所在区域 ID |
| `PLACE_NAME` | 店铺名 |
| `REGION_NAME` | 区域名 |
| `KEYCHIP_ID` | **由上面两个值自动推导**,不要手改 |
> ⚠️ 填错的话,发出去的请求带的就是别人的机台身份,服务端会直接拒绝。
>
> `config.py` 已在 `.gitignore` 里 —— 它是你的私有配置,**不要提交**。
### 3. 跑自检
```bash
python selftest.py # 零网络自检,应该 284 项全绿(不依赖你的 config)
```
### 主脚本一览
| 脚本 | 干什么 | 副作用 | 需要二维码 |
|---|---|---|---|
| `upsert.py` | 上传一局成绩 | ★ 有 | ✅ |
| `unlockmusic.py` | 解锁(解禁)歌曲 / 难度 | ★ 有 | ✅ |
| `buy_ticket.py` | 购买功能票 | ★ 有 | ✅ |
| `use_ticket.py` | 消耗当前持有的功能票 | ★ 有 | ✅ |
| `reincarnate.py` | 账号数据迁移(转生) | ★ 有 | ✅(目标号;源号可选) |
| `netdelivery.py` | 查 / 下 `.app` / `.opt` 更新包 | 只读 | ❌ |
| `selftest.py` | 零网络自检 | 无 | ❌ |
> **所有有副作用的脚本默认都不发包**:不传 `--send` 时只构造报文(dry-run)。
> 想看报文而不产生任何后果,先跑一次不带 `--send` 的。
> 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**(不是按次数计量的)。
---
## upsert.py —— 上传一局成绩
```bash
# 预检:登录 + 抓快照 + 组包,不发包(零副作用,建议先跑这个)
python upsert.py "<二维码>"
# 真实上传一局成绩(★ 有副作用,不可撤销)
python upsert.py "<二维码>" --send
# 常用开关
python upsert.py "<二维码>" --send --debug # 日志里带完整收发报文
python upsert.py "<二维码>" --send --cooldown 0 # 跳过发包前 15 秒冷却
python upsert.py "<二维码>" --send --no-verify # 跳过上传后的落库回查
python unlockmusic.py "<二维码>" --send --music 11414 # 解锁歌曲 / 解禁难度
```
**换歌 / 换成绩只改 `config.py` 的 `PLAY_*`,不用动任何代码。**
### 额外塞节点:`--extra-data`
想手工覆盖某几个节点(比如本局拿到的新 KaleidxScope 门):
```bash
python upsert.py "<二维码>" --send --extra-data extra.json
```
键必须是 `upsertUserAll` 的 **36 个节点**之一,写错会报错并列出可用节点名。
**JSON 在扫码之前就校验**,写错直接退出,不浪费二维码。
> ⚠️ 在 PowerShell / CMD 里写内联 JSON 很容易被吞掉双引号,**建议一律用文件**。
### ★ `isNew*` 位串:0 / 1 / 2 的含义
每个数组节点都配一条 `isNew*` 位串,**一位对应数组里的一条**(顺序必须一致)。
依据 `source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:309-364` 的 `BuildListData()`:
| 位 | 含义 | 服务端行为 |
|---|---|---|
| `0` | **存量更新** —— 旧数据里**已有**同主键的条目 | UPDATE |
| `1` | **新增** —— 旧数据里**没有**同主键的条目 | INSERT |
| `2` | **无变化** —— 只有 `userFavoriteList` 用,其他节点不会出现 | 跳过 |
主键见 `api/UpsertUserAll.py` 的 `DIFF_PRIMARY_KEYS`
(如 `userKaleidxScopeList` 用 `gateId`、`userMusicDetailList` 用 `musicId`+`level`)。
> ⚠️ **对已存在的条目填 `1` 会 HTTP 500** —— 服务端会去 INSERT 一条重复记录。
> 真机抓包样本里 `isNewFavoriteList = "22212"`,可以直观看到 0/1/2 混用的样子。
> **不确定填什么?** 先看 `data/<你的userId>.json` 里对应的节点:
> 主键已存在 → `0`,不存在 → `1`。
---
## unlockmusic.py —— 解锁(解禁)歌曲
```bash
python unlockmusic.py "<二维码>" --music 11414 # 预检,不发包
python unlockmusic.py "<二维码>" --music 11414 --send # ★ 真实解锁
python unlockmusic.py "<二维码>" --music 1085,11414 --send # 多首,逗号分隔
python unlockmusic.py "<二维码>" --music 11414 --send --no-unlock-remaster
```
**它没有自己的协议实现** —— 整个流程直接复用 `upsert.py` 的 `run()`:
扫码 → 预览 → 登录 → `UpsertUserAll` → 落库回查 → 登出,一模一样。
唯一多出来的东西是「要解锁哪些歌」这个入参。
**解锁也不是单独的接口** —— 它是往 `userItemList` 里补几条 `UserItem`:
| itemKind | 解锁什么 |
|---|---|
| `5` | 歌曲本体(`MusicUnlockList`) |
| `6` | Master 难度(`MusicUnlockMasterList`) |
| `7` | Re:Master 难度(`MusicUnlockReMasterList`) |
条目形如 `{"itemKind":5,"itemId":1085,"stock":1,"isValid":true}`。
- **标准谱(`musicId < 10000`)的 Master / Re:Master 天生解锁**,只写 `kind5`;
只有 DX 谱(`>= 10000`)才需要写 `kind6` / `kind7`。
- 服务器上**已存在的条目会自动跳过** —— 重复插入会让服务端直接 HTTP 500。
- Re:Master 还受活动开关约束,那是机台侧的事,脚本改不了;写了也可能进游戏仍是灰的。
| 参数 | 说明 |
|---|---|
| `--music ID[,ID...]` | 要解锁的 musicId,可重复、可逗号分隔(`--unlock-music` 也认) |
| `--send` | 真正发包(不加就是预检,零副作用) |
| `--no-unlock-master` | 不解禁 Master |
| `--no-unlock-remaster` | 不解禁 Re:Master |
| `--cooldown N` / `--no-verify` / `--debug` | 和 `upsert.py` 完全一样 |
> 依据反编译源码 `VOExtensions.cs:981-1008`(四个解锁列表并进 `userItemList`)
> 与 `NotesListManager.cs:415/455`(`if (id < 10000) return true;`)。
> 这部分**没有真机抓包可对照**(机台侧的解锁不上报),是按源码实现的。
---
## buy_ticket.py —— 购买功能票
```bash
python buy_ticket.py "<二维码>" --list # 登录后只列当前票券,不购买
python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文
python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买
```
| chargeId | 票 | 上报的 `price` |
|---|---|---|
| 2 | 2倍チケット | 1 |
| 3 | 3倍チケット | 2 |
| 5 | 5倍チケット | 4 |
> 默认 `--charge-id 5`。**不同店铺上架的票不一样**,买之前先 `--list`。
### 它做的安全检查
1. 跑前查 `GetUserChargeApi` —— **账号已持有功能票时直接拒绝购买**(重复购买有封号风险)
2. 买票前 **15 秒等待**(贴近真机节奏)
3. 跑后再查一次确认状态
> **票源只看 `GetUserChargeApi`**(账号自己的票券表),不再看 `GetGameChargeApi`
> (那是机台本地的上架清单,跟账号状态无关)。
>
> `UpsertUserChargelog` 内部还有两道硬闸门:
> **必须先跑过 `GetUserCharge`**(`api/ChargeGuard.py` 记账),
> 以及**同一账号同一进程只允许买一次票** —— 都拦在发包之前。
---
## use_ticket.py —— 消耗当前持有的功能票
```bash
python use_ticket.py "<二维码>" --list # 只列当前持有的票
python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗
```
**原理**:`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList` **格式完全相同**,
所以把该节点里目标票的 `stock` 减 1,再让 playlog 带上 `useTicketId` / `playCredit`,
服务端就认为这张票被这一局用掉了。
> **典型用途**:账号手里挂着票时 `buy_ticket.py` 会拒绝再买;
> 想再买一张就先用这个把它用掉。
详细原理与报文依据见 [功能票使用说明](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/功能票使用说明.md)。
---
## reincarnate.py —— 账号转生
把一个账号(源 A)的数据克隆到另一个账号(目标 B)。
```bash
# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/<A>.json)
python reincarnate.py --dst-qr "<B的二维码>" --src-qr "<A的二维码>"
# 源数据来自已保存的 JSON(离线,推荐)
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --plan
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --send
```
**它干了什么**:
1. 取源账号 A 的 19 个节点数据(联网抓或读 `data/<A>.json`)
2. 用目标账号 B 的二维码登录
3. 把 A 的 `userMusicDetailList` **分批**上传到 B —— 每包最多 4 首,
并自动避开**被服务端禁止的 2 首包**(`[4,4,2]` → `[4,3,3]`)
4. 每包之间:**等待 → 上传 → 登出(type=1)→ 续关登录**,循环到传完
5. 每包的 playlog 从曲目记录**反推合成**(`achievement` / `deluxscore` / `scoreRank` …)
| 参数 | 默认 | 说明 |
|---|---|---|
| `--plan` | — | 只列分批计划,不发包(**建议先跑这个**) |
| `--limit N` | 0(全部) | 只迁前 N 首(试跑用) |
| `--per-packet N` | 4 | 每包曲目数,**只能是 1 / 3 / 4** |
| `--wait N` | 5.0 | 每次上传前的等待秒数 |
| `--clone-identity` | 关 | 连 `userName` / `accessCode` 一起克隆(默认保留 B 自己的) |
> ⚠️ **未实跑过。** 建议先 `--plan`,再 `--limit 8` 小批量试。
完整流程、分批规则、续关与登出类型的依据见 [账号转生说明](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/账号转生说明.md)。
---
## netdelivery.py —— 查 / 下 `.app` / `.opt` 更新包
```bash
python netdelivery.py # 用 config.py 里的机台查,然后**多选下载**
python netdelivery.py A63E01ABCDE # 指定 Keychip
python netdelivery.py --list # 只列清单 + 直链,不下载
python netdelivery.py --all # 全下(不交互)
python netdelivery.py --only 1,3 # 只下第 1、3 项(不交互)
python netdelivery.py --ver 1.55 # 指定上报版本(默认 config.MAI_ENCODING)
python netdelivery.py --out D:\SDGB2026\update
```
交互选择支持:逗号/空格分隔、区间(`1-3`)、`a`=全选、`n`=全不选、`q`=退出。
下载带**断点续传**与进度/速率显示,默认存到 `download/`(不入库)。
> 这是**只读**操作,不写任何账号数据。
> 需要同目录下的 `authv1.py` 提供 ALL.Net 协议层。
---
## 主脚本怎么写
**核心原则:不要为每个 api 调用写日志。** 日志已经在 `api/` 里了。
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from api import (setup_logging, teardown_logging,
ParseQRCode, GetUserPreview, UserLogin, UserLogout, LogoutType,
UpsertUserAll)
from config import REGION_ID, PLACE_ID
def main() -> int:
setup_logging("我的脚本") # ← 只需要这一句
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
try:
user_id, token = ParseQRCode(sys.argv[1])
session.update(user_id=user_id, token=token)
GetUserPreview(user_id, token) # 顶号保护
login = UserLogin(user_id, token,
region_id=REGION_ID, place_id=PLACE_ID)
session.update(login_dt=login.loginDateTime, logged_in=True)
# ★ 一个函数跑完整条结算链路
UpsertUserAll(user_id, login.loginId,
login_date_time=login.loginDateTime,
auth_date_time=login.dateTime,
token=token,
dry_run=False, confirm=True)
return 0
finally:
if session["logged_in"]: # ★ 一定要保证登出
UserLogout(session["user_id"], session["token"], session["login_dt"],
logout_type=LogoutType.Logout,
region_id=REGION_ID, place_id=PLACE_ID)
teardown_logging()
if __name__ == "__main__":
sys.exit(main())
```
三条铁律:
1. **不要写日志** —— 每个 api 函数会自动打印 `=== 函数名 开始运行 ===` / `[运行结果]`
2. **一定要登出** —— 账号卡在 `isLogin=True` 时下次登录会 `returnCode=100`
3. **写接口要显式开闸门** —— 只写 `dry_run=False` 不够,必须同时给 `confirm=True`
完整指南见 [主脚本编写指南](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/主脚本编写指南.md)。
---
## 已完成 / 将来
**已完成(v0)**
- ✅ 扫码登录 / 登出 / 心跳,完整会话链路
- ✅ 上传一局成绩(36 节点 / 110 字段 / 差分 / 落库回查)
- ✅ 购买功能票
- ✅ 消耗持有的功能票
- ✅ 账号数据快照落盘(`data/<userId>.json`)
- ✅ 账号数据迁移(转生)
- ✅ 查 / 下 `.app` / `.opt` 更新包
- ✅ 解锁歌曲 / 解禁 Master 与 Re:Master 难度
- ✅ 零网络自检 284 项
- ✅ MelonLoader 抓包 Mod(`gamehook/`)
**将来可能会加**
- **网页控制** —— 用浏览器代替命令行操作
- 更多游戏功能(任务 / 地图 / 收藏夹 / 排行榜……)
- 更完整的真机验证覆盖
- `reincarnate.py` 的实跑验证
---
## 文档索引
全部在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs):
| 文档 | 内容 |
|---|---|
| [项目总览](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/项目总览.md) | 开发向主文档,该放哪 / 该怎么写 |
| [主脚本编写指南](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/主脚本编写指南.md) | 写新主脚本的规范 |
| [上传成绩开发文档](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/上传成绩开发文档.md) | `UpsertUserAll` 的 36 节点 / 110 字段 |
| [UpsertUserAll踩坑清单](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/UpsertUserAll踩坑清单.md) | 28 条踩坑记录 |
| [功能票使用说明](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/功能票使用说明.md) | 买票 / 用票的完整原理与报文依据 |
| [账号转生说明](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/账号转生说明.md) | `reincarnate.py` 完整流程 |
| [netdelivery说明](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/netdelivery说明.md) | DLI 协议与运行原理 |
| [常见坑与注意事项](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/常见坑与注意事项.md) | 二维码 / 账号状态 / 副作用 / 日志 / 网络 |
| [购买功能票流程](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/购买功能票流程.md) | 买票接口的完整排查过程 |
| [购买功能票_存档与交接](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/购买功能票_存档与交接.md) | 排查存档 |
| [真机Hook对照报告](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/真机Hook对照报告.md) | 抓包 Mod 对照 |
| [实跑验证记录](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/实跑验证记录.md) | 每次实跑的结果 |
| [舞萌DX_扫码登录协议技术规范](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/舞萌DX_扫码登录协议技术规范.md) | aimedb 扫码协议 |
| [日志与主脚本指南](https://git.fragrance.moe/Txiaochen/Astral/src/branch/docs/docs/日志与主脚本指南.md) | 日志系统说明 |
---
## 特别鸣谢
- **志成🍥** —— 提供脱壳 DLL
- [**WorkBuddy AI**](https://www.workbuddy.ai/)—— 提供 AI 模型
- **DeepSeek V4.1 Flash** —— 主要代码编写
- **GPT-Image-2** —— Logo 设计
- [**NVIDIA NIM**](https://build.nvidia.com/models)、
[**AMD**](https://developer.amd.com.cn/radeon/tokenfactory)、
[**Kilo**](https://kilo.ai/)—— 提供 AI 模型
- [**eaquira**](https://git.fragrance.moe/Fragrance/eaquira)—— 思路启发
- **神秘群U** —— 私有科技的 Web 启发(暂无)
- **香水所有群友** —— 和谐帮助
*(还有什么以后补充)*
---
## 免责声明
本项目仅供**学习与协议研究**使用。使用本工具产生的任何后果由使用者自行承担。
请遵守游戏服务条款,不要用于破坏游戏公平性。