feat: 从机台数据包抽出 music/item/map/gate 的 ID 映射表
风险页填的全是裸 ID,而「填了表里没有的 ID」是这类功能最常见的失败方式:
服务器照样回 returnCode: 1,客户端建池时静默跳过,看起来就是没生效。
- tools/gen_id_mapping.dart:读 Sinmai_Data/StreamingAssets/A0xx/<表>/<x>.xml,
按目录名升序合并、同 ID 后者覆盖(与 DataManager.LoadData 语义一致),
输出 data/id_mapping/{music,items,maps,gates,meta}.json。
ID 取 XML 的 <name><id>,不是目录名——目录名只是 dataName。
- 零依赖:自带一个只处理 XmlSerializer 那种规整结构的小扫描器,
不为此往依赖树里加 xml 包、也不搅动 pubspec.lock。
- 产物放 data/ 不放 assets/:目前没有运行时读取方,进 assets 会白白把
1.2MB 打进每个安装包。
用这份表顺手证实了三件事:musicId 11820 在曲表里根本不存在(11819、11821
也不在),所以它不是万花筒锁——lockType=4 的歌全表只有 6 首;角色 1001 同样
不存在,合法 ID 从 101 起;万花筒只有 gate 1~6,客户端 ForceAddMasterKey
写死的 gateId 7 在这份数据里没有对应行。
This commit is contained in:
1 parent
80df51c239
commit
264b1d0aa7
7 files changed
+51326
No files matched your search
@@ -48,6 +48,8 @@ lib/
|
||||
├── pages/ # 登录页与各功能页面
|
||||
├── services/ # 传输层、payload 组装、乐曲元数据、导出、RA 计算、调试日志
|
||||
└── widgets/ # 可复用视觉组件(B50 海报、卡片/提示、冷却 mixin、阶段进度卡)
|
||||
tools/ # 离线开发工具(不参与构建产物)
|
||||
data/id_mapping/ # 由 tools/gen_id_mapping.dart 从机台数据包抽出的 ID 映射表
|
||||
```
|
||||
|
||||
### 共享抽象
|
||||
@@ -159,6 +161,45 @@ lib/
|
||||
`MapTreasure.xml` 里(`DataManager.LoadMaps` / `LoadMapTresures` 从本地数据目录读,
|
||||
不经 title server),App 没有这份表,页面里也明写了这个限制。
|
||||
|
||||
## ID 映射表
|
||||
|
||||
风险页要填的全是裸 ID,而「填了机台表里没有的 ID」是这一类功能最常见的失败方式:
|
||||
服务器照样回 `returnCode: 1`,客户端却在建曲池/建角色池时静默跳过,看起来就是
|
||||
「上传成功但没生效」。为此把机台数据包里的表抽成可查的映射:
|
||||
|
||||
```bash
|
||||
dart run tools/gen_id_mapping.dart --package G:/Package
|
||||
```
|
||||
|
||||
产物落在 `data/id_mapping/`:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| `music.json` | musicId → 曲名 / 艺术家 / 流派 / `AddVersion` / `disable` / `lockType` / `subLockType` / `eventName` |
|
||||
| `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` |
|
||||
|
||||
要点:
|
||||
|
||||
- 真正的 ID 在 XML 的 `<name><id>` 里,**目录名只是 `dataName`,两者不相等**
|
||||
(`icon/icon550201/Icon.xml` 的 `name.id` 实际是 1)。
|
||||
- 数据包按 `Sinmai_Data/StreamingAssets/A0xx/` 分基础包与增补包,抽取时按目录名
|
||||
升序合并、同 ID 后者覆盖前者,与客户端 `DataManager.LoadData` 的语义一致。
|
||||
- 放在 `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` 都变不出来。
|
||||
- **角色(旅行伙伴)1001 不存在**,合法 ID 从 101 起,整表 983 条、上界到 605905。
|
||||
以前页面里「例如 1001」的提示正是这个坑。
|
||||
- **万花筒只有 gate 1~6**(蓝/白/紫/黑/黄/红,course 与 key 同为 1~6 一一对应)。
|
||||
客户端 `ForceAddMasterKey` 写死的 `gateId = 7`(万能钥匙门)在这份数据里没有对应行。
|
||||
|
||||
## 构建环境注意事项
|
||||
|
||||
以下问题在本仓库已确认,改动依赖或升级 Flutter 时请留意:
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"gateList": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "蓝色之门",
|
||||
"eventName": 25122432,
|
||||
"eventNameName": "251224_03_2:青のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "白色之门",
|
||||
"eventName": 26021032,
|
||||
"eventNameName": "260210_03_2:白のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 2
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "紫色之门",
|
||||
"eventName": 26032532,
|
||||
"eventNameName": "260325_03_2:紫のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 3
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "黑色之门",
|
||||
"eventName": 26042832,
|
||||
"eventNameName": "260428_03_2:黒のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 5
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "黃色之门",
|
||||
"eventName": 26061032,
|
||||
"eventNameName": "260610_03_2:黄のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 6
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "红色之门",
|
||||
"eventName": 26080532,
|
||||
"eventNameName": "260805_03_2:赤のカレイドスコープ解禁",
|
||||
"gateType": "Normal",
|
||||
"positionId": 4
|
||||
}
|
||||
],
|
||||
"courseList": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "青のカレイドスコープ",
|
||||
"gateName": 1,
|
||||
"gateNameName": "蓝色之门",
|
||||
"keyName": 1,
|
||||
"keyNameName": "蓝色钥匙"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "白のカレイドスコープ",
|
||||
"gateName": 2,
|
||||
"gateNameName": "白色之门",
|
||||
"keyName": 2,
|
||||
"keyNameName": "白色钥匙"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "紫のカレイドスコープ",
|
||||
"gateName": 3,
|
||||
"gateNameName": "紫色之门",
|
||||
"keyName": 3,
|
||||
"keyNameName": "紫色钥匙"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "黒のカレイドスコープ",
|
||||
"gateName": 4,
|
||||
"gateNameName": "黑色之门",
|
||||
"keyName": 4,
|
||||
"keyNameName": "黑色钥匙"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "黄のカレイドスコープ",
|
||||
"gateName": 5,
|
||||
"gateNameName": "黃色之门",
|
||||
"keyName": 5,
|
||||
"keyNameName": "黃色钥匙"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "赤のカレイドスコープ",
|
||||
"gateName": 6,
|
||||
"gateNameName": "红色之门",
|
||||
"keyName": 6,
|
||||
"keyNameName": "红色钥匙"
|
||||
}
|
||||
],
|
||||
"keyList": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "蓝色钥匙",
|
||||
"gateName": 1,
|
||||
"gateNameName": "蓝色之门"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "白色钥匙",
|
||||
"gateName": 2,
|
||||
"gateNameName": "白色之门"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "紫色钥匙",
|
||||
"gateName": 3,
|
||||
"gateNameName": "紫色之门"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "黑色钥匙",
|
||||
"gateName": 4,
|
||||
"gateNameName": "黑色之门"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "黃色钥匙",
|
||||
"gateName": 5,
|
||||
"gateNameName": "黃色之门"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "红色钥匙",
|
||||
"gateName": 6,
|
||||
"gateNameName": "红色之门"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"generatedFrom": "G:/Package",
|
||||
"dataDirs": [
|
||||
"A000",
|
||||
"A005",
|
||||
"A011",
|
||||
"A015"
|
||||
],
|
||||
"counts": {
|
||||
"music": 1367,
|
||||
"map": 168,
|
||||
"gate": 6,
|
||||
"course": 6,
|
||||
"key": 6,
|
||||
"itemKind1": 398,
|
||||
"itemKind2": 3000,
|
||||
"itemKind3": 1046,
|
||||
"itemKind9": 983,
|
||||
"itemKind10": 25,
|
||||
"itemKind11": 345,
|
||||
"itemKind12": 11
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,313 @@
|
||||
// 从机台数据包(G:\Package 这类整合包)里抽出 ID 映射表,生成
|
||||
// data/id_mapping/*.json,供风险页做 ID 校对与名称展示。
|
||||
//
|
||||
// 产物放在 data/ 而不是 assets/:目前没有代码在运行时读它,放 assets/ 会被
|
||||
// 原样打进每个安装包(约 1.2MB)。要接进 App 时再一起挪进 pubspec 的 assets。
|
||||
//
|
||||
// 用法:
|
||||
// dart run tools/gen_id_mapping.dart --package G:/Package
|
||||
//
|
||||
// 数据布局:`<pkg>/Sinmai_Data/StreamingAssets/A0xx/<表名>/<前缀><dataName>/<表名>.xml`,
|
||||
// 一个实体一个目录。真正的 ID 在 XML 的 `<name><id>` 里,目录名只是 dataName,
|
||||
// 二者不相等(例如 `icon/icon550201/Icon.xml` 的 name.id 其实是 1)。
|
||||
//
|
||||
// 多个 A0xx 目录要按名字升序合并、后者覆盖同 ID 的行——客户端 DataManager.LoadData
|
||||
// 就是这个语义(按 dirs 顺序遍历,`sortedDictionary[key] =` 直接覆写)。
|
||||
// 只认形如 `A???` 的目录,其余(Table/、RomConfig.xml 等)跳过。
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// 每张表的抽取规则。
|
||||
class _TableSpec {
|
||||
final String dirName;
|
||||
final String xmlFileName;
|
||||
|
||||
/// 要抽的标量字段(`<tag>value</tag>`)。
|
||||
final List<String> scalars;
|
||||
|
||||
/// 要抽的 StringID 字段(`<tag><id/><str/></tag>`),输出成 `<outKey>` 与 `<outKey>Name`。
|
||||
final List<String> stringIds;
|
||||
|
||||
/// 输出文件名里的表名。
|
||||
const _TableSpec({
|
||||
required this.dirName,
|
||||
required this.xmlFileName,
|
||||
this.scalars = const [],
|
||||
this.stringIds = const [],
|
||||
});
|
||||
}
|
||||
|
||||
/// `ItemKind`(`Net/VO/Mai2/ItemKind.cs`)到表目录的对应关系。
|
||||
/// 角色(9)与万花筒钥匙(15)不走 userItemList,但 ID 照样要能查,所以一并收。
|
||||
const List<(int itemKind, _TableSpec spec)> _itemTables = [
|
||||
(1, _TableSpec(dirName: 'plate', xmlFileName: 'Plate.xml', stringIds: ['name'])),
|
||||
(2, _TableSpec(dirName: 'title', xmlFileName: 'Title.xml', stringIds: ['name'])),
|
||||
(3, _TableSpec(dirName: 'icon', xmlFileName: 'Icon.xml', stringIds: ['name'])),
|
||||
(
|
||||
9,
|
||||
_TableSpec(dirName: 'chara', xmlFileName: 'Chara.xml', stringIds: ['name', 'genre']),
|
||||
),
|
||||
(10, _TableSpec(dirName: 'partner', xmlFileName: 'Partner.xml', stringIds: ['name'])),
|
||||
(11, _TableSpec(dirName: 'frame', xmlFileName: 'Frame.xml', stringIds: ['name'])),
|
||||
(12, _TableSpec(dirName: 'ticket', xmlFileName: 'Ticket.xml', stringIds: ['name'])),
|
||||
];
|
||||
|
||||
const _musicSpec = _TableSpec(
|
||||
dirName: 'music',
|
||||
xmlFileName: 'Music.xml',
|
||||
// lockType/subLockType 直接决定这首歌要不要靠 itemKind 5 解锁、
|
||||
// Re:Master 要不要额外条件(MusicLockType: Unlock=0 Lock=1 Challenge=2
|
||||
// Transmission=3 KaleidScope=4)。
|
||||
scalars: ['disable', 'lockType', 'subLockType', 'longMusic', 'version'],
|
||||
stringIds: ['name', 'artistName', 'genreName', 'AddVersion', 'eventName'],
|
||||
);
|
||||
|
||||
const _mapSpec = _TableSpec(
|
||||
dirName: 'map',
|
||||
xmlFileName: 'Map.xml',
|
||||
scalars: ['IsCollabo', 'IsInfinity'],
|
||||
stringIds: ['name', 'IslandId', 'OpenEventId', 'ColorId', 'BonusMusicId'],
|
||||
);
|
||||
|
||||
const _gateSpec = _TableSpec(
|
||||
dirName: 'kaleidxScopeGate',
|
||||
xmlFileName: 'KaleidxScopeGate.xml',
|
||||
scalars: ['gateType', 'positionId'],
|
||||
stringIds: ['name', 'eventName'],
|
||||
);
|
||||
|
||||
/// 课题表是 keyId <-> gateId 的唯一桥(`GetKaleidxScopeCourseByKeyId` 扫的就是它)。
|
||||
const _courseSpec = _TableSpec(
|
||||
dirName: 'kaleidxScopeCourse',
|
||||
xmlFileName: 'KaleidxScopeCourse.xml',
|
||||
stringIds: ['name', 'gateName', 'keyName'],
|
||||
);
|
||||
|
||||
const _keySpec = _TableSpec(
|
||||
dirName: 'kaleidxScopeKey',
|
||||
xmlFileName: 'KaleidxScopeKey.xml',
|
||||
stringIds: ['name', 'gateName'],
|
||||
);
|
||||
|
||||
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 =
|
||||
assetsRoot
|
||||
.listSync()
|
||||
.whereType<Directory>()
|
||||
.where((d) => RegExp(r'^A\d{3}$').hasMatch(_baseName(d.path)))
|
||||
.map((d) => d.path)
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
if (dataDirs.isEmpty) {
|
||||
stderr.writeln('$assetsRoot 下没有 A??? 数据目录');
|
||||
exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
outDir.createSync(recursive: true);
|
||||
|
||||
final music = _mergeById(dataDirs, _musicSpec);
|
||||
final maps = _mergeById(dataDirs, _mapSpec);
|
||||
final gates = _mergeById(dataDirs, _gateSpec);
|
||||
final courses = _mergeById(dataDirs, _courseSpec);
|
||||
final keys = _mergeById(dataDirs, _keySpec);
|
||||
|
||||
final items = <String, List<Map<String, dynamic>>>{};
|
||||
for (final (kind, spec) in _itemTables) {
|
||||
items['$kind'] = _mergeById(dataDirs, spec);
|
||||
}
|
||||
|
||||
final writes = <String, Object>{
|
||||
'music.json': music,
|
||||
'maps.json': maps,
|
||||
'gates.json': {
|
||||
'gateList': gates,
|
||||
'courseList': courses,
|
||||
'keyList': keys,
|
||||
},
|
||||
'items.json': items,
|
||||
'meta.json': {
|
||||
'generatedFrom': packageRoot,
|
||||
'dataDirs': dataDirs.map(_baseName).toList(),
|
||||
'counts': {
|
||||
'music': music.length,
|
||||
'map': maps.length,
|
||||
'gate': gates.length,
|
||||
'course': courses.length,
|
||||
'key': keys.length,
|
||||
for (final (kind, _) in _itemTables)
|
||||
'itemKind$kind': items['$kind']!.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (final entry in writes.entries) {
|
||||
final file = File('${outDir.path}/${entry.key}');
|
||||
file.writeAsStringSync(
|
||||
const JsonEncoder.withIndent(' ').convert(entry.value),
|
||||
);
|
||||
stdout.writeln('wrote ${file.path}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫描所有数据目录里该表的 XML,按 name.id 合并;同一 ID 后面出现的覆盖前面的。
|
||||
List<Map<String, dynamic>> _mergeById(
|
||||
List<String> dataDirs,
|
||||
_TableSpec spec,
|
||||
) {
|
||||
final byId = <int, Map<String, dynamic>>{};
|
||||
|
||||
for (final root in dataDirs) {
|
||||
final dir = Directory('$root/${spec.dirName}');
|
||||
if (!dir.existsSync()) continue;
|
||||
|
||||
for (final entity in dir
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((f) => _baseName(f.path) == spec.xmlFileName)) {
|
||||
final row = _extract(entity.readAsStringSync(), spec);
|
||||
final id = row['id'];
|
||||
if (id is int) byId[id] = row;
|
||||
}
|
||||
}
|
||||
|
||||
final ids = byId.keys.toList()..sort();
|
||||
return [for (final id in ids) byId[id]!];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _extract(String xml, _TableSpec spec) {
|
||||
final row = <String, dynamic>{};
|
||||
|
||||
final primary = _stringId(xml, 'name');
|
||||
if (primary == null) return row;
|
||||
row['id'] = primary.id;
|
||||
row['name'] = primary.str;
|
||||
|
||||
for (final tag in spec.stringIds) {
|
||||
if (tag == 'name') continue;
|
||||
final v = _stringId(xml, tag);
|
||||
if (v == null) continue;
|
||||
// ID 用来做关联与校验,str 只是给人看的,分开两个 key 存。
|
||||
row[tag] = v.id;
|
||||
row['${tag}Name'] = v.str;
|
||||
}
|
||||
|
||||
for (final tag in spec.scalars) {
|
||||
final v = _scalar(xml, tag);
|
||||
if (v == null) continue;
|
||||
row[tag] = _asTyped(v);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
class _StringId {
|
||||
final int id;
|
||||
final String str;
|
||||
const _StringId(this.id, this.str);
|
||||
}
|
||||
|
||||
/// 取 `<tag>...</tag>` 平衡块内的 `<id>` 与 `<str>`。
|
||||
_StringId? _stringId(String xml, String tag) {
|
||||
final block = _balancedBlock(xml, tag);
|
||||
if (block == null) return null;
|
||||
final idText = _scalar(block, 'id');
|
||||
if (idText == null) return null;
|
||||
final id = int.tryParse(idText);
|
||||
if (id == null) return null;
|
||||
return _StringId(id, _scalar(block, 'str') ?? '');
|
||||
}
|
||||
|
||||
/// 第一个 `<tag>` … `</tag>` 平衡块的内容;`<tag/>` 视为空。
|
||||
String? _balancedBlock(String xml, String tag) {
|
||||
final open = xml.indexOf('<$tag>');
|
||||
final selfClosed = xml.indexOf('<$tag ');
|
||||
final bareSelfClosed = xml.indexOf('<$tag/>');
|
||||
|
||||
int start;
|
||||
if (open >= 0 && (selfClosed < 0 || open < selfClosed)) {
|
||||
start = open;
|
||||
} else if (bareSelfClosed >= 0) {
|
||||
return '';
|
||||
} else if (selfClosed >= 0) {
|
||||
start = selfClosed;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
final closeTag = '</$tag>';
|
||||
var depth = 0;
|
||||
var i = start;
|
||||
while (i < xml.length) {
|
||||
final nextOpen = xml.indexOf('<$tag', i);
|
||||
final nextClose = xml.indexOf(closeTag, i);
|
||||
if (nextClose < 0) return null;
|
||||
|
||||
if (nextOpen >= 0 && nextOpen < nextClose) {
|
||||
// 只有确实是开标签才算嵌套(后面跟 > / 空格 / /)。
|
||||
final after = xml[nextOpen + tag.length + 1];
|
||||
if (after == '>' || after == ' ' || after == '/') depth++;
|
||||
i = nextOpen + tag.length + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth == 1 || depth == 0) {
|
||||
final contentStart = xml.indexOf('>', start) + 1;
|
||||
return xml.substring(contentStart.clamp(0, xml.length), nextClose);
|
||||
}
|
||||
depth--;
|
||||
i = nextClose + closeTag.length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// `<tag>value</tag>`,或 `<tag />` / `<tag/>` 当作空值忽略。
|
||||
String? _scalar(String xml, String tag) {
|
||||
final open = xml.indexOf('<$tag>');
|
||||
if (open < 0) return null;
|
||||
final end = xml.indexOf('</$tag>', open);
|
||||
if (end < 0) return null;
|
||||
final raw = xml.substring(open + tag.length + 2, end).trim();
|
||||
if (raw.isEmpty) return null;
|
||||
return _decodeEntities(raw);
|
||||
}
|
||||
|
||||
String _decodeEntities(String v) => v
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('&', '&');
|
||||
|
||||
/// `false`/`true` 转 bool,纯数字转 int,其余保留字符串。
|
||||
Object _asTyped(String v) {
|
||||
if (v == 'true') return true;
|
||||
if (v == 'false') return false;
|
||||
final n = int.tryParse(v);
|
||||
return n ?? v;
|
||||
}
|
||||
|
||||
String? _argValue(List<String> args, String name) {
|
||||
final i = args.indexOf(name);
|
||||
if (i < 0 || i + 1 >= args.length) return null;
|
||||
return args[i + 1];
|
||||
}
|
||||
|
||||
String _baseName(String path) => path.replaceAll(r'\', '/').split('/').last;
|
||||
Reference in new issue
Block a user