chore: 不再提交 config.py,改为 config.py.example 模板
- config.py 加入 .gitignore(含机台/店铺私有标识,不入库) - 新增 config.py.example:机台与店铺标识 6 项留空待填 - README:新增「先填 config.py」步骤,列出必填项 - 脱敏:清除仓库里的 userId / clientId / placeId / regionId / rating - selftest:改为显式传值,不再依赖 config.py(换配置也能全绿)
This commit is contained in:
@@ -7,6 +7,9 @@ __pycache__/
|
|||||||
# ---- 本地查证用(含 SEGA 二进制 / 反编译代码 / 真机日志,不入库)----
|
# ---- 本地查证用(含 SEGA 二进制 / 反编译代码 / 真机日志,不入库)----
|
||||||
source/
|
source/
|
||||||
|
|
||||||
|
# ---- 私有配置:只提交 config.py.example,不提交 config.py ----
|
||||||
|
config.py
|
||||||
|
|
||||||
# ---- 文档单独放 docs 分支,主分支不含 ----
|
# ---- 文档单独放 docs 分支,主分支不含 ----
|
||||||
docs/
|
docs/
|
||||||
|
|
||||||
|
|||||||
@@ -103,11 +103,40 @@ WaHappy/
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 环境
|
### 1. 装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install requests pycryptodome
|
pip install requests pycryptodome
|
||||||
python selftest.py # 零网络自检,应该 236 项全绿
|
```
|
||||||
|
|
||||||
|
### 2. ★ 先填 `config.py`
|
||||||
|
|
||||||
|
仓库里只有模板 `config.py.example`,先复制一份:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config.py.example config.py # Linux / macOS
|
||||||
|
copy config.py.example config.py # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
然后**必须**填好 **`config.py` 第二节「机台与店铺标识」的 6 个值**:
|
||||||
|
|
||||||
|
| 变量 | 填什么 |
|
||||||
|
|---|---|
|
||||||
|
| `KEYCHIP_ID` | 你的机台 Keychip(**完整形式**,带连字符,形如 `XXXX-XXXXXXXXXXX`) |
|
||||||
|
| `CLIENT_ID` | Keychip **短值**(去掉连字符取前 11 位) |
|
||||||
|
| `REGION_ID` | 你所在区域 ID |
|
||||||
|
| `PLACE_ID` | 你所在店铺 ID |
|
||||||
|
| `PLACE_NAME` | 店铺名(游戏服务器不返回,只能手填) |
|
||||||
|
| `REGION_NAME` | 区域名 |
|
||||||
|
|
||||||
|
> ⚠️ **这 6 个值留空或填错,发出去的请求带的就是别人的机台身份,服务端会直接拒绝。**
|
||||||
|
>
|
||||||
|
> `config.py` 已在 `.gitignore` 里 —— 它是你的私有配置,**不要提交**。
|
||||||
|
|
||||||
|
### 3. 跑自检
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python selftest.py # 零网络自检,应该 236 项全绿(不依赖你的 config)
|
||||||
```
|
```
|
||||||
|
|
||||||
### upsert.py 用法
|
### upsert.py 用法
|
||||||
|
|||||||
+1
-2
@@ -89,8 +89,7 @@ QR_TOTAL_LEN = QR_PREFIX_LEN + QR_PAYLOAD_LEN # 84
|
|||||||
#: clientId = AMDaemon.System.KeychipId.ShortValue; // 短值
|
#: clientId = AMDaemon.System.KeychipId.ShortValue; // 短值
|
||||||
#:
|
#:
|
||||||
#: 即:aimedb 扫码用完整 KeychipId,游戏服务器用 ShortValue。
|
#: 即:aimedb 扫码用完整 KeychipId,游戏服务器用 ShortValue。
|
||||||
#: docs/舞萌DX_扫码登录协议技术规范.md §3.4 的抓包验证向量也是
|
#: docs 分支的《扫码登录协议技术规范》§3.4 里也有对应的抓包验证向量。
|
||||||
#: chipID = "XXXX-XXXXXXXXXXX"。
|
|
||||||
#: token 与 chipID 绑定,用错会导致后续接口「returnCode=1 但不落库」。
|
#: token 与 chipID 绑定,用错会导致后续接口「returnCode=1 但不落库」。
|
||||||
DEFAULT_CHIP_ID = KEYCHIP_ID
|
DEFAULT_CHIP_ID = KEYCHIP_ID
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,448 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
config.py —— 舞萌DX 纯网络脚本工程 · 运行时可配置项
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
★★ 这是**模板文件**。第一次使用请先复制一份:
|
||||||
|
|
||||||
|
cp config.py.example config.py # Linux / macOS
|
||||||
|
copy config.py.example config.py # Windows
|
||||||
|
|
||||||
|
然后**必须**填好第二节「机台与店铺标识」的 6 个值,否则脚本发出去的请求
|
||||||
|
带的是别人的机台身份,服务端会拒绝。
|
||||||
|
|
||||||
|
★ config.py 已在 .gitignore 里 —— 它属于你的私有配置,不要提交。
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
本文件集中存放**用户可能需要修改**的内容,其余协议常量写死在各自脚本内。
|
||||||
|
|
||||||
|
分六块:
|
||||||
|
1. 网络与加密常量(AES 密钥、协议版本、服务器地址)
|
||||||
|
2. 机台与店铺标识(Keychip、区域、店铺)
|
||||||
|
3. 扫码登录常量(aimedb)
|
||||||
|
4. ★ UpsertUserAll 即将上传的歌曲数据(默认取 source/2026-10-05.log 真机样本)
|
||||||
|
5. UpsertUserAll 安全闸门
|
||||||
|
6. 日志级别
|
||||||
|
|
||||||
|
权威来源全部标注在注释里,改之前请先看依据。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 一、网络与加密常量
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- 游戏服务器(Maimai2Servlet)---
|
||||||
|
# 依据:source/Assembly-CSharp/Manager/OperationManager.cs:225,338
|
||||||
|
# _operationData.ServerUri = Auth.GameServerUri + MovieServerUri("mE2s3Jhd/")
|
||||||
|
# 实测:source/2026-10-05.log:1
|
||||||
|
BASE_URL = "https://maimai-gm.wahlap.com:42081/Maimai2Servlet/mE2s3Jhd/"
|
||||||
|
|
||||||
|
# --- AES 参数 ---
|
||||||
|
# 依据:source/Assembly-CSharp/Net/CipherAES.cs:11-12
|
||||||
|
# AesKey = "FKM2JX:VjZNK6hc:A0<JU:i5oR7LA]9W" (32 字节 -> AES-256)
|
||||||
|
# AesIV = "F>;24DjU9W6ZsRH[" (16 字节)
|
||||||
|
AES_KEY = b"FKM2JX:VjZNK6hc:A0<JU:i5oR7LA]9W"
|
||||||
|
AES_IV = b"F>;24DjU9W6ZsRH["
|
||||||
|
|
||||||
|
# --- API 名混淆盐 ---
|
||||||
|
# 依据:source/Assembly-CSharp/Net.Packet/Packet.cs:44 ObfuscateParam = "8bF76dE9"
|
||||||
|
# 用法:URL 尾段 = md5(逻辑接口名 + "MaimaiChn" + OBFUSCATE_PARAM) 小写 hex
|
||||||
|
# "MaimaiChn" 后缀来自 source/Assembly-CSharp/Net.VO/NetQuery.cs:14 Api = api + "MaimaiChn"
|
||||||
|
OBFUSCATE_PARAM = "8bF76dE9"
|
||||||
|
|
||||||
|
# --- 报文版本标识 ---
|
||||||
|
# 依据:source/Assembly-CSharp/Net/NetHttpClient.cs:69 DefaultHttpHeaderEncryption = "1.55"
|
||||||
|
# (请求头 Mai-Encoding 的值;1.56 ROM 实测仍发 1.55)
|
||||||
|
MAI_ENCODING = "1.55"
|
||||||
|
|
||||||
|
# --- 超时(毫秒)---
|
||||||
|
# 依据:source/Assembly-CSharp/Net/NetConfig.cs:7-11
|
||||||
|
TIMEOUT_MS = 10000 # 普通接口
|
||||||
|
TIMEOUT_LONG_MS = 60000 # 登录 / 结算上传
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 二、机台与店铺标识
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Keychip ID(机台唯一标识,完整格式带连字符)
|
||||||
|
# ★★ 这是你自己的机台身份,不要外传;填错服务端会拒。
|
||||||
|
# 依据:source/Assembly-CSharp/Manager/ChimeReaderManager.cs:250
|
||||||
|
# KeychipID = AMDaemon.System.KeychipId.Value
|
||||||
|
KEYCHIP_ID = "" # ★★ 必填:形如 "XXXX-XXXXXXXXXXX"(带连字符)
|
||||||
|
|
||||||
|
# 客户端 ID(Keychip 短值,去掉连字符取前 11 位)
|
||||||
|
# 依据:source/Assembly-CSharp/Net.Packet.Mai2/PacketGetGameSetting.cs:23
|
||||||
|
# clientId = AMDaemon.System.KeychipId.ShortValue
|
||||||
|
# 实测:真机日志里 clientId 恒等于 Keychip 短值
|
||||||
|
CLIENT_ID = "" # ★★ 必填:Keychip 短值,去掉连字符取前 11 位
|
||||||
|
|
||||||
|
# 区域 ID / 店铺 ID
|
||||||
|
# 依据:source/Assembly-CSharp/Net.Packet.Mai2/PacketUserLogin.cs:24-25
|
||||||
|
# regionId = Auth.RegionCode ; placeId = Auth.LocationId
|
||||||
|
# 实测:真机日志里的 regionId / placeId 与机台所在地一致
|
||||||
|
REGION_ID = 0 # ★★ 必填:你所在区域 ID
|
||||||
|
PLACE_ID = 0 # ★★ 必填:你所在店铺 ID
|
||||||
|
|
||||||
|
# 店铺名 / 区域名
|
||||||
|
#
|
||||||
|
# ★ 为什么必须手工填:**游戏服务器没有任何接口返回店铺名**。
|
||||||
|
# 逐行扫过 source/2026-10-05.log 全部响应:
|
||||||
|
# - GetGameSettingApi 只回 gameSetting,无店名
|
||||||
|
# - GetUserDataApi 的 lastPlaceName 回的是 null
|
||||||
|
# - GetUserRegionApi 只回 regionId/playCount/created
|
||||||
|
# 店铺名来自机台本地 AMDaemon:
|
||||||
|
# source/AMDaemon.NET/AMDaemon/Allnet/Auth.cs:33 LocationName <- allnet_Auth_getLocationName()
|
||||||
|
# 是开机时 ALL.Net 认证下发的,客户端本地就有,不会向游戏服务器索取。
|
||||||
|
#
|
||||||
|
# 取值示例(真机 UpsertUserAll 报文):
|
||||||
|
# userPlaylogList[0].placeName = "<你的店铺名>"
|
||||||
|
# userData[0].lastRegionName = "<你的区域名>"
|
||||||
|
PLACE_NAME = "" # ★★ 必填:店铺名(游戏服务器不返回,只能手填)
|
||||||
|
REGION_NAME = "" # ★★ 必填:区域名
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 三、扫码登录常量(aimedb)
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 依据:source/ChimeLib.NET/ChimeLib/NET/COMM/CCommGetUserData.cs
|
||||||
|
# + docs/舞萌DX_扫码登录协议技术规范.md
|
||||||
|
|
||||||
|
# aimedb 服务器
|
||||||
|
AIMEDB_HOST = "ai.sys-allnet.cn"
|
||||||
|
AIMEDB_PORT = 443
|
||||||
|
AIMEDB_PATH = "/wc_aime/api/get_data"
|
||||||
|
|
||||||
|
# 签名密钥(含前导 X,32 字符)
|
||||||
|
# 来源:chimelib_dll.dll .rdata UTF-16 @0x00B040,抓包验证一致
|
||||||
|
QR_SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
|
||||||
|
|
||||||
|
# 固定字段
|
||||||
|
QR_OPEN_GAME_ID = "MAID"
|
||||||
|
QR_TITLE_KEY = "SDGB"
|
||||||
|
QR_USER_AGENT = "WC_AIME_LIB"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 三之二、★ 功能票(Ticket)主数据
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 依据:游戏本地主数据
|
||||||
|
# D:\SDGB2026\Package\Sinmai_Data\StreamingAssets\A000\ticket\ticketXXXXXX\Ticket.xml
|
||||||
|
# 源码侧 Manager.MaiStudio.TicketData.creditNum
|
||||||
|
#
|
||||||
|
# ★ 为什么脚本需要它:
|
||||||
|
# UpsertUserChargelogApi(购买功能票)的 userChargelog.price 取的是
|
||||||
|
# **ticket.creditNum(票的信用点倍数)**,不是 GetGameChargeApi 里的 money price:
|
||||||
|
# source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:410
|
||||||
|
# result.price = ticket.creditNum;
|
||||||
|
#
|
||||||
|
# ticketKind 含义(Ticket.xml 里的枚举):
|
||||||
|
# 1=Paid(付费,可在机台买) 2=Event 3=Free 4=Link 5=ShopWeekly 6=ShopChara
|
||||||
|
TICKET_CREDIT_NUM = {
|
||||||
|
0: 0,
|
||||||
|
2: 1, 3: 2, 4: 3, 5: 4, 6: 5, # 付费票:N 倍券的 creditNum = N-1
|
||||||
|
10005: 0, 10105: 0, 10205: 0, # 活动票
|
||||||
|
11001: 0, # 免费 1.5 倍券
|
||||||
|
30001: 0, 30002: 0, 30003: 0, # 连动 / 商店票
|
||||||
|
}
|
||||||
|
|
||||||
|
#: 票名(只用于日志好看,不影响协议)
|
||||||
|
TICKET_NAMES = {
|
||||||
|
0: "使用しない",
|
||||||
|
2: "2倍チケット", 3: "3倍チケット", 4: "4倍チケット", 5: "5倍チケット", 6: "6倍チケット",
|
||||||
|
10005: "イベント5倍チケット_1", 10105: "イベント5倍チケット_2",
|
||||||
|
10205: "イベント5倍チケット_3", 11001: "無料1.5倍チケット",
|
||||||
|
30001: "連動チケット", 30002: "每周区域前进2倍票", 30003: "旅行伙伴等级提升5倍票",
|
||||||
|
}
|
||||||
|
|
||||||
|
#: 付费票(ticketKind=1)—— 只有这些能在机台购买
|
||||||
|
PAID_TICKET_IDS = (2, 3, 4, 5, 6)
|
||||||
|
|
||||||
|
#: 买票闸门(和 UpsertUserAll 一样,默认不放行)
|
||||||
|
UPSERT_CHARGELOG_CONFIRM = False
|
||||||
|
|
||||||
|
#: 票有效期算法参数
|
||||||
|
#: 依据 source/Assembly-CSharp/Manager/TicketManager.cs:564
|
||||||
|
#: DateTime.Now.AddHours(-4.0).AddDays(90.0).ToString("yyyy-MM-dd 04:00:00")
|
||||||
|
#: 真机验证:chargeId=5 的 purchaseDate=2026-10-04 18:22:10
|
||||||
|
#: -> 减 4 小时 = 10-04 14:22,加 90 天 = 2027-01-02 -> "2027-01-02 04:00:00" ✅
|
||||||
|
TICKET_VALID_HOUR_OFFSET = -4
|
||||||
|
TICKET_VALID_DAYS = 90
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 四、★ UpsertUserAll 即将上传的歌曲数据
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# ★ 为什么放在 config:
|
||||||
|
# 一局游玩的成绩是**唯一无法由脚本自行产生**的输入 ——
|
||||||
|
# 纯网络脚本复刻不出游玩过程,服务端要的就是这份原始成绩。
|
||||||
|
# 换歌 / 换成绩只改这里,不用动任何 api 脚本。
|
||||||
|
#
|
||||||
|
# ★ 默认值 = source/2026-10-05.log:123 的**真机跑通报文**(UpsertUserAllApi 请求)
|
||||||
|
# 即 userPlaylogList[0] 逐字段照抄。
|
||||||
|
#
|
||||||
|
# ★ 两个"歌曲数据"的区别(别混):
|
||||||
|
# userPlaylogList 本局**原始判定输入** —— 服务端据此算分、写战绩表
|
||||||
|
# userMusicDetailList 按 (musicId, level) 的**历史归档** —— 本局那条 playCount +1
|
||||||
|
# 下面 PLAY_* 同时喂给这两处,保证一致。
|
||||||
|
|
||||||
|
# --- 曲目与难度 ---
|
||||||
|
PLAY_MUSIC_ID = 143 # 曲目 ID
|
||||||
|
PLAY_MUSIC_LEVEL = 4 # 0=Basic 1=Advanced 2=Expert 3=Master 4=Re:Master
|
||||||
|
PLAY_TRACK_NO = 1 # 本局第几首
|
||||||
|
|
||||||
|
# --- 版本号(同一版本两种写法,两处都要)---
|
||||||
|
PLAY_VERSION_INT = 1056000 # 整数形式,用于 userPlaylogList[].version(1.56.00)
|
||||||
|
PLAY_VERSION_STR = "1.56.00" # 字符串形式,用于 userGamePlaylogList[].version
|
||||||
|
|
||||||
|
# --- 成绩 ---
|
||||||
|
PLAY_ACHIEVEMENT = 0 # 达成率 ×10000(0 = 本局失败)
|
||||||
|
PLAY_DELUXSCORE = 0 # DX 分数
|
||||||
|
PLAY_SCORE_RANK = 0 # 评级档(0 = 无)
|
||||||
|
PLAY_IS_CLEAR = False # 是否 Clear
|
||||||
|
|
||||||
|
PLAY_MAX_COMBO = 0
|
||||||
|
PLAY_TOTAL_COMBO = 655 # 该谱面 note 总数(= 各类型判定数之和)
|
||||||
|
PLAY_MAX_SYNC = 0
|
||||||
|
PLAY_TOTAL_SYNC = 0
|
||||||
|
PLAY_COMBO_STATUS = 0 # 0=无 1=全连
|
||||||
|
PLAY_SYNC_STATUS = 0
|
||||||
|
|
||||||
|
PLAY_FAST_COUNT = 0
|
||||||
|
PLAY_LATE_COUNT = 0
|
||||||
|
|
||||||
|
# --- 判定明细 {类型: {判定位: 次数}} ---
|
||||||
|
# 真机样本是一局**失败**的成绩(全 Miss)
|
||||||
|
PLAY_JUDGE_COUNTS = {
|
||||||
|
"tap": {"CriticalPerfect": 0, "Perfect": 0, "Great": 0, "Good": 0, "Miss": 441},
|
||||||
|
"hold": {"CriticalPerfect": 0, "Perfect": 0, "Great": 0, "Good": 0, "Miss": 19},
|
||||||
|
"slide": {"CriticalPerfect": 0, "Perfect": 0, "Great": 0, "Good": 0, "Miss": 192},
|
||||||
|
"touch": {"CriticalPerfect": 0, "Perfect": 0, "Great": 0, "Good": 0, "Miss": 0},
|
||||||
|
"break": {"CriticalPerfect": 0, "Perfect": 0, "Great": 0, "Good": 0, "Miss": 3},
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 评级(本局前后)---
|
||||||
|
PLAY_BEFORE_RATING = 15000
|
||||||
|
PLAY_AFTER_RATING = 15000
|
||||||
|
PLAY_BEFORE_GRADE = 0
|
||||||
|
PLAY_AFTER_GRADE = 0
|
||||||
|
PLAY_AFTER_GRADE_RANK = 1
|
||||||
|
PLAY_BEFORE_DELUX_RATING = 15000
|
||||||
|
PLAY_AFTER_DELUX_RATING = 15000
|
||||||
|
|
||||||
|
# --- 曲种索引 ---
|
||||||
|
# ★ 必须与 userExtend.categoryIndex 一致(真机两处都是 2022)
|
||||||
|
PLAY_CATEGORY_INDEX = 2022
|
||||||
|
PLAY_MUSIC_INDEX = 10 # userExtend.musicIndex
|
||||||
|
PLAY_EXTRA_FLAG = 1721 # userExtend.extraFlag
|
||||||
|
|
||||||
|
# --- 本局使用的角色快照(characterId1..5 / Level / Awakening)---
|
||||||
|
#
|
||||||
|
# ★ 语义:**进入游玩时**的快照值,不要加经验 —— 这一点极易搞错。
|
||||||
|
# (结算后的等级在 upsertUserAll.userCharacterList 里,那处才 +经验)
|
||||||
|
# 真机实证(source/2026-10-05.log:123 同一份报文):
|
||||||
|
# userPlaylogList[0].characterLevel1..5 = 3800/4455/134/134/134 ← 快照(游玩前)
|
||||||
|
# upsertUserAll.userCharacterList = 3801/4456/135/135/135 ← 结算后
|
||||||
|
#
|
||||||
|
# ★★ 通常**不用改这里**:main.py 会调用 derive_play_characters(snapshot),
|
||||||
|
# 直接从服务器刚下发的 userData.charaSlot + userCharacterList 推出游玩前等级,
|
||||||
|
# 并覆盖本配置。只有拿不到快照(离线组包)时才回退到这里。
|
||||||
|
#
|
||||||
|
# 默认值 = 真机 charaSlot [395, 101, 400402, 400401, 200401] 的游玩前等级
|
||||||
|
PLAY_CHARACTERS = [
|
||||||
|
{"characterId": 395, "level": 3800, "awakening": 5},
|
||||||
|
{"characterId": 101, "level": 4455, "awakening": 5},
|
||||||
|
{"characterId": 400402, "level": 134, "awakening": 3},
|
||||||
|
{"characterId": 400401, "level": 134, "awakening": 3},
|
||||||
|
{"characterId": 200401, "level": 134, "awakening": 3},
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- 本局消耗 ---
|
||||||
|
# ★ playCredit 语义(VOExtensions.cs:838-845):
|
||||||
|
# playCredit = Credit.GameCostEnoughPlay()(投币成本) + TicketManager.GetTicketCredit(useTicketId)
|
||||||
|
# 即"这一局总共折合多少 credit",不是余额、不是曲目数。真机样本 = 1。
|
||||||
|
PLAY_CREDIT = 1
|
||||||
|
# ★ -1 = 不使用票。依据 TicketManager.cs:47 初始化 _useTicketId[i] = -1;
|
||||||
|
# GetTicketCredit(-1) 里 GetTicket(-1) 返回 null -> 返回 0,即不用票不折算 credit。
|
||||||
|
PLAY_USE_TICKET_ID = -1
|
||||||
|
PLAY_MODE = 0 # 0=通常
|
||||||
|
PLAY_IS_TUTORIAL = False
|
||||||
|
PLAY_IS_EVENT_MODE = False
|
||||||
|
PLAY_IS_NEW_FREE = False
|
||||||
|
PLAY_IS_FREEDOM_MODE = False
|
||||||
|
|
||||||
|
# --- 机台本地计数器(★ 无法从服务器推导,只能手填)---
|
||||||
|
#
|
||||||
|
# 依据:source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:881
|
||||||
|
# result.playCount = Backup.localParameter.playCount
|
||||||
|
# 这是机台**本地**的游玩计数器,任何 Get 接口都不会返回它,
|
||||||
|
# 所以既推不出来也不会被服务端纠正 —— 填错只是个统计数字不对。
|
||||||
|
# 取值 = 该机台当天/本轮的累计游玩局数(每打一局 +1)。
|
||||||
|
# 真机实测:19:21 那一局 = 9,22:31 那一局 = 10。
|
||||||
|
PLAY_LOCAL_PLAY_COUNT = 8
|
||||||
|
|
||||||
|
# --- ★ playSpecial 里的 "CalcSpecialNum"(机台硬件标志位)---
|
||||||
|
#
|
||||||
|
# 依据:source/Assembly-CSharp/Manager/GameManager.cs:332-378
|
||||||
|
# source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:846-855
|
||||||
|
#
|
||||||
|
# playSpecial = 位反转32( Random.Range(1,1037933) * 2069 + CalcSpecialNum() )
|
||||||
|
#
|
||||||
|
# CalcSpecialNum() 是**按位或**出来的机台特征掩码,逐位含义(源码原文):
|
||||||
|
# 0x001 SpecialAP (测试/特殊 AP)
|
||||||
|
# 0x002 IsAutoPlay()
|
||||||
|
# 0x004 Win32.getRawInputDeviceCount 的 keyboard > 0
|
||||||
|
# 0x008 同上 mouse > 0
|
||||||
|
# 0x010 config.IsAutoPlay / IsDebugDisp / IsAllOpen
|
||||||
|
# 0x020 AMDaemon.System.IsDevelop
|
||||||
|
# 0x040 !SystemInfo.deviceName.Contains("ACA") ← 真机是 ACA,故该位为 0
|
||||||
|
# 0x080 !graphicsDeviceName.Contains("GeForce GTX") ← 真机是 GTX,故该位为 0
|
||||||
|
# 0x100 !processorType.Contains("Core(TM) i3-8100") ← 真机是 i3-8100,故该位为 0
|
||||||
|
# 0x400 无条件置位(源码 GameManager.cs:375) ← ★ 恒为 1
|
||||||
|
#
|
||||||
|
# ★ 真机实测(gamehook 抓到 CalcSpecialNum 调用):
|
||||||
|
# [CalcSpecialNum] = 1024 (SpecialAP=False IsFreedomMode=False)
|
||||||
|
# 1024 = 0x400,即除"无条件置位"那一位外全部为 0 —— 正好是一台标准 ACA 机台。
|
||||||
|
#
|
||||||
|
# ★ 反推验证:真机 playSpecial = 892014272
|
||||||
|
# 位反转32(892014272) = 57726124 = 27900 * 2069 + 1024
|
||||||
|
# 余数恰好 1024,与抓到的 CalcSpecialNum 完全吻合。
|
||||||
|
#
|
||||||
|
# 改成 0 不会报错,但 playSpecial 的位模式就不是真机那种了。
|
||||||
|
PLAY_SPECIAL_NUM = 1024
|
||||||
|
|
||||||
|
# --- 曲目历史游玩次数增量 ---
|
||||||
|
#
|
||||||
|
# ★ 默认 0 = 本局**不改动**该曲目的历史游玩次数。
|
||||||
|
# ★★ 注意语义:是「条目照传、次数不变」,**不是**「不回传这个节点」。
|
||||||
|
# 实测(同一份代码、同一账号,唯一差别就是这个节点):
|
||||||
|
# userMusicDetailList 回传 1 条 -> returnCode=1 且 playCount 真的 +1(落库)
|
||||||
|
# userMusicDetailList 留空 -> returnCode=1 但 playCount 纹丝不动(**没落库**)
|
||||||
|
# 所以「不想让乐曲 playCount 增加」必须靠把**增量**设成 0,
|
||||||
|
# 而不是把整个节点清空 —— 后者会让整包被服务端静默丢弃。
|
||||||
|
#
|
||||||
|
# 想复刻真机的「该谱面 playCount +1」就改成 1。
|
||||||
|
PLAY_MUSIC_PLAY_COUNT_GAIN = 0
|
||||||
|
|
||||||
|
# --- 地图进度增量 ---
|
||||||
|
#
|
||||||
|
# ★ 默认 0 = **本局不推进地图**,userMapList 回传空数组。
|
||||||
|
# 想复刻真机的"地图 +11000"就改成 11000(真机样本:mapId=550003 的
|
||||||
|
# distance 1424000 -> 1435000)。
|
||||||
|
#
|
||||||
|
# 依据:source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs 的地图导出逻辑
|
||||||
|
# —— 没变化的条目不会进差分结果。
|
||||||
|
PLAY_MAP_DISTANCE_GAIN = 0
|
||||||
|
|
||||||
|
# --- 组队角色等级增量 ---
|
||||||
|
#
|
||||||
|
# ★ 默认 0 = **本局不给角色加经验**,userCharacterList 回传空数组。
|
||||||
|
# 想复刻真机的"组队 5 只各 +1 级"就改成 1(真机样本 4455->4456 等)。
|
||||||
|
#
|
||||||
|
# ★ 注意:设成 0 时 userCharacterList 差集为空 -> 该节点不回传,
|
||||||
|
# 这是**预期的**,不是错误;validate_before_send 也只在你把增量设成非 0
|
||||||
|
# 却又没产出条目时才告警。
|
||||||
|
PLAY_CHARACTER_EXP_GAIN = 0
|
||||||
|
|
||||||
|
# --- ★ 关于 userGetPointList / userLoginBonusList(留空即可)---
|
||||||
|
#
|
||||||
|
# 这两个节点是**登录奖励发放**才产生的,普通游玩结算不需要回传:
|
||||||
|
#
|
||||||
|
# userGetPointList(里程/点数明细)
|
||||||
|
# 真机 39 次 UpsertUserAll 样本里,**25 次为空(64%)**;
|
||||||
|
# 只有发生登录奖励/任务奖励时才非空(如 getKind=1 point=200)。
|
||||||
|
# 留空 => 服务端不累加 point,账户点数保持不变。
|
||||||
|
#
|
||||||
|
# userLoginBonusList(签到奖励)
|
||||||
|
# 同理,只有本次登录触发新签到才回传那一条(isCurrent=true)。
|
||||||
|
# 留空 => isNewLoginBonusList 也是空串。
|
||||||
|
#
|
||||||
|
# ★ 连带影响(正常现象):留空后 userData.point 保持基线值不变。
|
||||||
|
#
|
||||||
|
# 源码参考(如需还原发放行为):
|
||||||
|
# Manager/UserData.cs:57-62 enum MaiMileGetKind { Mission=1, FriendBonus=2, Present=3 }
|
||||||
|
# Manager/UserData.cs:352-367 AddMissionPointLog / AddFriendBonusPointLog
|
||||||
|
|
||||||
|
# --- 店铺库存查询清单(不填则跳过该节点)---
|
||||||
|
PLAY_SHOP_ITEM_IDS = []
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 五、UpsertUserAll 安全闸门
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# ★ 这是**唯一有副作用**的接口:会把本局成绩真实写进服务器,不可撤销。
|
||||||
|
# 保持 False(默认),调用时必须显式传 confirm=True 才真正发包。
|
||||||
|
# 改成 True 会永久放行(不建议)。
|
||||||
|
UPSERT_CONFIRM = False
|
||||||
|
|
||||||
|
# 真实发包前的冷却等待(秒)
|
||||||
|
#
|
||||||
|
# ★ 语义:**每次**真实发送 UpsertUserAll 之前,都固定等待这么多秒(含首次)。
|
||||||
|
# 目的:让上报节奏贴近真机 —— 真机从登录到结算上报约 80 秒,
|
||||||
|
# 不会登录后立刻上报(source/2026-10-05.log:25 登录 → :123 上报,间隔 81 秒)。
|
||||||
|
#
|
||||||
|
# ★ 只作用于**真实发送**(confirm=True);dry_run 不等待。
|
||||||
|
# ★ 冷却发生在「发包前自检」之后,所以自检有问题时会先看到告警,不必白等。
|
||||||
|
# ★ 调试时想跳过等待:把这里改成 0,或调用时传 cooldown_sec=0。
|
||||||
|
UPSERT_SEND_COOLDOWN_SEC = 60
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 六、日志级别
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# "info" 只输出 api 脚本的运行情况与结果:
|
||||||
|
# === XxxApi 开始运行 ===
|
||||||
|
# === XxxApi 运行结束 ===
|
||||||
|
#
|
||||||
|
# "debug" 在上面基础上,额外输出向 api 发送的数据与 api 返回的数据:
|
||||||
|
# → 请求 XxxApi URL / 请求头 / 请求体 JSON 全文
|
||||||
|
# ← 响应 XxxApi 状态码 / 耗时 / 响应头 / 响应体 JSON 全文
|
||||||
|
#
|
||||||
|
LOG_LEVEL = "info"
|
||||||
|
|
||||||
|
# 单条日志里内嵌长文本的截断长度(0 = 不截断)
|
||||||
|
LOG_VALUE_MAX = 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 导出
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# 网络与加密
|
||||||
|
"BASE_URL", "AES_KEY", "AES_IV", "OBFUSCATE_PARAM", "MAI_ENCODING",
|
||||||
|
"TIMEOUT_MS", "TIMEOUT_LONG_MS",
|
||||||
|
# 机台与店铺
|
||||||
|
"KEYCHIP_ID", "CLIENT_ID", "REGION_ID", "PLACE_ID", "PLACE_NAME", "REGION_NAME",
|
||||||
|
# 功能票
|
||||||
|
"TICKET_CREDIT_NUM", "TICKET_NAMES", "PAID_TICKET_IDS",
|
||||||
|
"UPSERT_CHARGELOG_CONFIRM", "TICKET_VALID_HOUR_OFFSET", "TICKET_VALID_DAYS",
|
||||||
|
# 扫码登录
|
||||||
|
"AIMEDB_HOST", "AIMEDB_PORT", "AIMEDB_PATH", "QR_SIGN_KEY",
|
||||||
|
"QR_OPEN_GAME_ID", "QR_TITLE_KEY", "QR_USER_AGENT",
|
||||||
|
# 歌曲数据
|
||||||
|
"PLAY_MUSIC_ID", "PLAY_MUSIC_LEVEL", "PLAY_TRACK_NO",
|
||||||
|
"PLAY_VERSION_INT", "PLAY_VERSION_STR",
|
||||||
|
"PLAY_ACHIEVEMENT", "PLAY_DELUXSCORE", "PLAY_SCORE_RANK", "PLAY_IS_CLEAR",
|
||||||
|
"PLAY_MAX_COMBO", "PLAY_TOTAL_COMBO", "PLAY_MAX_SYNC", "PLAY_TOTAL_SYNC",
|
||||||
|
"PLAY_COMBO_STATUS", "PLAY_SYNC_STATUS", "PLAY_FAST_COUNT", "PLAY_LATE_COUNT",
|
||||||
|
"PLAY_JUDGE_COUNTS",
|
||||||
|
"PLAY_BEFORE_RATING", "PLAY_AFTER_RATING",
|
||||||
|
"PLAY_BEFORE_GRADE", "PLAY_AFTER_GRADE", "PLAY_AFTER_GRADE_RANK",
|
||||||
|
"PLAY_BEFORE_DELUX_RATING", "PLAY_AFTER_DELUX_RATING",
|
||||||
|
"PLAY_CATEGORY_INDEX", "PLAY_MUSIC_INDEX", "PLAY_EXTRA_FLAG",
|
||||||
|
"PLAY_CHARACTERS", "PLAY_CREDIT", "PLAY_USE_TICKET_ID", "PLAY_MODE",
|
||||||
|
"PLAY_IS_TUTORIAL", "PLAY_IS_EVENT_MODE", "PLAY_IS_NEW_FREE", "PLAY_IS_FREEDOM_MODE",
|
||||||
|
"PLAY_LOCAL_PLAY_COUNT", "PLAY_SPECIAL_NUM",
|
||||||
|
"PLAY_MUSIC_PLAY_COUNT_GAIN", "PLAY_MAP_DISTANCE_GAIN", "PLAY_CHARACTER_EXP_GAIN",
|
||||||
|
"PLAY_SHOP_ITEM_IDS",
|
||||||
|
# 闸门
|
||||||
|
"UPSERT_CONFIRM", "UPSERT_SEND_COOLDOWN_SEC",
|
||||||
|
# 日志
|
||||||
|
"LOG_LEVEL", "LOG_VALUE_MAX",
|
||||||
|
]
|
||||||
+7
-2
@@ -348,7 +348,10 @@ section("8. userPlaylog 字段完整性(110 字段)")
|
|||||||
|
|
||||||
check("PLAYLOG_FIELDS 共 110 个", len(PLAYLOG_FIELDS) == 110, str(len(PLAYLOG_FIELDS)))
|
check("PLAYLOG_FIELDS 共 110 个", len(PLAYLOG_FIELDS) == 110, str(len(PLAYLOG_FIELDS)))
|
||||||
|
|
||||||
pl = build_user_playlog(7692848101484135861, 1791159576)
|
# ★ 显式传中性值,**不依赖 config.py** —— 否则用户改了 config 这里就红
|
||||||
|
pl = build_user_playlog(7692848101484135861, 1791159576,
|
||||||
|
PlayData(before_rating=15000, after_rating=15000),
|
||||||
|
place_id=1000, client_id="TESTCLIENT1")
|
||||||
check("构造出的 playlog 有 110 个键", len(pl) == 110, str(len(pl)))
|
check("构造出的 playlog 有 110 个键", len(pl) == 110, str(len(pl)))
|
||||||
check("无缺失字段", all(f in pl for f in PLAYLOG_FIELDS))
|
check("无缺失字段", all(f in pl for f in PLAYLOG_FIELDS))
|
||||||
check("无多余字段", all(k in PLAYLOG_FIELDS for k in pl))
|
check("无多余字段", all(k in PLAYLOG_FIELDS for k in pl))
|
||||||
@@ -429,7 +432,8 @@ check("宴谱(musicId>=100000)level 强制为 10",
|
|||||||
|
|
||||||
section("9. userGamePlaylogList(13 字段)")
|
section("9. userGamePlaylogList(13 字段)")
|
||||||
|
|
||||||
gp = build_user_game_playlog(7692848101484135861, 1791159576)
|
gp = build_user_game_playlog(7692848101484135861, 1791159576,
|
||||||
|
client_id="TESTCLIENT1")
|
||||||
REAL_GP = {
|
REAL_GP = {
|
||||||
"playlogId": 7692848101484135861, "version": "1.56.00",
|
"playlogId": 7692848101484135861, "version": "1.56.00",
|
||||||
"playMode": 0, "useTicketId": -1, "playCredit": 1, "playTrack": 1,
|
"playMode": 0, "useTicketId": -1, "playCredit": 1, "playTrack": 1,
|
||||||
@@ -968,6 +972,7 @@ _creq = UpsertUserChargelog(
|
|||||||
charge_list=[{"chargeId": 2, "stock": 3, "purchaseDate": "x", "validDate": "y"}],
|
charge_list=[{"chargeId": 2, "stock": 3, "purchaseDate": "x", "validDate": "y"}],
|
||||||
game_charge_list=[{"chargeId": 2, "price": 1}],
|
game_charge_list=[{"chargeId": 2, "price": 1}],
|
||||||
user_data={"playCount": 1260, "playerRating": 15000},
|
user_data={"playCount": 1260, "playerRating": 15000},
|
||||||
|
place_id=1000, region_id=1, client_id="TESTCLIENT1",
|
||||||
purchase_date="2026-10-06 13:00:00", guard_no_stock=False, dry_run=True)
|
purchase_date="2026-10-06 13:00:00", guard_no_stock=False, dry_run=True)
|
||||||
check("请求体 4 个顶层字段且顺序照声明",
|
check("请求体 4 个顶层字段且顺序照声明",
|
||||||
list(_creq.__dataclass_fields__) == ["userId", "userChargelog", "userCharge", "loginDateTime"],
|
list(_creq.__dataclass_fields__) == ["userId", "userChargelog", "userCharge", "loginDateTime"],
|
||||||
|
|||||||
Reference in New Issue
Block a user