孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
12 KiB
12 KiB
舞萌DX 纯网络脚本工程 · 项目总览
项目名 WaHappy —— 舞萌DX 的游戏助手。 面向使用者的说明见根目录
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/ 里的每个脚本是一个功能模块,对外暴露函数;函数能接收参数,
内部完成「组包 → 发包 → 解析 → 返回结果」的全过程。
def 某Api函数(必填参数, *, 可选参数...) -> 结果:
"""一句话说明(会出现在日志的 [接口说明] 里)"""
...
参数形态规律:
- 业务数据(user_id / token / 二维码 / playlog_id)→ 位置参数
- 配置项(client_id / region_id / timeout_ms)→ 关键字参数,可省略走默认值
四、日志
4.1 两种模式
| 模式 | 输出内容 |
|---|---|
| INFO(默认) | 只展示 api 脚本的运行情况和结果 |
| DEBUG | 在 INFO 基础上,额外展示向 api 发送的数据和api 返回的数据 |
切换只改 config.py 一行:
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 主脚本怎么写
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 要做什么
- 在
api/下新建脚本,定义一个能传参的函数 - 函数首行写 docstring(会变成日志的
[接口说明]) - 用
from .log_setup import log_info, log_request, log_response记日志 - 用
from .maimai_net import request_dict发包 - 在
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 后请同步更新第六节清单。