first commit

This commit is contained in:
2026-08-01 14:00:17 +02:00
commit 130f8b4c1d
60 changed files with 9324 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
# 问题分析:AttachTest 高亮异常与误报诊断
> 日期:2026-08-01。基于在 AttachTest `Data/Mod.xml` 上复现的问题。
> 本文件只做分析,**代码尚未修改**,修复方案见文末,待审阅后实施。
## 一、现象
1. **语法高亮异常**
- `<?xml version="1.0" encoding="UTF-8"?>` 显示为纯文本颜色;
- `<Includes` 之后的 `>` 无高亮;
- `<Include type="reference" ...>``Include` 之后的内容全部无高亮;
- 整体表现为"只有少量标签名被着色,其余按纯文本处理"。
2. **误报诊断**
- `<Include type="reference">``type` 值报 `Unresolved reference "reference"`
- `<Include source="DATA:Static.xml">``source` 值报 `Unresolved reference`
- 同一处 hover 显示解析结果为 `C:\Apps\RA3-MODSDK-X\SageXml\Static.xml`,但实际应按编译参数解析到 SDK 根目录 `C:\Apps\RA3-MODSDK-X\Static.xml`
## 二、实测证据
用当前构建代码对 AttachTest 索引后验证:
```
[resolve] DATA:Static.xml -> C:\Apps\RA3-MODSDK-X\Static.xml ← 解析器结果正确
[candidate] 精确匹配 -> C:\Apps\RA3-MODSDK-X\SageXml\Static.xml ← hover 用的是这条
[model] Include@type refType: null enum: [reference, instance, all]
[model] Include@source refType: null type: anyURI
[diag] Include@type='reference' targets: 0
```
三个问题均稳定复现。
## 三、根因分析
### 1. 高亮异常:grammar 注册方式覆盖了内置 XML 语法
`package.json` 中的 grammar 贡献写法:
```json
{
"language": "xml",
"scopeName": "source.ra3modxml",
"injectTo": ["source.xml"]
}
```
同时填写 `language``injectTo` 时,VS Code 会把该 grammar 注册为 **`xml` 语言的主 grammar**(替换内置 XML 语法),而不仅是注入。结果是:
- 内置 `source.xml` 的完整 token 化规则不再生效;
- 我的语法只有几个领域关键词模式,没有 XML 声明、标签括号、属性名/值的规则;
- 于是 `<?xml ...?>``>`、属性区全部退化为纯文本,只有 `$DEFINE``inheritFrom`、结构标签名等被我的模式着色——与现象完全吻合。
正确的做法是注入式 grammar 不声明 `language`(VS Code 官方注入语法示例即省略该字段)。
### 2. 误报 `Unresolved reference`:诊断把所有属性值都当成"引用"检查
诊断代码在 `checkValueReferences` 中先调用 `resolveReferenceTargets(...)`,并以其返回空数组作为"未解析"依据。而 `resolveReferenceTargets` 对**非引用属性**(如 `Include@type` 枚举、`Include@source``anyURI`)也返回空数组——两者无法区分。
这是早期实现回归:最初代码有 `isRef`(检查 `refType``inheritFrom`)守卫,重构为共享函数后丢失了该守卫。
同理,hover 对非引用属性值也会走到"未找到匹配定义"分支,显示误导性文案。
### 3. `DATA:Static.xml` hover 路径错误:候选表优先于解析器,且候选表大小写不统一
- 解析器 `resolveSource` 按搜索路径正确解析到 SDK 根 `Static.xml`
- 但 hover/定义跳转/文档链接都是**先查 `idx.sourceCandidates` 精确匹配**
- 候选表里同一 source 字符串 `DATA:Static.xml` 出现两条:一条来自 `SageXml\Static.xml`(扫描更早、排在前面),一条来自 SDK 根目录浅扫描;
- `find` 精确匹配返回第一条 → 显示 SageXml 路径,而按 BAB 顺序应先命中 SDK 根目录。
修复方向相应调整为:**候选表只服务补全**;hover/跳转/链接一律先走 `resolveSource`(按 BAB 顺序校验文件存在)。候选表本身也按 source 去重并让 SDK 根目录条目排前。
另外,当前搜索路径来自 `check_duplicate_ids.py` 的简版(`[SDK根, 项目Data, Mods, SDK/SageXml]`),与 BAB 编译参数不完全一致,缺少 `modGranParent``SDK\Mods` 两个条目。`defaultscript.cs``getIncludePaths()` 实际参数为:
```
/data: ".;{modGranParent};{0}\Data;.\Mods;{1};.\SageXml"
/art: ".;{modGranParent};{0}\Art1;{0}\Art;.\Mods;{1};.\Art"
/audio: ".;{modGranParent};{0}\Audio1;{0}\Audio;.\Mods;{1};.\Audio"
其中 {0}=ModPath, {1}=modParentPath, {2}=modGranParent, "." = SDK 根目录(编译时 cd 到 SDK)
```
展开后(以 AttachTest 为例):
| 前缀 | 搜索路径(BAB 顺序) |
|---|---|
| DATA | SDK根 → modGranParent → Mod\Data → SDK\Mods → modParentPath → SDK\SageXml |
| ART | SDK根 → modGranParent → Mod\Art1 → Mod\Art → SDK\Mods → modParentPath → SDK\Art |
| AUDIO | SDK根 → modGranParent → Mod\Audio1 → Mod\Audio → SDK\Mods → modParentPath → SDK\Audio |
`DATA:Static.xml` 在 BAB 顺序下首先命中 SDK 根的占位文件(与用户预期一致)。
## 四、修复方案
### 4.1 语法高亮(P0
- `package.json``grammars` 条目**移除 `"language": "xml"`**,只保留 `scopeName``path``injectTo`,使其成为纯粹的注入语法,叠加在内置 XML 语法之上。
- 精简 `syntaxes/ra3modxml.tmLanguage.json`:保留不冲突的模式(`$DEFINE` 常量、`inheritFrom``xai:joinAction`、TODO 注释、结构标签名);删除对 `Replace`/`Remove` 的裸词匹配(避免误染普通内容)。
- 验证方式:扩展开发宿主中打开 `Data/Mod.xml`,确认 XML 声明、标签、属性恢复完整高亮且领域关键词仍有专属颜色。
### 4.2 诊断与 hover 的引用判断(P0)
- 恢复"是否引用属性"守卫:仅当属性为 `inheritFrom` 或模型中有 `refType` 时才进入未解析引用检查;`Include@type``Include@source` 等不再误报。
- hover 同步加守卫:非引用属性值不再显示"未找到匹配定义"文案;`Include@source` 走专门的"Include 源文件"分支。
### 4.3 Include 源解析(P0
- hover / 定义跳转 / 文档链接统一改为**优先调用 `resolveSource`**(按 BAB 顺序、校验文件存在),候选表只作为兜底与补全来源。
- `buildSearchPaths` 对齐 BAB 编译参数:DATA/ART/AUDIO 各补上 `modGranParent``SDK\Mods` 两个搜索条目,顺序与 `defaultscript.cs` 一致。
- 候选表去重改为大小写不敏感(按 `source.toLowerCase()` 去重),避免同一文件因大小写不同出现两条候选。
### 4.4 测试补充
- 新增/更新单测:
- 非引用属性(`Include@type``Include@source`)不会产生 unresolved-reference 目标;
- `DATA:Static.xml` 按 BAB 顺序解析到 SDK 根目录而非 SageXml
- `buildSearchPaths` 展开结果与 BAB 参数一致(构造 SDK 内/外两种项目布局断言顺序)。
- 回归验证:AttachTest / GenEvoTest / Corona 各索引一次,确认 0 误报、补全/跳转正常。
### 4.5 不在本次范围
- P1 搜索与索引复用设计;
- manifest 深度解析(资产类型不匹配提示的进一步细化)。
## 五、风险与影响
- grammar 改动只影响高亮层,不影响索引与诊断逻辑;
- 搜索路径顺序变化可能影响个别 include 的解析结果(更接近 BAB 真实行为),需在三个真实项目上回归;
- 候选表去重策略变化理论上减少补全条目重复,不影响条目正确性。
## 六、实施结果(2026-08-01
已按上述方案完成修复并验证:
1. **grammar 改为纯注入**`package.json``grammars` 条目移除 `"language": "xml"`,内置 XML 语法恢复为主语法,领域关键词以注入方式叠加。
2. **引用属性守卫恢复**:新增 `isReferenceAttribute()` 纯函数,诊断与 hover 仅对 `inheritFrom` / 带 `refType` 的属性做未解析引用检查;`Include@type``Include@source` 不再误报。
3. **Include 源解析优先 `resolveSource`**:hover / 定义跳转 / 文档链接统一先按 BAB 顺序解析;候选表仅作补全来源并按 source 去重、SDK 根目录条目排前。
4. **搜索路径对齐 BAB 参数**`buildSearchPaths` 的 DATA/ART/AUDIO 各补入 `modGranParent``SDK\Mods`,顺序与 `defaultscript.cs``/data /art /audio` 一致。
实测(AttachTestC: 盘):
```
DATA:Static.xml resolves to: C:\Apps\RA3-MODSDK-X\Static.xml (OK,不再指向 SageXml)
DATA:static.xml candidates: 1 -> C:\Apps\RA3-MODSDK-X\Static.xml (去重生效)
isReferenceAttribute(Include,type) = false
isReferenceAttribute(Include,source) = false
CommandSet 引用仍能解析到 LogicCommandSet 定义(无回归)
```
单元测试 24/24 通过(新增:BAB 搜索路径顺序、SDK 根优先于 SageXml、`isReferenceAttribute` 判定)。
> 注:本环境当前无法访问 `D:\Mods\CoronaMod`D: 盘不可见),GenEvoTest / Corona 的实机回归待 D: 盘可用后补跑;相关逻辑已被 AttachTest 与单元测试覆盖。
---
## 七、问题分析(第二轮,2026-08-01)
用户在 AttachTest `Guardian Tank\GameObject.xml` 上报了三个新问题,均已修复并验证。
### 问题 A`Side="Allies"` 误报"未被定义"
**现象**`Side="Allies"` 报 unresolved reference,但 `global.manifest` 中存在 `PlayerTemplate:Allies`
**根因**`Side` 属性确实是引用(`PlayerTemplateWeakRef``PlayerTemplate`),但 manifest 解析器依赖 OpenSAGE `AssetType.cs` 的 TypeId 哈希表,而该表**不完整**(实测 `Global.manifest` 11268 个资产中有 2707 个哈希未知,`PlayerTemplate` 即缺失)。未知类型被记成 `#哈希`,无法与 `refType=PlayerTemplate` 匹配。
**修复**manifest 资产名本身是 `类型名:ID` 格式(如 `PlayerTemplate:Allies`),新增 `deriveAssetType()`——哈希已知时用哈希;未知时从名称前缀推导类型(资产 ID 不允许含冒号,切分无歧义)。修复后 `Side="Allies"` 正确解析到 `PlayerTemplate@Global.manifest`
### 问题 B`<Weapon>` 报没有 `Ordering` 属性
**现象**`WeaponSetUpdate → WeaponSlotTurret → Weapon` 链中 `<Weapon Ordering=...>` 报未知属性;XSD 里 `WeaponSlot_WeaponData` 确实有 `Ordering`
**根因**:模型用"元素名→类型"的**全局单映射**,同名元素(如 `<Weapon>` 出现在武器槽、慢速死亡、引用等许多上下文)以"先到先得"注册。`Weapon` 被注册成 `WeaponRef`,因此按全局映射查不到 `Ordering`。这是典型的**上下文相关类型**问题。
**修复**:新增上下文感知类型解析:
- `childTypeOf(父类型, 子元素名)` / `elementTypeIn(父元素, 子元素名)` / `attributesOfType(类型)`
- `resolveElementType(元素)` 沿解析树向上逐层用父类型的子元素声明解析真实类型,失败时回退全局映射。
修复后 `Weapon`(在 `WeaponSlotTurret` 下)解析为 `WeaponSlot_WeaponData``Ordering` 合法;且 `Ordering` 本身不是引用,不再被误判。
### 问题 C:嵌套 `xi:include`(第 248 行)未处理
**现象**`<xi:include href="DATA:Includes/HeadlightDraw2.xml" xpointer="...">` 位于元素内部(非 `AssetDeclaration` 直接子级),原实现只处理根级 `xi:include`,嵌套的一律静默忽略。
**根因**:索引器 `walk()` 只遍历根的直接子元素处理 `xi:include`
**修复**:解析后遍历文档全部元素,对**任意层级**的 `xi:include`:解析 `href`(按 BAB 搜索路径);目标缺失时产生 `include-not-found` 诊断(不再静默);目标存在时记录并 walk 目标文件,使其内容/资产进入索引。`href` 的文档链接(Ctrl+点击)此前已可用。
### 举一反三的测试
- `test/typeContext.test.mjs`:用真实结构(GameObject → BehaviorModules → WeaponSetUpdate → WeaponSlotTurret → Weapon)验证上下文类型解析;`childTypeOf` 原语断言。
- `test/manifestTypes.test.mjs``deriveAssetType` 前缀推导(含无冒号/前导冒号边界);模拟 `PlayerTemplate:Allies``Side` 引用解析成功;`isReferenceAttributeOfType` 判定。
- `test/indexer.test.mjs`:新增嵌套 `xi:include` 断言——目标文件被索引、目标资产可用、缺失目标产生诊断。
- AttachTest 实机验证:`Side="Allies"``PlayerTemplate@Global.manifest``Weapon` 类型 → `WeaponSlot_WeaponData`(含 `Ordering`);`HeadlightDraw2.xml` 进入索引、无相关诊断。
单元测试 28/28 通过,`.vsix` 已重新打包。
---
## 八、问题分析(第三轮,2026-08-01)
用户在 AttachTest `Guardian Tank\GameObject.xml` 上报了三个问题,均已修复并验证。
### 问题 A`Locomotor="..."` 误报 "has no definition of type true"
**现象**`LocomotorSet/@Locomotor` 引用报错,且类型显示为 **`true`**。
**根因**:模型生成器把简单类型的 `xas:isRef="true"` 当成了 `refType` 值。`Locomotor` 的类型是 `AssetReference`(只有 `xas:isRef="true"`、没有 `refType`),于是 refType 被写成字符串 `"true"`,导致任何类型匹配都失败。这是生成器 `refType` 取值逻辑的 bug。
**修复**
- 生成器只从 `xas:refType` 取 refType`xas:isRef` 只作为布尔标记 `isRef`(属性条目新增该字段);
- `isReferenceAttributeOfType``refType``isRef` 都视为引用;
- `resolveReferenceTargetsForType` 对**无类型引用**isRef 且无 refType)不做类型过滤,直接匹配同名 id——正是 `Locomotor` 这类"引用任意已声明资产"的语义。
修复后 `Locomotor="AlliedAntiVehicleVehicleTech1Locomotor"` 正确解析到 mod 的 `LocomotorTemplate`(以及 static.manifest 中的同名原版资产)。
### 问题 B`Name="AUAntiVehicleVehicleTech1_SKN"` 误报 BaseRenderAssetType 未定义
**现象**W3D 模型名引用报"no definition of type BaseRenderAssetType",但资产确实存在于 static.manifest。
**根因**:两个叠加原因:
1. manifest 名格式是 `类型:子类型:文件名`(如 `W3dContainer:W3DContainer:AUANTIVEHICLEVEHICLETECH1_SKN`),旧的 id 提取只切掉第一个前缀,得到 `W3DContainer:AUANTIVEHICLEVEHICLETECH1_SKN`,无法匹配 XML 里的 `AUAntiVehicleVehicleTech1_SKN`
2. manifest 哈希表里的类型名是 `W3dContainer`(小写 d),而 XSD 类型名是 `W3DContainer`,大小写不一致导致继承链匹配失败。
**修复**
- `deriveAssetId` 改为取**最后一个冒号段**(资产 ID 不允许含冒号),兼容 `PlayerTemplate:Allies``W3dContainer:W3DContainer:...` 两种格式;
- `canonicalTypeName` 大小写不敏感规范化,`typeChain` / `isAssignableTo` / 属性查询统一走规范化;manifest 资产入库时类型名也规范化。
类型匹配严格遵循 XSD 继承链:`W3DContainer` / `W3DMesh``BaseRenderAssetType` ✓;`W3DHierarchy``BaseAssetType`**不是** BaseRenderAssetType)✗。实测 `_SKN` 只解析出 `W3DContainer@Static.manifest` 一个候选,碰撞盒(id 带 `.OBBOX` 后缀)与层级自然排除。
### 问题 C:跳转候选"只能打开文件、不能精确定位"
**现象**`Template="AlliedAntiVehicleVehicleTech1Cannon"` 有两个候选——mod 定义和 `SageXml\globaldata\weapon.xml`(由 manifest 的 `manifestSource` 映射而来)。后者只打开文件、不定位。
**修复**
- 所有 XML 定义的跳转改为**精确范围**:用 `id` 属性值的起止偏移构造 Range(找不到时回退到元素起始标签),mod 与 SageXml 一致;
- manifest 定义映射到源码文件时,同样在源码文件内查找同名 id 并精确定位(实测定位到 `weapon.xml:2357`);
- 新增配置 `ra3modxml.definitionMode``all`(默认,mod + 原版全部列出,mod 优先)或 `project-only`(项目内已定义时只跳 mod 定义)。
### 举一反三的测试(31/31 通过)
- `test/manifestTypes.test.mjs``deriveAssetId` 最后一段提取(含 W3D/纹理双前缀、无冒号边界);大小写规范化后的 `isAssignableTo``W3dContainer``BaseRenderAssetType` 为真、`W3dHierarchy` 为假)。
- `test/refs.test.mjs`:无类型引用(`LocomotorSet/@Locomotor`)解析到任意声明类型的同名 id;非引用属性仍返回空。
- AttachTest 实机:三个原始场景全部按预期(Locomotor 命中 LocomotorTemplate、_SKN 命中 W3DContainer、cannon 双候选均可精确定位)。
`.vsix` 已重新打包(13:33)。D 盘恢复后照旧可补跑 GenEvoTest / Corona 回归。
+144
View File
@@ -0,0 +1,144 @@
# 调研结论与实施计划(已按最新代码同步更新)
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-01)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、无类型引用、精确跳转范围、嵌套 `xi:include`、注入式语法高亮等。
## 一、调研结论(带证据)
### 1. Include 解析规则(证据:`check_duplicate_ids.py` + `defaultscript.cs`
`Data/Mod.xml` 递归处理 `<Include type="all">`(以及任意层级的 `xi:include`)。路径解析按 `defaultscript.cs` `getIncludePaths()` 的编译参数(`/data /art /audio``.` 为 SDK 根目录):
- `DATA:`SDK 根 → modGranParent → 项目 `Data``SDK\Mods` → modParentPath → `SDK\SageXml`
- `ART:`SDK 根 → modGranParent → 项目 `Art1` → 项目 `Art``SDK\Mods` → modParentPath → `SDK\Art`
- `AUDIO:`SDK 根 → modGranParent → 项目 `Audio1` → 项目 `Audio``SDK\Mods` → modParentPath → `SDK\Audio`
其中 modParentPath = 项目目录的父目录,modGranParent = modParentPath 的父目录(如 AttachTest 位于 SDK 内时二者都收敛到 SDK 目录)。无前缀路径相对于当前文件目录解析;`ART:` 支持 2 字母前缀匹配。`defaultscript.cs` 第 4 步“建立全局数据”按 `additionalmaps\mapmetadata_*.xml` 逐个编译;第 5 步“建立基础数据”编译 `Data/Mod.xml`
### 2. 文件规模(证据:实测统计)
- Corona `Data`**7540 个 XML / 38.1 MB**
- SDK `SageXml`**5976 个 XML / 21.4 MB**
- 合计约 1.3 万个文件、60 MB → 索引必须后台化、缓存解析结果、按需惰性加载。
### 3. XSD 结构(证据:`Schemas/xsd` 实测)
-**821 个 XSD / 1.5 MB**,入口 `CnC3Types.xsd`
- 根元素 `AssetDeclaration``Tags` / `Includes` / `Defines` + **295 个顶层资产元素**(含内联声明)。
- 每个元素名对应一个 `complexType`,子元素用 `xs:sequence` / `xs:choice` 定义,属性用 `xs:attribute` 定义;复杂类型通过 `xs:extension` 继承(如 `BaseInheritableAsset` 提供 `inheritFrom`)。
- `Includes/Ref.xsd` 定义了大量带 `xas:refType="<资产类型>"` 的引用类型(如 `CommandSet` 引用 `LogicCommandSet`)→ 补全/导航按引用类型过滤的依据。
- `XmlEdit:Default` 提供默认值;`xs:enumeration` 提供枚举值;`xas:isRef="true"``refType` 时为**无类型引用**(匹配任意已声明资产)。
- **同名元素在不同父节点下类型不同**(如 `<Weapon>` 在武器槽下是 `WeaponSlot_WeaponData`,在别处可能是 `WeaponRef`)→ 需要上下文感知解析。
### 4. 领域特有约定
- `$NAME` / `=$NAME``<Defines>` 中定义的常量引用(Corona `GlobalData/GlobalDefines.xml` 实测),属性值补全与 hover 支持。
- `xai:joinAction` 实测取值 `Replace``Remove`
- 原版数据双来源:SDK `SageXml` 提供 XML 源码;`reference` 指向的编译 manifest 提供完整资产表(含美术素材)。
### 5. 网络与工具链(证据:本机实测)
- Node v24.16.0、npm 11.13.0、VS Code 1.130 可用;无全局 `vsce`、Python 不可用。
- npm 注册表可用(需提升权限安装依赖);esbuild 原生二进制在沙箱内受限,构建需提升权限。
### 6. manifest 二进制格式(证据:工作区 `OpenSAGE/` 源码)
用户已克隆 OpenSAGE 并切到 `d45d361``src/OpenSage.Game/Data/StreamFS/ManifestFile.cs` 给出完整格式:
- 头部:首 4 字节为 0 时版本固定为 7;否则 `IsBigEndian`(1B) + `IsLinked`(1B) + `Version`(u165/6) + 10 个 u32 缓冲区/计数信息。
- 资产条目(版本 ≥ 6 时每条 48B):`TypeId`/`InstanceId`/`TypeHash`/`InstanceHash`/`AssetReferenceOffset`/`AssetReferenceCount`/`NameOffset`/`SourceFileNameOffset`/`InstanceDataSize`/`RelocationDataSize`/`ImportsDataSize`
- 之后依次是:资产引用缓冲区、被引用 manifest 名缓冲区、资产名字符串缓冲区、源文件名字符串缓冲区。
- `TypeId` 为哈希;OpenSAGE `AssetType.cs` 枚举提供部分哈希→类型名映射(**不完整**:`Global.manifest` 11268 个资产中 2707 个哈希未知,如 `PlayerTemplate`)。
- **实测发现**manifest 资产名是 `类型名:ID` 格式,美术资产还带子类型段(`W3dContainer:W3DContainer:AUANTIVEHICLEVEHICLETECH1_SKN`)。类型可从第一个冒号段推导,可引用 ID 取最后一个冒号段。
**结论**manifest 解析为 P0 能力,用于 reference include 的资产补全/悬停/导航/诊断。
## 二、技术方案
### 选型
- **TypeScript + VS Code 扩展 API(非 LSP**:补全 / hover / 跳转 / 诊断 / 大纲均用原生 provider,无需语言服务器进程。
- **核心与编辑器解耦**`language/``model/``indexer/` 为纯 TS 模块(不 import `vscode`),可单测与复用(呼应 P1 需求 7)。
- **esbuild 打包**,产物 `dist/extension.js`
- **XSD → JSON 模型**:开发期工具 `tools/xsd-to-model.mjs` 把 821 个 XSD 解析成 `schema-model.json`(元素树、属性、文档、枚举、引用类型映射),随插件发布;运行时不再解析 XSD。
- **运行时 XML 解析**:自研带源码偏移的轻量解析器 `language/xmlParser.ts`(标签/属性/值均记录起止偏移,容错解析以支持输入中的补全与诊断)。`fast-xml-parser` 仅用于开发期 XSD 生成。
- **AssetType 哈希表**`tools/extract-asset-types.mjs` 从 OpenSAGE `AssetType.cs` 提取 `asset-types.json`;哈希未知时以 manifest 名称前缀推导类型。
### 架构
```
src/
extension.ts 激活入口(provider 注册、索引调度、诊断调度)
workspace.ts 项目检测(Data/Mod.xml / mod.babproj)、索引生命周期、状态栏、重建防抖
settings.ts 配置读取(sdkPath、indexSageXml、definitionMode 等)
language/
xmlParser.ts 带源码偏移的轻量 XML 解析器(格式错误定位、容错)
context.ts 补全上下文分析(元素名/属性名/属性值/内容)
typeContext.ts 上下文感知元素类型解析(resolveElementType 沿解析树逐层解析)
model/
schemaModel.ts schema-model.json 的类型/属性/子元素查询 + 类型名规范化(纯 TS)
schema-model.json 由 tools/xsd-to-model.mjs 生成
asset-types.json 由 tools/extract-asset-types.mjs 生成(TypeId 哈希→类型名)
indexer/
includeResolver.ts Include 路径解析(纯 TSBAB /data /art /audio 顺序)
manifestParser.ts .manifest 二进制解析 + 类型/ID 推导(纯 TS)
fileScanner.ts 目录遍历缓存 + Include source 候选收集
refs.ts 引用目标解析(按 refType / isRef / inheritFrom 过滤,纯 TS
indexer.ts 工作区索引器(资产/Define/流/manifest 合并,LRU 解析缓存)
types.ts 共享类型
features/
completion.ts 补全 provider(元素/属性/值,上下文感知)
hover.ts hover provider
navigation.ts 定义/引用/文档链接/大纲
diagnostics.ts 实时诊断
syntaxes/
ra3modxml.tmLanguage.json 注入 source.xml 的领域高亮(纯注入,不替换 XML 主语法)
tools/
xsd-to-model.mjs XSD → schema-model.json
extract-asset-types.mjs OpenSAGE AssetType.cs → asset-types.json
test/
fixtures/minimod 样例 Modinclude 各种情形、同名 ID、嵌套 xi:include、manifest 回退)
*.test.mjs 8 个测试文件(xmlParser / includeResolver / manifestParser /
indexer / schemaModel / refs / typeContext / manifestTypes
```
### 关键设计决策
1. **语言激活范围**:不劫持 `*.xml`。通过 `workspaceContains:**/Data/Mod.xml``**/*.babproj` 激活;语法高亮为**纯注入** grammar(不声明 `language`,避免覆盖内置 XML 语法)。
2. **索引范围与默认值**:索引“项目 Data + additionalmaps + 沿 include 可达的 SageXml 原版源码”;SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(可配置)。`reference` include 解析为 `builtmods` 下对应 manifest(惰性解析、按文件缓存),manifest 缺失/无效时回退到占位 XML。
3. **manifest 资产建模**:类型优先用哈希表,未知时从名称前缀推导;可引用 ID 取最后冒号段;类型名统一走大小写规范化(`W3dContainer``W3DContainer`),类型匹配严格遵循 XSD 继承链。
4. **上下文感知元素类型**:同名元素按父元素类型解析(`resolveElementType` 沿解析树逐层 `childTypeOf`,失败回退全局映射),保证 `<Weapon>` 等元素的属性/引用判定正确。
5. **引用判定与解析**`refType``isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);无类型引用匹配任意声明 ID;`inheritFrom` 按可继承类型过滤。
6. **重复 ID 诊断**:与 `check_duplicate_ids.py` 一致——SageXml 不参与冲突判定,mod 覆盖原版视为正常。
7. **未解析引用诊断**:按设置严重级别报告(默认 warning);类型不匹配时给出明确文案("有同名 ID 但类型不匹配")。`definitionMode` 设置控制跳转候选:`all`(mod + 原版全部列出,mod 优先)或 `project-only`
8. **跳转精度**XML 定义跳转到 `id` 属性值的精确 Range;manifest 定义映射到源码文件(如 SageXml)时也在文件内精确定位;找不到再回退到记录行。
9. **嵌套 `xi:include`**:任意层级处理——目标缺失产生诊断、目标存在则纳入索引;根级 `xpointer` 容器内容按顶层资产索引。
10. **性能**:索引在后台执行;解析结果 LRU 缓存(约 64 个文档);文件保存后防抖全量重建(1.5s),重建期间的新请求标记脏并在完成后重跑;状态栏显示进度与统计。
## 三、实施步骤
1. [x] 调研(Include 规则、XSD、示例项目、规模、工具链)——见本文档第一部分。
2. [x] 脚手架:`package.json``tsconfig.json`、esbuild、`.vscodeignore`、README。
3. [x] 生成模型:`schema-model.json`XSD295 顶层元素 / 1851 类型)与 `asset-types.json`79 个 AssetType 哈希)。
4. [x] 纯 TS 核心:include 解析、manifest 解析、XML 解析封装、索引器、引用解析。
5. [x] 功能层:补全、hover、导航、诊断、大纲、高亮 grammar。
6. [x] 单测(fixture Mod31 个用例全绿)+ 编译 + `vsce package` 打包(ra3-mod-xml-0.1.0.vsix,约 259KB)。
7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 三轮分析)。
## 四、验证结果(实测)
| 项目 | 规模 | 索引耗时 | 资产数 | 说明 |
|---|---|---|---|---|
| AttachTest | 71 文件 | ~0.6s | 35,546manifest 35,322 | 2 个流(static + mapmetadata |
| GenEvoTest | 24 文件 | ~1.8s | 35,392manifest 35,322 | 项目 IDalliedmcv 等)正确收录 |
| Corona | 3,448 文件(+ 非 XML 资产路径) | ~54.5s | 55,305manifest 35,322 | 3 个流、183 个 Define、0 诊断 |
单元测试覆盖:XML 解析(自闭合/容错/偏移)、include 解析(BAB 顺序、SDK 根优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、`childTypeOf`、大小写规范化)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、无类型引用、`Side="Allies"` 命中 manifest 的 `PlayerTemplate`)。
> 注:`D:\Mods\CoronaMod` 位于移动硬盘,当前未连接;GenEvoTest / Corona 的回归需在 D: 盘可用时补跑(用户会另行通知)。
## 五、假设与开放问题
- 假设 SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(与 prompts 一致),可在设置中修改。
- 假设补全/导航以“文本语义分析”为主,不做完整 XSD 校验(BAB 才是权威校验器)。
- 开放:是否发布到 VS Code Marketplace(需要 publisher)——本期先保证本地 `vsce package` 可安装。
- 开放:嵌套 `xi:include` 内容的"虚拟合并"进父文档(用于父文档内的补全/诊断感知被内联内容)——当前仅保证目标文件可索引、可导航、缺失可诊断。
+91
View File
@@ -0,0 +1,91 @@
# RA3 Mod XML VSCode 插件 — 情况描述与需求清单
> 由 `prompts` 中的零散描述整理而成。目标产物:一个用于编辑《命令与征服:红色警戒 3》Mod XML 文件的 VS Code 扩展。
## 一、情况描述(整理后)
红警 3 的 Mod 数据以 XML 文件组织,由 EA 的 BinaryAssetBuilderBAB)编译。一个 Mod 项目通常包含两类数据:
1. **基础数据(Static Data**:单位、武器、建筑、贴图、音频、AI、UI 等绝大多数内容。入口文件是 `Data/Mod.xml`
2. **全局数据(Global Data**:无法放进基础数据里的全局配置(游戏设置、全局单例等)。入口是 `Data/additionalmaps/mapmetadata_*.xml` 这类文件——它们原本是 EA 的地图元数据文件,但因为加载最早且允许写任意标签,被 modder 用作“全局数据”入口。
XML 之间的组织靠 `<Include>` 标签,共有三种语义:
| type | 语义 | 说明 |
|---|---|---|
| `reference` | 引用预编译 manifest | 不展开内容。典型用法是 `DATA:static.xml` / `DATA:global.xml` / `DATA:audio.xml` 这三个占位文件,实际对应 SDK `builtmods` 里已编译的 `static.manifest` / `global.manifest` / `audio.manifest` |
| `instance` | 源码级可见 | 不展开进当前文件,但编译时“看得到”目标源码;典型用于 `inheritFrom` 继承 |
| `all` | 内容合并 | 等价于把目标文件内容直接复制进来;`Mod.xml` 通过它递归聚合整个 Mod |
路径解析规则(来自现有工具 `check_duplicate_ids.py` 与编译脚本 `defaultscript.cs`):
- 带前缀 `DATA:` / `ART:` / `AUDIO:` 的路径按固定搜索顺序查找;
- 无前缀的路径相对于当前文件所在目录;
- `ART:` 路径支持“文件名前两个小写字母作为子目录”的匹配(如 `JUAntiShip``ju/JUAntiShip`)。
继承机制:`inheritFrom` 让一个元素默认获得目标元素的所有内容;具体合并行为由 `xai:joinAction``uri:ea.com:eala:asset:instance` 命名空间)控制,实际项目中出现的取值为 `Replace``Remove`
全部 XML 语法由 XSD 定义:SDK 自带 `Schemas/xsd/CnC3Types.xsd`(及其 800+ 个子 XSD)。大型 Mod 项目(如 Corona)还会携带自己修改过的 XSD 副本。
## 二、需求清单
### P0:近期核心功能
1. **语法高亮**:为 RA3 Mod XML 提供可读的高亮;重点补充普通 XML 高亮之外的领域标记(如 `$DEFINE` 常量引用、`inheritFrom` 等)。
2. **自动补全**
- 元素名(按当前父元素的 XSD 定义补全,含顶层资产元素);
- 属性名(按当前元素的 XSD 定义补全,`id` 必填者优先);
- 属性值:
- 引用型属性(XSD 中带 `xas:refType`)补全已定义的资产 ID
- `inheritFrom` 补全可继承的资产 ID
- 枚举值(XSD `xs:enumeration`);
- `$DEFINE` 常量(如 `$CIV_HEALTH_SMALL`);
- `<Include source>` 补全可解析的文件路径(`DATA:` / `ART:` / `AUDIO:`)。
3. **引用提示(Hover**:元素/属性悬停显示 XSD 文档、类型、默认值;资产 ID 悬停显示定义位置;`$DEFINE` 悬停显示值与定义位置。
4. **引用导航**
- 从引用型属性值跳转到对应资产定义(Go to Definition);
- 查找某资产 ID 的所有引用(Find All References);
- `<Include source>` / `xi:include href` 直接打开目标文件;
- `inheritFrom` 跳转到被继承元素;
- 文档大纲:显示文件内的顶层资产元素。
5. **错误检查(实时诊断)**
- XML 格式错误(well-formedness);
- 未知元素 / 未知属性(相对 XSD 模型);
- 缺失必填 `id`(顶层资产);
- 重复 ID(同类型 + 同 id,mod 文件之间;覆盖原版 SageXml 不算冲突);
- 引用未解析(引用了不存在的资产 ID,可配置是否忽略原版 manifest 中的 ID);
- `<Include>` 目标文件找不到、Include 循环;
- `$DEFINE` 未定义。
**补充(manifest 解析,支持 include reference 后的补全/导航/诊断)**
-`Mod.xml`(或其他文件)用 `<Include type="reference" source="DATA:static.xml" />` 引用占位文件时,实际内容来自 SDK `builtmods` 下对应的已编译二进制 manifest(`static.manifest` / `global.manifest` / `audio.manifest`)。
- 通过解析这些 manifest,可以得到其包含的全部资产(名称、类型、来源文件),从而:
- **代码补全**:例如 reference 了 `audio.xml` 后,所有音频资产 ID 都能出现在引用型属性(如 `AudioEventRef`)的补全里;
- **引用导航/悬停**:能定位资产来自哪个 manifest、哪个源文件;
- **诊断**:能把“引用了 manifest 中的 ID”识别为已解析,而不是误报未解析引用。
- manifest 为二进制格式,解析逻辑参考 OpenSAGE `ManifestFile.cs`(用户已在本工作区 `OpenSAGE/` 克隆并切到指定 commit)。关键格式要点:
- 头部含版本(5/6/7)、端序标志、各缓冲区大小、资产数量;
- 每个资产条目含 `TypeId`(哈希)、`NameOffset``SourceFileNameOffset` 等;
- `TypeId` 哈希 → 类型名的映射来自 OpenSAGE 的 `AssetType` 枚举(本工作区可提取);
- 资产名与源文件名各自存放在独立的空字符结尾字符串缓冲区中。
### P1:非近期目标(本期不做,但预留扩展点)
6. **高效搜索**Mod 项目巨大(Corona 约 7500 个 XML、38MB)时直接全文搜索很慢,需要一个高效的 XML 内容索引机制。
7. **索引机制的复用性**:希望索引不仅能服务 VS Code 插件,也能被其他工具(如搜索、静态分析)复用,因此索引/解析核心应设计成与编辑器无关的纯模块。
## 三、环境与参考资源
- SDK`C:\Apps\RA3-MODSDK-X`(含 `defaultscript.cs` 编译脚本、`Schemas/xsd``SageXml` 原版源码、`builtmods` 编译产物)。
- 中小项目:`C:\Apps\RA3-MODSDK-X\Mods\AttachTest``D:\Mods\CoronaMod\mods\mods\GenEvoTest`
- 大型项目:`D:\Mods\CoronaMod\mods\mods\corona`(自带 `xsd/`)。
- 现有工具:工作区 `check_duplicate_ids.py`include 解析与重复 ID 检测的参考实现)。
- manifest 格式参考:OpenSAGE `src/OpenSage.Game/Data/StreamFS/ManifestFile.cs`commit `d45d361`,最新分支已移除该文件)。本机网络受限未能拉取,且 manifest 为压缩/哈希的二进制,本期不实现其解析。
## 四、验收标准
- 在 AttachTest / GenEvoTest 上开箱即用(高亮、补全、跳转、诊断)。
- 在 Corona 规模的目录上不卡 UI:索引在后台执行、保存文件后增量更新。
- 纯解析/索引核心不依赖 VS Code API,可被其他工具复用。
- 可用 `vsce package` 打出可安装的 `.vsix`