# -*- 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. 日志级别 权威来源全部标注在注释里,改之前请先看依据。 """ # ============================================================================= # 一、网络与加密常量 # ============================================================================= # ★ BASE_URL / AES_KEY / AES_IV / OBFUSCATE_PARAM 已下沉到 api/MaimaiNet.py # --- 报文版本标识 --- # 依据: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 # 登录 / 结算上传 # ============================================================================= # 二、机台与店铺标识 # ============================================================================= # 客户端 ID(Keychip 短值) # 依据:source/Assembly-CSharp/Net.Packet.Mai2/PacketGetGameSetting.cs:23 # clientId = AMDaemon.System.KeychipId.ShortValue # 实测:真机日志里 clientId 恒等于 Keychip 短值 # ★ 形态 = 15 位 Keychip 去掉连字符后的**前 11 位**(大写十六进制)。 CLIENT_ID = "" # ★★ 必填:形如 "A63E01C3047" # 区域 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 # Keychip ID(机台唯一标识,完整格式带连字符) # ★★ 这是你自己的机台身份,不要外传;填错服务端会拒。 # 依据:source/Assembly-CSharp/Manager/ChimeReaderManager.cs:250 # KeychipID = AMDaemon.System.KeychipId.Value # ★ 组成 = CLIENT_ID 的前 4 位 + "-" + CLIENT_ID 第 5 位起 + PLACE_ID # 例:CLIENT_ID "A63E01C3047" + PLACE_ID 1007 -> "A63E-01C30471007" # 所以它由上面两个值**推导**,不要手改。 KEYCHIP_ID = (CLIENT_ID[:4] + "-" + CLIENT_ID[4:] + str(PLACE_ID)) if CLIENT_ID else "" # 店铺名 / 区域名 # # ★ 为什么以前必须手工填:**游戏服务器没有任何接口返回店铺名**。 # 逐行扫过 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 认证下发的,客户端本地就有,不会向游戏服务器索取。 # # ★★ 现在不用手工抄了:python authv1.py <你的Keychip> 会直接问 ALL.Net # 认证服务器要回 name / region_name0。把下面的 AUTHV1_WRITE_CONFIG 打开, # 它还能把结果自动写回 config.py。 PLACE_NAME = "" # ★★ 必填:店铺名 REGION_NAME = "" # ★★ 必填:区域名 # --- authv1 回写开关 --- # # authv1.py(Keychip -> 机台身份)查到结果后,是否**自动写回 config.py**。 # # True : 每次查询成功后,把 CLIENT_ID / REGION_ID / PLACE_ID / # PLACE_NAME / REGION_NAME 五个值改写进 config.py。 # (KEYCHIP_ID 是推导值,会自动跟着变,不用写。) # 改写前会把原文件备份成 config.py.bak,写坏不落盘。 # False : 只打印结果,不动 config.py。(默认,安全) # # 命令行可临时覆盖:--write 强制写 / --no-write 强制不写。 AUTHV1_WRITE_CONFIG = False # ============================================================================= # 三、扫码登录常量(aimedb) # ============================================================================= # # 依据:source/ChimeLib.NET/ChimeLib/NET/COMM/CCommGetUserData.cs # + docs/舞萌DX_扫码登录协议技术规范.md # ★ aimedb / QR 常量已下沉到 api/Chime.py # ============================================================================= # 三之二、★ 功能票(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 / TICKET_NAMES / PAID_TICKET_IDS / # TICKET_VALID_HOUR_OFFSET / TICKET_VALID_DAYS # 已下沉到 api/UpsertUserChargelog.py #: 买票闸门(和 UpsertUserAll 一样,默认不放行) UPSERT_CHARGELOG_CONFIRM = False # ============================================================================= # 四、★ 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/AFTER_RATING 等已改为运行时从服务器快照取(derive_play_rating) # --- 曲种索引 --- # ★ 必须与 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] 的游玩前等级 # --- 本局消耗 --- # ★ 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 恒为 2(已下沉到 api/UpsertUserAll.py) # --- ★ 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 已下沉到 api/UpsertUserAll.py # --- 用户总游玩次数增量 --- # # ★ 默认 0 = **userData.playCount / currentPlayCount 都不变**(本局不计入总次数)。 # 想复刻真机的「+1」就改成 1。 # # 为什么默认改成 0: # playCount 是账号的**总游玩次数**,是玩家可见的统计值; # 本工程做「成绩上传」时不需要改动它,保持与上传前一致更安全。 # ★ 改成 0 之后的副作用:「落库回查」不能再靠 playCount 是否 +1 判断, # 会自动改用 lastPlayDate 是否更新来判断(见 verify_upsert_landing)。 PLAY_USER_PLAY_COUNT_GAIN = 0 # --- 曲目历史游玩次数增量 --- # # ★ 默认 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", "AUTHV1_WRITE_CONFIG", # 功能票 "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_USER_PLAY_COUNT_GAIN", "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", ]