孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
# 舞萌DX 纯网络脚本工程 · 项目总览
|
||
|
||
> 项目名 **WaHappy** —— 舞萌DX 的游戏助手。
|
||
> 面向使用者的说明见根目录 [`README.md`](../README.md);
|
||
> 本文件是**开发向的主文档**,开工前先读它;遇到「该放哪、该怎么写」的问题以本文件为准。
|
||
|
||
---
|
||
|
||
## 一、这个工程是什么
|
||
|
||
用 **Python 脚本**,通过**纯网络(纯 HTTP)** 形式直接与舞萌DX(SDGB 国服)的**游戏服务器**交互,
|
||
复刻游戏客户端的协议行为。
|
||
|
||
| 对象 | 是什么 | 对应反编译源码 |
|
||
|---|---|---|
|
||
| **游戏服务器** | 玩家数据读写:扫码登录、取数、上传成绩、登出 | `source/Assembly-CSharp/` |
|
||
| **纯网络形式** | 脚本**直接收发 HTTP 报文**(AES 加密 JSON),不依赖运行中的游戏客户端 | `source/Assembly-CSharp/Net/` |
|
||
| **扫码登录** | 微信二维码换 userId + token,走 aimedb | `source/ChimeLib.NET/` |
|
||
|
||
---
|
||
|
||
## 二、目录结构
|
||
|
||
```
|
||
wahappy-v0-goal/
|
||
├── config.py ★ 运行时可配置项(密钥/设备/区域/歌曲数据/日志级别)
|
||
├── upsert.py ★ 主脚本范例:扫码登录 → 上传成绩 → 登出
|
||
├── buy_ticket.py ★ 主脚本范例:扫码登录 → 购买功能票 → 登出
|
||
├── selftest.py 零网络自检(220 项,判据=源码+真机日志+真机抓包)
|
||
│
|
||
├── api/ ★ 所有与服务器交互的脚本(一个 api 一个脚本)
|
||
│ ├── __init__.py 统一导出 + 日志跟踪登记
|
||
│ ├── log_setup.py 统一日志(INFO / DEBUG 双模式)
|
||
│ ├── maimai_net.py 网络核心(AES / deflate / URL 混淆 / Cookie)
|
||
│ ├── user_getter_common.py 取数脚本公共底座
|
||
│ ├── chime.py 扫码登录
|
||
│ ├── upsert_user_all.py ★ 上传成绩
|
||
│ └── ... 其余见第六节
|
||
│
|
||
├── source/ ★ 反编译源码 + 网络日志,只读查证用
|
||
├── docs/ ★ 文档
|
||
└── logs/ ★ 日志输出(latest.log + 日期.log)
|
||
```
|
||
|
||
---
|
||
|
||
## 三、api/ 脚本契约
|
||
|
||
`api/` 里的每个脚本是一个**功能模块**,对外暴露**函数**;函数**能接收参数**,
|
||
内部完成「组包 → 发包 → 解析 → 返回结果」的全过程。
|
||
|
||
```python
|
||
def 某Api函数(必填参数, *, 可选参数...) -> 结果:
|
||
"""一句话说明(会出现在日志的 [接口说明] 里)"""
|
||
...
|
||
```
|
||
|
||
参数形态规律:
|
||
|
||
- **业务数据**(user_id / token / 二维码 / playlog_id)→ **位置参数**
|
||
- **配置项**(client_id / region_id / timeout_ms)→ **关键字参数**,可省略走默认值
|
||
|
||
---
|
||
|
||
## 四、日志
|
||
|
||
### 4.1 两种模式
|
||
|
||
| 模式 | 输出内容 |
|
||
|---|---|
|
||
| **INFO**(默认) | 只展示 api 脚本的**运行情况和结果** |
|
||
| **DEBUG** | 在 INFO 基础上,额外展示**向 api 发送的数据**和**api 返回的数据** |
|
||
|
||
切换只改 `config.py` 一行:
|
||
|
||
```python
|
||
LOG_LEVEL = "info" # 只看流程
|
||
LOG_LEVEL = "debug" # 连收发数据一起看
|
||
```
|
||
|
||
### 4.2 运行提示
|
||
|
||
每个 api 函数工作时会提示开始,结束时提示结束:
|
||
|
||
```
|
||
=== ParseQRCode 开始运行 ===
|
||
[接口说明] 解析二维码并换取 userId 与 token。
|
||
[位置参数] 'SGWCMAID2610022219463A297AF4CA...'
|
||
[运行结果] 成功 tuple(2 项)(耗时 412 ms)
|
||
=== ParseQRCode 运行结束 ===
|
||
```
|
||
|
||
DEBUG 模式额外输出:
|
||
|
||
```
|
||
-> 请求 GetGameSettingApi
|
||
URL : https://maimai-gm.wahlap.com:42081/Maimai2Servlet/mE2s3Jhd/83a5...
|
||
请求头 : {"Content-Type":"application/json","Mai-Encoding":"1.55"}
|
||
请求体 :
|
||
{
|
||
"placeId": 1000,
|
||
"clientId": "TESTCLIENT1"
|
||
}
|
||
|
||
<- 响应 GetGameSettingApi
|
||
状态码 : 200
|
||
耗时 : 312 ms
|
||
响应体 :
|
||
{
|
||
"isAouAccession": false,
|
||
"gameSetting": { ... }
|
||
}
|
||
```
|
||
|
||
### 4.3 日志文件(两种形式同时存在、同时写入)
|
||
|
||
| 文件 | 模式 | 说明 |
|
||
|---|---|---|
|
||
| `logs/latest.log` | **覆盖**(`w`) | 只包含**当次**主脚本运行产出的日志,下次运行被覆盖 |
|
||
| `logs/YYYY-MM-DD.log` | **追加**(`a`) | 存放**当日**产生的所有日志 |
|
||
|
||
每行都带 `[YYYY-MM-DD HH:MM:SS]` 时间戳。
|
||
|
||
### 4.4 主脚本怎么写
|
||
|
||
```python
|
||
from api import setup_logging, teardown_logging, ParseQRCode, UserLogin
|
||
|
||
def main():
|
||
setup_logging("我的主脚本") # 开头一句;api 调用从此自动记录
|
||
try:
|
||
user_id, token = ParseQRCode(qr)
|
||
login = UserLogin(user_id, token)
|
||
finally:
|
||
teardown_logging() # 结尾一句(不写也会在退出时自动收尾)
|
||
```
|
||
|
||
**不需要**写装饰器,**不需要**自己埋点 —— `api/__init__.py` 在导入时就统一套好了日志外壳。
|
||
|
||
---
|
||
|
||
## 五、协议实现要点(全部有源码依据)
|
||
|
||
| 环节 | 依据 | 要点 |
|
||
|---|---|---|
|
||
| **URL 生成** | `Packet.cs:485` + `NetQuery.cs:14` | BASE_URL + md5(接口名 + "MaimaiChn" + "8bF76dE9") |
|
||
| **请求封装** | `NetHttpClient.cs:262-300` | JSON → 原始 deflate → 0x78 0x9C + Adler32(大端) → AES-256-CBC |
|
||
| **响应解封装** | `NetHttpClient.cs:355-380` | AES 解密 → 去 2 字节头 / 4 字节尾 → deflate → JSON |
|
||
| **AES 参数** | `CipherAES.cs:11-12` | Key 32B / IV 16B / CBC / PKCS7 |
|
||
| **请求头** | `NetHttpClient.cs:174-190` | Mai-Encoding: 1.55、Content-Encoding: deflate、UA = 混淆名#userId |
|
||
| **number 头** | `Packet.cs:187-189` + `PingChecker.cs:227` | = Ping 心跳的往返毫秒(`GameManager.PingMsec`),**只由 Ping 刷新**;除 Ping 外每个接口都带 |
|
||
| **Cookie** | `Packet.cs:190-193,228-234` | ★ **UserLoginApi 响应会下发 `Set-Cookie: JSESSIONID=...`**,之后每个带 userId 的请求都会带上;`requests.Session` 自动处理,但**必须复用同一个 client**(`get_default_client()`) |
|
||
| **playSpecial** | `VOExtensions.cs:846-855` + `GameManager.cs:332-378` | `位反转32(Random(1,1037933)*2069 + CalcSpecialNum)`;标准 ACA 机台 `CalcSpecialNum = 1024 = 0x400` |
|
||
| **扫码签名** | 技术规范 §4 | SHA256(chipID + timestamp + KEY) 大写;**不带 Content-Type** |
|
||
| **loginDateTime** | `PacketUserLogin.cs:29` | 「本地墙上时间当 UTC」= 标准 Unix 秒 + 时区偏移 |
|
||
|
||
### ★ 一个必须注意的坑:Accept-Encoding 必须为空
|
||
|
||
`requests` 默认发 `Accept-Encoding: gzip, deflate`。本协议响应体是 AES 密文(二进制),
|
||
服务端若据此做内容编码会返回**空 body**。C# 端不需要是因为 `HttpWebRequest`
|
||
默认 `AutomaticDecompression = None`。所以 `maimai_net.py` 固定置空该头。
|
||
|
||
---
|
||
|
||
## 六、API 清单
|
||
|
||
### 6.1 会话链路(6 个)
|
||
|
||
| 脚本 | 函数 | 对应接口 |
|
||
|---|---|---|
|
||
| `chime.py` | `ParseQRCode(qr, *, chip_id)` | aimedb /wc_aime/api/get_data |
|
||
| `gamesettings.py` | `GetGameSettings(*, place_id, client_id)` | GetGameSettingApi |
|
||
| `ping.py` | `Ping(*, client_id)` | Ping |
|
||
| `userpreview.py` | `UserPreview(user_id, token, *, ...)` | GetUserPreviewApi |
|
||
| `userlogin.py` | `UserLogin(user_id, token, *, region_id, place_id)` | UserLoginApi |
|
||
| `userlogout.py` | `UserLogout(user_id, token, login_dt, *, logout_type)` | UserLogoutApi |
|
||
|
||
### 6.2 上传成绩(2 个)
|
||
|
||
| 脚本 | 函数 | 说明 |
|
||
|---|---|---|
|
||
| `upsert_user_all.py` | `UpsertUserAll(user_id, playlog_id, *, login_date_time, auth_date_time, token, snapshot, extra_data, play, ...)` | ★ **一个函数跑完整条结算链路**:抓快照 → 组包 → 前置接口 → 自检 → 60 秒冷却 → 发包 → 落库回查。唯一有副作用的接口 |
|
||
| `upsert_user_all.py` | `derive_play_characters(snapshot)` | 从服务器快照推「本局 5 只角色 + 游玩前等级」,免得手填的 `config.PLAY_CHARACTERS` 过期 |
|
||
| `upsert_user_all.py` | `load_extra_data(extra)` / `merge_extra_data(user_all, extra)` | 把外部 JSON(dict / JSON 串 / 文件路径)自动匹配插入 36 节点 |
|
||
| `upsert_user_all.py` | `verify_upsert_landing(user_id, snapshot, play)` | 发包后重新拉服务器数据,确认真的落库 |
|
||
| `upsert_snapshot.py` | `fetch_upsert_snapshot(user_id, ...)` | 一键抓结算基线快照(`UpsertUserAll` 内部会按需自动调) |
|
||
| `upsert_user_chargelog.py` | `UpsertUserChargelog(user_id, charge_id, login_date_time, ...)` | ★ **购买功能票**(UpsertUserChargelogApi):自动取当前票券/档案、算 validDate 与 price、闸门、发包 |
|
||
| `upsert_user_chargelog.py` | `calc_valid_date(now)` / `ticket_price(id)` / `ticket_name(id)` | 有效期算法(-4h+90d)、票的 creditNum、票名 |
|
||
|
||
### 6.3 取数(一个接口一个脚本)
|
||
|
||
| 脚本 | 函数 | 对应接口 |
|
||
|---|---|---|
|
||
| `userdata.py` | UserData | GetUserDataApi |
|
||
| `user_extend.py` | UserExtend | GetUserExtendApi |
|
||
| `user_option.py` | UserOption | GetUserOptionApi |
|
||
| `user_character.py` | UserCharacter | GetUserCharacterApi |
|
||
| `user_ghost.py` | UserGhost | GetUserGhostApi |
|
||
| `user_map.py` | UserMap / UserMapAll | GetUserMapApi |
|
||
| `user_login_bonus.py` | UserLoginBonus / UserLoginBonusAll | GetUserLoginBonusApi |
|
||
| `user_rating.py` | UserRating | GetUserRatingApi |
|
||
| `user_item.py` | UserItem / UserItemAll | GetUserItemApi |
|
||
| `user_music.py` | UserMusicDetail / UserMusicDetailAll | GetUserMusicApi |
|
||
| `user_course.py` | UserCourse / UserCourseAll | GetUserCourseApi |
|
||
| `user_charge.py` | UserCharge | GetUserChargeApi |
|
||
| `user_favorite.py` | UserFavorite / UserFavoriteAll | GetUserFavoriteApi |
|
||
| `user_activity.py` | UserActivity | GetUserActivityApi |
|
||
| `user_mission.py` | UserMissionData | GetUserMissionDataApi |
|
||
| `user_intimate.py` | UserIntimate | GetUserIntimateApi |
|
||
| `user_shop_stock.py` | UserShopStock | GetUserShopStockApi |
|
||
| `user_favorite_item.py` | UserFavoriteItem / UserFavoriteItemAll | GetUserFavoriteItemApi |
|
||
| `user_rival.py` | UserRivalData / UserRivalMusic(All) | GetUserRivalDataApi / GetUserRivalMusicApi |
|
||
| `user_friend_bonus.py` | UserFriendBonus | GetUserFriendBonusApi |
|
||
| `user_kaleidx_scope.py` | UserKaleidxScope / GameKaleidxScope | GetUserKaleidxScopeApi / GetGameKaleidxScopeApi |
|
||
| `user_region.py` | UserRegion | GetUserRegionApi |
|
||
| `user_recommend_music.py` | UserRecommendRateMusic / UserRecommendSelectMusic | GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi |
|
||
| `user_new_item.py` | UserNewItemList | GetUserNewItemListApi |
|
||
| `game_ranking.py` | GetGameRanking | GetGameRankingApi |
|
||
| `game_event.py` | GetGameEvent | GetGameEventApi |
|
||
| `game_charge.py` | GetGameCharge | GetGameChargeApi |
|
||
| `game_weekly_data.py` | GetGameWeeklyData | GetGameWeeklyDataApi |
|
||
| `game_tournament_info.py` | GetGameTournamentInfo | GetGameTournamentInfoApi |
|
||
| `game_ng_music_id.py` | GetGameNgMusicId | GetGameNgMusicIdApi |
|
||
|
||
---
|
||
|
||
## 七、新增一个 api 要做什么
|
||
|
||
1. 在 `api/` 下新建脚本,定义一个能传参的函数
|
||
2. 函数首行写 docstring(会变成日志的 `[接口说明]`)
|
||
3. 用 `from .log_setup import log_info, log_request, log_response` 记日志
|
||
4. 用 `from .maimai_net import request_dict` 发包
|
||
5. 在 `api/__init__.py` 里**导入**并**加进 `_API_FUNCS`** ← 漏了这步就不会有运行提示
|
||
|
||
---
|
||
|
||
## 八、相关文档
|
||
|
||
| 文件 | 内容 |
|
||
|---|---|
|
||
| `docs/上传成绩开发文档.md` | UpsertUserAll 的 36 节点 / 110 字段 / 差分语义 / 真机对照 |
|
||
| `docs/主脚本编写指南.md` | ★ **写自己的主脚本**:三条铁律 / 最小骨架 / 四个流程模板 / 常见错误 |
|
||
| `docs/购买功能票流程.md` | ★ 登录 → 买票 → 登出:接口结构 / 源码流程 / validDate 算法 / 待抓包确认项 |
|
||
| `docs/UpsertUserAll踩坑清单.md` | ★ **28 条坑的逐条记录**:现象 / 根因 / 源码依据 / 正确做法 / 自检项 + 发包前 Checklist |
|
||
| `docs/实跑验证记录.md` | ★ **真实跑通记录**:returnCode=1 + 回读确认 playCount 1260→1261,含完整时间线与落库证据 |
|
||
| `docs/真机Hook对照报告.md` | ★ MelonLoader Mod 真机抓包(70 请求)与本实现的逐项对照:number / Cookie / CalcSpecialNum / 剩余差异说明 |
|
||
| `docs/UpsertUserAll实跑对比.md` | 逐字段对比明细(110 字段 × 真机 vs 本工程) |
|
||
| `docs/UpsertUserAll落库排查报告.md` | 「returnCode=1 但没落库」的根因分析 |
|
||
| `docs/日志与主脚本指南.md` | 日志模块完整参考 + 主脚本写法 |
|
||
| `docs/舞萌DX_扫码登录协议技术规范.md` | 扫码登录协议(上一代整理) |
|
||
| `source/2026-10-05.log` | 真机网络日志(本工程最重要的判据) |
|
||
|
||
---
|
||
|
||
*新增 api 后请同步更新第六节清单。*
|