feat: ID 映射支持叠加单独导出的数据包,并纳入 event 表

D:/A031OptDump/Root 是比 G:\Package 更新的一层增补(曲目到 12024、宴曲
111537/111765/112002),原来的 --package 只认整合包内的 A??? 目录,套不到它。

- 新增可重复的 --data-root,把一个目录直接当数据根,按传参顺序追加、后者覆盖
  同 ID 行;--package 与 --data-root 可混用。
- 新增 events.json(eventId → 活动名 / alwaysOpen / infoType / disableArea)。
  本地 event 表没有日期窗口,开没开由服务器 GetGameEventApi 的
  GameEvent{startDate,endDate} 决定,README 里写明了这一点,免得误用它判状态。
- 合并两层后:music 1367→1378、map 168→169、title 3000→3001、event 824。
  11820 在两层数据包里都不存在,结论不变。
- lockType 分布按合并后数据更正为 Unlock 1347 / Lock 18 / Challenge 5 /
  Transmission 2 / KaleidScope 6,顺带说明大多数歌 lockType==Unlock、
  本来就可玩,不查 itemKind 5。
This commit is contained in:
Cookiues committed 2026-10-02 01:16:15 +08:00
1 parent 264b1d0aa7
commit 2c7f9c1de7
7 files changed
+6071 -37

No files matched your search

+19 -5
View File
@@ -168,7 +168,11 @@ data/id_mapping/ # 由 tools/gen_id_mapping.dart 从机台数据包抽
「上传成功但没生效」。为此把机台数据包里的表抽成可查的映射:
```bash
# 整合包(自动找 <pkg>/Sinmai_Data/StreamingAssets/A0xx/)
dart run tools/gen_id_mapping.dart --package G:/Package
# 再叠一层单独导出的数据包,后面的覆盖前面的同 ID 行
dart run tools/gen_id_mapping.dart --package G:/Package --data-root D:/A031OptDump/Root
```
产物落在 `data/id_mapping/`:
@@ -179,22 +183,32 @@ dart run tools/gen_id_mapping.dart --package G:/Package
| `items.json` | 按 `ItemKind` 分组(1 姓名框 2 称号 3 头像 9 角色 10 搭档 11 背景板 12 功能票)的 id → 名称 |
| `maps.json` | mapId → 区域名 / `IslandId` / `OpenEventId` / `ColorId` / `BonusMusicId` / `IsInfinity` / `IsCollabo` |
| `gates.json` | 万花筒三张表:`gateList`(gateId → 门名 / `gateType` / `eventName`)、`courseList`(courseId ↔ gateId ↔ keyId)、`keyList` |
| `events.json` | eventId → 活动名 / `alwaysOpen` / `infoType` / `disableArea` |
要点:
- 真正的 ID 在 XML 的 `<name><id>` 里,**目录名只是 `dataName`,两者不相等**
(`icon/icon550201/Icon.xml` 的 `name.id` 实际是 1)。
(`icon/icon550201/Icon.xml` 的 `name.id` 实际是 1;`music/music111537` 的
`name.id` 是 111537,属宴曲段)。
- 数据包按 `Sinmai_Data/StreamingAssets/A0xx/` 分基础包与增补包,抽取时按目录名
升序合并、同 ID 后者覆盖前者,与客户端 `DataManager.LoadData` 的语义一致。
- **本地 event 表没有日期窗口**:开关状态是服务器 `GetGameEventApi` 的
`GameEvent{startDate,endDate}` 决定的。`events.json` 只能告诉你某个
`eventName.id` 是哪个活动、是否 `alwaysOpen`,判断「现在开没开」得问服务器。
- 放在 `data/` 而不是 `assets/`:目前还没有代码在运行时读它,进 `assets/` 会被
原样打进每个安装包(约 1.2MB)。真要接进 App 时再连同 `pubspec.yaml` 一起挪。
顺手用这份表核出来的几个事实,都能解释此前的「不显示」:
- **musicId 11820 在曲表里根本不存在**(11819、11821 也不在;11814 之后直接跳到
11822)。它不是万花筒锁——`lockType = 4 (KaleidScope)` 的歌整张表只有 6 首
(11740 / 11745 / 11749 / 11753 / 11809 / 11814)。11820 属于「机台没有这首歌」,
任何 `UpsertUserAll` 都变不出来。
- **musicId 11820 在任何一层数据包里都不存在**(11819、11821 也不在;11814 之后
直接跳到 11822,G:\Package 与 D:/A031OptDump 两层都查过)。它不是万花筒锁——
`lockType = 4 (KaleidScope)` 的歌整张表只有 6 首(11740 / 11745 / 11749 /
11753 / 11809 / 11814)。11820 属于「机台没这首歌」,任何 `UpsertUserAll`
都变不出来。
- `lockType` 的分布是 `Unlock=1347 / Lock=18 / Challenge=5 / Transmission=2 /
KaleidScope=6`(合并两层数据包后共 1378 首),所以绝大多数歌根本不查
`itemKind 5`(`lockType == Unlock` 时 `IsPlayable` 直接为 true)——这也解释了
为什么有些「解歌」包发下去看不出效果:那首歌本来就可玩。
- **角色(旅行伙伴)1001 不存在**,合法 ID 从 101 起,整表 983 条、上界到 605905。
以前页面里「例如 1001」的提示正是这个坑。
- **万花筒只有 gate 1~6**(蓝/白/紫/黑/黄/红,course 与 key 同为 1~6 一一对应)。
File diff suppressed because it is too large. Load diff
+4
View File
@@ -13593,6 +13593,10 @@
{
"id": 609000,
"name": "WEC2026 以光之明"
},
{
"id": 708106,
"name": "OV3RCLOCK"
}
],
"3": [
+14
View File
@@ -2350,5 +2350,19 @@
"BonusMusicIdName": "Paradigm: Rebootちほーボーナス曲",
"IsCollabo": true,
"IsInfinity": false
},
{
"id": 650056,
"name": "ヒメヒナちほー2",
"IslandId": 60,
"IslandIdName": "maimaiDX CiRCLE",
"OpenEventId": 261002041,
"OpenEventIdName": "261002_04_1:ヒメヒナちほー2 解禁",
"ColorId": 650056,
"ColorIdName": "ヒメヒナ2",
"BonusMusicId": 650056,
"BonusMusicIdName": "ヒメヒナちほー2ボーナス曲",
"IsCollabo": true,
"IsInfinity": false
}
]
+6 -5
View File
@@ -1,19 +1,20 @@
{
"generatedFrom": "G:/Package",
"dataDirs": [
"A000",
"A005",
"A011",
"A015"
"A015",
"Root"
],
"counts": {
"music": 1367,
"map": 168,
"music": 1378,
"map": 169,
"gate": 6,
"course": 6,
"key": 6,
"event": 824,
"itemKind1": 398,
"itemKind2": 3000,
"itemKind2": 3001,
"itemKind3": 1046,
"itemKind9": 983,
"itemKind10": 25,
+190 -3
View File
@@ -18963,13 +18963,13 @@
"genreNameName": "POPSアニメ",
"AddVersion": 21,
"AddVersionName": "BUDDiES",
"eventName": 1,
"eventNameName": "無期限常時解放",
"eventName": 0,
"eventNameName": "解放なし",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 24006
"version": 24003
},
{
"id": 11640,
@@ -22252,6 +22252,142 @@
"longMusic": 0,
"version": 25007
},
{
"id": 11997,
"name": "キスキツネ",
"artistName": 781,
"artistNameName": "HIMEHINA",
"genreName": 101,
"genreNameName": "POPSアニメ",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261002021,
"eventNameName": "261002_02_1:ヒメヒナちほー2 3曲追加《キスキツネ、WWW、うたかたよいかないで》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26506
},
{
"id": 11998,
"name": "WWW",
"artistName": 781,
"artistNameName": "HIMEHINA",
"genreName": 101,
"genreNameName": "POPSアニメ",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261002021,
"eventNameName": "261002_02_1:ヒメヒナちほー2 3曲追加《キスキツネ、WWW、うたかたよいかないで》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26506
},
{
"id": 11999,
"name": "うたかたよいかないで",
"artistName": 781,
"artistNameName": "HIMEHINA",
"genreName": 101,
"genreNameName": "POPSアニメ",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261002021,
"eventNameName": "261002_02_1:ヒメヒナちほー2 3曲追加《キスキツネ、WWW、うたかたよいかないで》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26506
},
{
"id": 12000,
"name": "Intergalactic Bound",
"artistName": 1127,
"artistNameName": "Yunosuke / CircusP",
"genreName": 102,
"genreNameName": "niconicoボーカロイド",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016021,
"eventNameName": "261016_02_1:niconico2曲、バラエティ1曲追加《Intergalactic Bound、LEMON MELON COOKIE、3rd Avenue》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26507
},
{
"id": 12001,
"name": "LEMON MELON COOKIE",
"artistName": 1128,
"artistNameName": "TAK",
"genreName": 102,
"genreNameName": "niconicoボーカロイド",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016021,
"eventNameName": "261016_02_1:niconico2曲、バラエティ1曲追加《Intergalactic Bound、LEMON MELON COOKIE、3rd Avenue》",
"disable": false,
"lockType": 0,
"subLockType": 0,
"longMusic": 0,
"version": 26507
},
{
"id": 12002,
"name": "3rd Avenue",
"artistName": 1138,
"artistNameName": "Sound Souler",
"genreName": 104,
"genreNameName": "ゲームバラエティ",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016021,
"eventNameName": "261016_02_1:niconico2曲、バラエティ1曲追加《Intergalactic Bound、LEMON MELON COOKIE、3rd Avenue》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26507
},
{
"id": 12019,
"name": "Cryogenic",
"artistName": 1148,
"artistNameName": "Camellia ft. Petra Gurin「In Falsus」",
"genreName": 104,
"genreNameName": "ゲームバラエティ",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016022,
"eventNameName": "261016_02_2:バラエティ1曲追加《Cryogenic》",
"disable": false,
"lockType": 0,
"subLockType": 1,
"longMusic": 0,
"version": 26507
},
{
"id": 12024,
"name": "OV3RCLOCK",
"artistName": 1152,
"artistNameName": "Alicemetix",
"genreName": 105,
"genreNameName": "maimai",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261002024,
"eventNameName": "261002_02_4:伝導楽曲 1曲(Lock)",
"disable": false,
"lockType": 3,
"subLockType": 1,
"longMusic": 0,
"version": 26506
},
{
"id": 100018,
"name": "[協]Love You",
@@ -22949,6 +23085,23 @@
"longMusic": 0,
"version": 24015
},
{
"id": 111537,
"name": "[即]Straight into the lights",
"artistName": 890,
"artistNameName": "Cosmograph",
"genreName": 107,
"genreNameName": "宴会場",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016023,
"eventNameName": "261016_02_3:宴譜面追加3",
"disable": false,
"lockType": 0,
"subLockType": 0,
"longMusic": 0,
"version": 26508
},
{
"id": 111557,
"name": "[蛸]不機嫌なスリーカード",
@@ -23051,6 +23204,23 @@
"longMusic": 0,
"version": 25007
},
{
"id": 111765,
"name": "[音]愛包ダンスホール",
"artistName": 781,
"artistNameName": "HIMEHINA",
"genreName": 107,
"genreNameName": "宴会場",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261002022,
"eventNameName": "261002_02_2:宴譜面追加4",
"disable": false,
"lockType": 0,
"subLockType": 0,
"longMusic": 0,
"version": 26507
},
{
"id": 111772,
"name": "[宴]人マニア",
@@ -23102,6 +23272,23 @@
"longMusic": 0,
"version": 25502
},
{
"id": 112002,
"name": "[覚]3rd Avenue",
"artistName": 1138,
"artistNameName": "Sound Souler",
"genreName": 107,
"genreNameName": "宴会場",
"AddVersion": 26,
"AddVersionName": "CiRCLEPLUS",
"eventName": 261016023,
"eventNameName": "261016_02_3:宴譜面追加3",
"disable": false,
"lockType": 0,
"subLockType": 0,
"longMusic": 0,
"version": 26508
},
{
"id": 120227,
"name": "[r]Garakuta Doll Play",
+68 -24
View File
@@ -4,16 +4,20 @@
// 产物放在 data/ 而不是 assets/:目前没有代码在运行时读它,放 assets/ 会被
// 原样打进每个安装包(约 1.2MB)。要接进 App 时再一起挪进 pubspec 的 assets。
//
// 用法:
// 用法(两种根可以混用,后面的覆盖前面的同 ID 行):
// dart run tools/gen_id_mapping.dart --package G:/Package
// dart run tools/gen_id_mapping.dart --package G:/Package --data-root D:/A031OptDump/Root
//
// 数据布局:`<pkg>/Sinmai_Data/StreamingAssets/A0xx/<表名>/<前缀><dataName>/<表名>.xml`,
// 一个实体一个目录。真正的 ID 在 XML 的 `<name><id>` 里,目录名只是 dataName,
// 二者不相等(例如 `icon/icon550201/Icon.xml` 的 name.id 其实是 1)。
// `--package` 会自动去找 `<pkg>/Sinmai_Data/StreamingAssets/A0xx/`;
// `--data-root` 直接把给定目录当一个数据根(像 A031OptDump/Root 那种单独导出的包)。
//
// 多个 A0xx 目录要按名字升序合并、后者覆盖同 ID 的行——客户端 DataManager.LoadData
// 数据布局:`<root>/<表名>/<前缀><dataName>/<表名>.xml`,一个实体一个目录。
// 真正的 ID 在 XML 的 `<name><id>` 里,目录名只是 dataName(`icon/icon550201/Icon.xml`
// 的 name.id 其实是 1;`music/music111537` 的 name.id 是 111537,属宴曲段)。
//
// 多根合并按名字升序、后者覆盖同 ID 的行——客户端 DataManager.LoadData
// 就是这个语义(按 dirs 顺序遍历,`sortedDictionary[key] =` 直接覆写)。
// 只认形如 `A???` 的目录,其余(Table/、RomConfig.xml 等)跳过。
// `--package` 下只认形如 `A???` 的目录,其余(Table/、RomConfig.xml 等)跳过。
import 'dart:convert';
import 'dart:io';
@@ -90,33 +94,59 @@ const _keySpec = _TableSpec(
stringIds: ['name', 'gateName'],
);
/// 本地事件表只有名字与 `alwaysOpen`;**开关的日期窗口不在这里**,
/// 是服务器 `GetGameEventApi` 下发的 `GameEvent{startDate,endDate}`。
/// 所以这张表能用来看某个 eventName.id 属于什么活动,但判断「现在开没开」
/// 得去问服务器。
const _eventSpec = _TableSpec(
dirName: 'event',
xmlFileName: 'Event.xml',
scalars: ['infoType', 'alwaysOpen', 'disableArea'],
stringIds: ['name'],
);
void main(List<String> args) {
final packageRoot = _argValue(args, '--package') ?? 'G:/Package';
final outDir = Directory(
_argValue(args, '--out') ?? 'data/id_mapping',
);
final assetsRoot = Directory(
'$packageRoot/Sinmai_Data/StreamingAssets',
);
if (!assetsRoot.existsSync()) {
stderr.writeln('找不到 $assetsRoot');
exitCode = 1;
return;
}
final dataDirs = <String>[];
// 升序,让高编号的增补包覆盖基础包,和客户端一致。
final dataDirs =
// 整合包:先按 A??? 升序铺进来,让高编号的增补包覆盖基础包。
final packageRoot = _argValue(args, '--package');
if (packageRoot != null) {
final assetsRoot = Directory(
'${_norm(packageRoot)}/Sinmai_Data/StreamingAssets',
);
if (!assetsRoot.existsSync()) {
stderr.writeln('找不到 ${assetsRoot.path}');
exitCode = 1;
return;
}
dataDirs.addAll(
assetsRoot
.listSync()
.whereType<Directory>()
.where((d) => RegExp(r'^A\d{3}$').hasMatch(_baseName(d.path)))
.map((d) => d.path)
.map((d) => _norm(d.path))
.where((p) => RegExp(r'^A\d{3}$').hasMatch(_baseName(p)))
.toList()
..sort();
..sort(),
);
}
// 单独导出的数据根(如 D:/A031OptDump/Root),按传参顺序追加,后面的赢。
for (final extra in _argValues(args, '--data-root')) {
final dir = Directory(_norm(extra));
if (!dir.existsSync()) {
stderr.writeln('找不到数据根 ${dir.path}');
exitCode = 1;
return;
}
dataDirs.add(_norm(extra));
}
if (dataDirs.isEmpty) {
stderr.writeln('$assetsRoot 下没有 A??? 数据目录');
stderr.writeln('没有可用数据根,给 --package 或 --data-root');
exitCode = 1;
return;
}
@@ -128,6 +158,7 @@ void main(List<String> args) {
final gates = _mergeById(dataDirs, _gateSpec);
final courses = _mergeById(dataDirs, _courseSpec);
final keys = _mergeById(dataDirs, _keySpec);
final events = _mergeById(dataDirs, _eventSpec);
final items = <String, List<Map<String, dynamic>>>{};
for (final (kind, spec) in _itemTables) {
@@ -143,15 +174,16 @@ void main(List<String> args) {
'keyList': keys,
},
'items.json': items,
'events.json': events,
'meta.json': {
'generatedFrom': packageRoot,
'dataDirs': dataDirs.map(_baseName).toList(),
'dataDirs': dataDirs.map((p) => _baseName(p)).toList(),
'counts': {
'music': music.length,
'map': maps.length,
'gate': gates.length,
'course': courses.length,
'key': keys.length,
'event': events.length,
for (final (kind, _) in _itemTables)
'itemKind$kind': items['$kind']!.length,
},
@@ -310,4 +342,16 @@ String? _argValue(List<String> args, String name) {
return args[i + 1];
}
String _baseName(String path) => path.replaceAll(r'\', '/').split('/').last;
/// 可重复参数(`--data-root A --data-root B`)。
List<String> _argValues(List<String> args, String name) {
final out = <String>[];
for (var i = 0; i < args.length - 1; i++) {
if (args[i] == name) out.add(args[i + 1]);
}
return out;
}
/// Windows 传参常见反斜杠,统一成 slash 再拼路径。
String _norm(String path) => path.replaceAll(r'\', '/');
String _baseName(String path) => _norm(path).split('/').last;