422 lines
35 KiB
Markdown
422 lines
35 KiB
Markdown
# 调研结论与实施计划(已按最新代码同步更新)
|
||
|
||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-04)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)、simple-content 元素文本引用(补全 / hover / 跳转 / 诊断 / Find All References)等。
|
||
|
||
## 一、调研结论(带证据)
|
||
|
||
### 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:refType` 可声明在 simple type 上,也可声明在 `<xs:attribute>` 节点上(模型生成器两者都读、属性级优先)。
|
||
- `Poid`("Pipeline Object Id",`xas:isWeakRef="true"`)表示**管线局部标识**:`id` 属性定义元素自身(如 `ModuleData@id` → refType `ModuleData`);`ModuleId`、`AutoResolveBody`、`SoundRef` 等 Poid 属性引用同一资产/子树内的模块、子对象、材质——它们都不对全局资产索引做 resolved 判定。
|
||
- **同名元素在不同父节点下类型不同**(如 `<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`(u16,5/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 沿解析树逐层解析)
|
||
semanticTokens.ts 语义 token 兜底高亮(纯 TS:标签/属性/值范围,仅 malformed 时启用)
|
||
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 路径解析(纯 TS,BAB /data /art /audio 顺序)
|
||
existence.ts 文件集存在性快照(目录枚举 Set,替代逐路径 statSync)
|
||
manifestParser.ts .manifest 二进制解析 + 类型/ID 推导(纯 TS)
|
||
fileScanner.ts 目录遍历缓存 + Include source 候选收集
|
||
refs.ts 引用目标解析(属性 + 元素文本内容,按 refType / isRef /
|
||
inheritFrom 过滤,纯 TS)
|
||
shallowScan.ts 大体积美术资产(.w3x 等)顶层浅扫描(纯 TS,不建 DOM)
|
||
records.ts 每文件紧凑索引记录(资产/Define/Include/xi + 行号)
|
||
caches.ts 跨重建持久缓存(DocumentCache / IndexRecordsCache /
|
||
IncludeResolveCache)+ 失效纪元 InvalidationsEpoch
|
||
diskCache.ts 跨会话磁盘缓存(gzip JSON、原子写、多信号 stat 校验)
|
||
indexer.ts 工作区索引器(资产/Define/流/manifest/w3x 合并,
|
||
分阶段:XML → art,中间快照可发布)
|
||
types.ts 共享类型
|
||
features/
|
||
completion.ts 补全 provider(元素/属性/值,上下文感知;xs:list 多值按当前段过滤)
|
||
hover.ts hover provider
|
||
navigation.ts 定义/引用/文档链接/大纲
|
||
diagnostics.ts 实时诊断
|
||
semanticTokens.ts 语义 token provider(文档有解析错误时接管着色)
|
||
syntaxes/
|
||
ra3modxml.tmLanguage.json 注入 source.xml 的领域高亮(纯注入,不替换 XML 主语法)
|
||
tools/
|
||
xsd-to-model.mjs XSD → schema-model.json(含 xs:list:继承 item 枚举/引用语义,isList 标记)
|
||
extract-asset-types.mjs OpenSAGE AssetType.cs → asset-types.json
|
||
test/
|
||
fixtures/minimod 样例 Mod(include 各种情形、同名 ID、嵌套 xi:include、manifest 回退)
|
||
*.test.mjs 11 个测试文件(xmlParser / context / completion / semanticTokens /
|
||
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。
|
||
**美术资产(.w3x)**:`<Include type="all">` / `ART:` 指向的 `.w3x`(及内容嗅探为
|
||
XML 的未知扩展名文件)按其顶层资产入库(`W3DContainer` / `W3DMesh` /
|
||
`W3DHierarchy` / `W3DCollisionBox` 等),使 `Model@Name`、`Hierarchy`、`Mesh`
|
||
等引用可解析;大模型文件**浅扫描**(不建 DOM),结果缓存在 workspace 级、
|
||
跨重建复用(详见设计决策 14)。
|
||
3. **manifest 资产建模**:类型优先用哈希表,未知时从名称前缀推导;可引用 ID 取最后冒号段;类型名统一走大小写规范化(`W3dContainer` ↔ `W3DContainer`),类型匹配严格遵循 XSD 继承链。
|
||
4. **上下文感知元素类型**:同名元素按父元素类型解析(`resolveElementType` 沿解析树逐层 `childTypeOf`,失败回退全局映射),保证 `<Weapon>` 等元素的属性/引用判定正确。
|
||
5. **引用判定与解析**:`refType` 或 `isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);`inheritFrom` 按可继承类型过滤。**局部作用域例外**(`isLocalReferenceAttribute`):`id` 是元素自身的定义点——无 refType 或 refType 与自身类型兼容时不检查、不解析(`RoadObject@id→Road` 这类跨类型 id 引用保留检查);Poid 类型属性是管线局部引用,全局索引无法判定,不检查、不解析。
|
||
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),重建期间的新请求标记脏并在完成后重跑;状态栏显示进度与统计。
|
||
11. **`xs:list` 建模与多值补全**:list 简单类型继承 itemType 的枚举 / refType / isRef /
|
||
allowsDefine 并标记 `isList`(`LocomotorSurfaceBitFlags`、`KindOfBitFlags` 等 79 个
|
||
类型、317 处属性声明受益);补全只对“最后一个空格段”过滤,替换范围只覆盖当前段,
|
||
支持 `Surfaces="GROUND ` 之后继续输入 `W` 提示 `WATER`。第十三轮(2026-08-04)
|
||
补全触发与编辑体验:空格注册为触发字符;列表过滤排除已出现的 flag;当前段已是
|
||
完整枚举值且没有更长变体时进入“追加模式”(零宽 range + `insertText=" FLAG"`,
|
||
可直接在闭合值末尾/中间追加);替换范围止于光标,中间插入不会删除尾部 flag。
|
||
12. **未闭合引号的行尾恢复**:起始标签扫描到 EOF 且引号未闭合时,在第一个换行处截断
|
||
标签并继续解析,未闭合只影响当前行(仍上报 `Unterminated start tag`),后续元素
|
||
的补全 / hover / 诊断不中断。第十三轮补充:恢复出的元素带 `recoveredStartTag`
|
||
标记并补挂父链;补全上下文对“光标在恢复元素内但越过 `startTagEnd`”的情况按
|
||
`text.slice(tagStart, cursor)` 重新解析部分标签,使多行书写的未闭合属性
|
||
(如 `Disposition="`)仍可获得 attribute-value 补全,且不影响全局解析。
|
||
13. **语义 token 兜底高亮**:TextMate 对未闭合引号会把后续内容当字符串吞掉(任何
|
||
XML 编辑器皆然);扩展注册 `DocumentSemanticTokensProvider`,仅当解析报错时用
|
||
语义 token 覆盖标签名 / 属性名 / 属性值(标准 token 类型 `type` / `property` /
|
||
`string`,主题自带配色)。合法文件返回空,观感与纯 TextMate 完全一致。
|
||
14. **大体积美术资产浅扫描 + 跨重建持久缓存**(第八轮,2026-08-02):
|
||
- 实测 Corona:3788 个 w3x / 2.64 GB,163 个超过 4 MB,最大 22.8 MB;大文件是
|
||
`W3DHierarchy` + 若干 `W3DMesh`(`Vertices/V`、`Triangles/T` 等 unbounded 数值
|
||
载荷),顶层记录通常只有几个到二十几个。
|
||
- 全量解析内存放大约 17 倍(6.3 MB 文本 → +109 MB DOM),不可接受;`scanXmlShallow`
|
||
单次线性扫描只提取顶层 `name+id`、`<Includes>`、`<xi:include>`、`<Defines>`,
|
||
22 MB 文件 ~600 ms、保留内存≈0。
|
||
- 索引按扩展名三分:`.xml` 全量解析(4 MB 上限不变);`.w3x` 浅扫描;
|
||
未知扩展名嗅探文件头(`<` 开头、无 NUL)决定按 XML 浅扫描或二进制登记
|
||
(manifest 是 `*.manifest` 二进制,不存在 `.manifestxml` 源码格式)。
|
||
- `DocumentCache` / `ShallowScanCache` 由 `ModWorkspace` 持有,每次重建传入新的
|
||
`ModIndexer`;按 `mtimeMs + size` 校验,未变化不重读。Corona 第二次构建
|
||
w3x 重扫数为 0(4,829 次缓存命中)。
|
||
- 读取整个文件不可避免(顶层边界需要全量扫描),但建 DOM 不是;优化的是
|
||
"不分配子节点对象"与"跨重建不重读",两者叠加后方案可行。
|
||
15. **重建零 stat + 记录驱动索引**(第九轮,2026-08-02,v0.1.1):
|
||
- 插桩发现每次重建(含信任重建)都有约 11 万次同步 `statSync`
|
||
(`resolveSource` 的 include 存在性检查),机械盘上占 10-20s;
|
||
`IncludeResolveCache` 按(目录 + source)缓存解析结果,内容编辑不清、
|
||
创建/删除文件与强制 reindex 才清。信任重建 statSync 降到 0。
|
||
- `IndexRecordsCache` 缓存每文件紧凑索引记录(顶层资产 / Define / Include /
|
||
xi:include + 1-based 行号),信任重建完全不接触 DOM;`DocumentCache`
|
||
双重淘汰(条数 LRU + 元素预算,超预算先淘汰最大树)把 DOM 常驻内存封顶。
|
||
- w3x 缓存不再保留 LineMap(Corona 全量约 700MB),浅扫描直接产出带行号的记录。
|
||
- 实测:Corona 信任二次构建 21s → **2.0s**(statSync 0);首建后 2.5GB
|
||
堆保留确认为构建期可回收垃圾,常驻 ~100MB;强制 reindex ~5-25s。
|
||
- 候选目录扫描并行化;`stats` 新增 `candidatesMs` / `walkMs` / `resolveCalls` /
|
||
`resolveCacheHits` 供索引报告定位耗时。
|
||
16. **分阶段索引与部分可用性**(第十轮,2026-08-03):索引分两阶段发布——
|
||
阶段 A(xml)只走 XML + manifest include 链,w3x 只登记进待扫队列;
|
||
阶段 B(art)浅扫描队列并继续走 w3x 内的 include。阶段 A 结束即发布
|
||
不可变快照(`snapshotIndex` 深拷贝嵌套 Map/数组),XML/枚举/语法类功能
|
||
在首建早期即可用;引用类诊断在 `!complete || stale` 时“显示但标注”
|
||
(code 为 `*-indexing`,消息注明 index incomplete)。快照携带
|
||
`complete` / `phase` / `stale`,状态栏显示阶段。
|
||
17. **构建中失效与 stale 标记**(第十轮):watcher 的 change / create /
|
||
delete 均触发防抖重建;`InvalidationsEpoch` 记录失效次数,快照发布时若
|
||
期间有失效则标记 stale,由 dirty 机制随后重建收敛;构建失败保留上一个
|
||
快照而非清空索引。
|
||
18. **多信号文件 stamp**(第十轮):`IndexedFile.stat` 扩展为
|
||
`{ size, mtimeMs, birthtimeMs, ctimeMs }`,任一不匹配即重读,为磁盘
|
||
持久化缓存铺路(FAT32 mtime 2s 粒度、工具保留 mtime 等场景)。
|
||
19. **文件集快照替代 statSync**(第十一轮,2026-08-03):`ExistenceSnapshot`
|
||
用**惰性按目录 readdir**(只读查询到的父目录,按目录缓存)回答 include
|
||
存在性,覆盖根之外才 statSync 回退;盘符根不枚举。首版全量递归枚举使
|
||
XML 阶段从 27s 涨到 45s,已改为惰性模式(复测 24.0s)。
|
||
`stats.snapshotHits / snapshotFallbacks` 入报告;Corona 首建 75,926 次
|
||
查询全部由快照回答、0 回退。
|
||
20. **磁盘持久化缓存**(第十一轮):`DiskRecordsCache` 持久化 records 缓存
|
||
(gzip JSON、原子写、identity key);启动时并发 stat 多信号校验,不匹配
|
||
丢弃重读;构建后异步回写。Corona 缓存仅 651 KB,冷启动约 11s(原
|
||
~2.5 分钟)。
|
||
21. **缓存命令**(第十一轮):`ra3modxml.clearCache`(清内存 + 磁盘 +
|
||
强制重建)、`ra3modxml.showCacheReport`(路径/大小/校验统计/命中数)。
|
||
22. **重建插桩**(第十一轮补充):`buildCount` / `lastBuildTrigger`
|
||
(initial、save、watcher-*、config、reindex、clear-cache、
|
||
dirty-followup)进入索引/缓存报告;“RA3 Mod XML” 输出通道记录每次构建
|
||
的触发原因、phase A 发布时间与完成耗时,用于定位“首建后又重建一次”
|
||
之类的现象。
|
||
23. **watcher 噪声过滤与 URI 日志**(第十一轮补充):watcher 事件把触发
|
||
URI 写入输出通道;路径含 `.git` 段的事件直接忽略(后台 fetch /
|
||
maintenance 会周期性触碰 `.git`,不应触发重建或 stale 标记)。
|
||
24. **watcher 内容白名单**(第十一轮补充):临时文件命名模式
|
||
(`.git`/`.tmp`/`.lock`/`~`/`.swp`/`.bak`/`.orig` 后缀、`.#`/`.~`
|
||
前缀)全部忽略;`onDidChange` 只响应扩展名白名单(`.xml` / `.w3x`,
|
||
RA3 合理文本格式为 xml/w3x/lua,lua 暂未索引、manifest 为二进制)或已在
|
||
索引中的文件;创建/删除仍响应所有真实文件(影响 include 存在性)。
|
||
25. **当前文档局部链 + 逻辑树展开(第十二轮,T1)**:新增 `xpointer.ts` /
|
||
`logicalTree.ts` / `localScope.ts`。打开文件时按当前文本建立局部 overlay
|
||
(自身资产 / `$DEFINE` / include 链),并生成展开 `xi:include` 的逻辑树;
|
||
features 经 `ws.getScope(document)` 拿到 overlay-aware 索引。Poid 引用
|
||
(`AttachModuleId` / `ModuleId` / `AutoResolveBody` 等)在最近 GameObject
|
||
子树内解析;未命中不新增诊断(保守策略,避免跨文件误报)。
|
||
顶层 `<Include type="all">` 暂不并入逻辑树(保留为后续扩展)。
|
||
26. **属性补全的插入布局与类型化默认值(第十四轮,2026-08-04)**:
|
||
`attributeInsertLayout` 按临近属性的排版决定插入方式——贴引号时补空格、
|
||
一行一个属性时补换行 + 缩进、已在新行时用临近缩进替换当前行空白、
|
||
inline 风格的手动换行保留用户缩进;`attributeValuePlaceholder` 对引用/
|
||
枚举/list/布尔等建议类属性保留 `$1` + 自动触发,对标量属性填 XSD 默认值
|
||
或类型示例(`0d` / `0s` / `100%` / `0.0` / `0`),具体默认值不再弹空
|
||
suggest。
|
||
第十五轮(2026-08-04)最终结论:VS Code 插入含换行的补全文本时会把当前
|
||
行基础缩进与文本内嵌缩进相加(3+3=6、6+3=9…),因此换行前缀只插入 `\n`、
|
||
缩进交给编辑器;同时半截属性名(`hasValue=false`)不再作为缩进锚点,改用
|
||
第一个独占一行的完整属性作为规范缩进,插入换行时顺带吞掉触发补全留下的
|
||
尾随空格;属性名补全改用 `SnippetString`(`$1` 成为真正占位符),并新增
|
||
输出通道调试日志。
|
||
27. **simple-content 元素文本引用(第十六轮,2026-08-04)**:simple type
|
||
子元素(如 `ObjectCreationList` 内嵌套 `<CreateObject>`,类型
|
||
`GameObjectWeakRef`)的**标签间文本**就是资产引用。内容区补全现在区分
|
||
“复杂元素 → 子元素名”与“简单元素 → 值补全”;用户已输入 `<` 时替换范围从
|
||
`<` 开始,杜绝 `<<`;simple type 元素片段固定为 `<Name>$1</Name>`(可填
|
||
值)并自动触发值补全。hover / Ctrl 跳转 / 诊断 / Find All References
|
||
均增加内容 token 分支。只有**带 `xas:refType`** 的 simple 内容按全局引用
|
||
处理(291 处子元素声明);无类型 `AssetReference`
|
||
(`FXShaderConstantTexture@Value`、`RenderSubObjectReference@Mesh` 等
|
||
真实数据是贴图/子对象名)与 `Poid` 不参与全局解析,避免误报。
|
||
补充:真实文件中 `<` 后还有 `</…>` 时,`findTagEnd` 曾把闭合标签的 `>` 误
|
||
当成残缺开始标签的结束,生成空名/半截名伪元素,补全走 element-name 分支
|
||
导致 `<<`。修复为引号外遇到 `<` 即视为未闭合(行尾恢复),且
|
||
`elementNameItems` 的替换范围包含 `<`。实机再回归:范围含 `<` 会让 VS Code
|
||
用 `<` 做过滤前缀导致菜单为空——改为保留已输入的 `<`、range 从 `<` 之后
|
||
开始、插入文本不带开括号;`textContentTokenAt` 对未闭合元素用 `el.end`
|
||
作内容边界。
|
||
|
||
## 三、实施步骤
|
||
|
||
1. [x] 调研(Include 规则、XSD、示例项目、规模、工具链)——见本文档第一部分。
|
||
2. [x] 脚手架:`package.json`、`tsconfig.json`、esbuild、`.vscodeignore`、README。
|
||
3. [x] 生成模型:`schema-model.json`(XSD,295 顶层元素 / 1851 类型)与 `asset-types.json`(79 个 AssetType 哈希)。
|
||
4. [x] 纯 TS 核心:include 解析、manifest 解析、XML 解析封装、索引器、引用解析。
|
||
5. [x] 功能层:补全、hover、导航、诊断、大纲、高亮 grammar。
|
||
6. [x] 单测(fixture Mod,73 个用例全绿:含 xs:list 枚举、未闭合引号恢复、list 多值
|
||
分段、带 vscode stub 的补全集成、语义 token 兜底)+ 编译 + `vsce package` 打包
|
||
(ra3-mod-xml-0.1.1.vsix,约 499KB)。
|
||
7. [x] 在 AttachTest / GenEvoTest / Corona 上做冒烟验证,并按真实项目反馈修复问题(详见 `docs/analysis-issues.md` 八轮分析)。
|
||
8. [x] w3x 美术资产索引(第八轮):新增 `shallowScan.ts` / `caches.ts`,w3x 与内容嗅探
|
||
XML 走浅扫描,Include source 补全候选加入 w3x,BOM 剥离,缓存跨重建持久化;
|
||
AttachTest 报错场景(`AUGunship_SKN`)修复,Corona 首次/二次构建实测验证。
|
||
9. [x] Corona 性能与内存优化(第九轮,v0.1.1):`records.ts` 记录驱动索引、
|
||
`IncludeResolveCache` 零 stat 重建、DOM 元素预算淘汰、w3x LineMap 移除、
|
||
候选并行扫描、阶段计时;Corona 信任重建 2.0s;确认首建 2.5GB 为可回收垃圾。
|
||
10. [x] 部分可用性 + 分阶段索引(第十轮,2026-08-03):T0 解耦(语法/模型
|
||
诊断、枚举/子元素补全、Include 链接无索引可用)、A/B 分阶段发布、watcher
|
||
触发重建 + 失效纪元 stale 标记、stat 多信号扩展;测试 73 → 79。
|
||
11. [x] 冷启动提速(第十一轮,2026-08-03):文件集快照替代 statSync、
|
||
磁盘持久化缓存(多信号校验、原子写)、clearCache/showCacheReport 命令;
|
||
测试 79 → 90;Corona 冷启动 ~11s。
|
||
12. [x] 惰性存在性快照 + 重建插桩(第十一轮补充,2026-08-03):全量目录枚举
|
||
改为惰性按目录 readdir(phase A 45s → 24.0s);新增 buildCount /
|
||
lastBuildTrigger 与输出通道日志;索引/缓存报告显示构建序号与触发原因。
|
||
13. [x] watcher 噪声过滤 + URI 日志(第十一轮补充,2026-08-03):输出通道
|
||
记录 `[watcher-*] <path>`;`.git` 段路径忽略;测试 90 → 91。
|
||
14. [x] watcher 内容白名单(第十一轮补充,2026-08-03):临时文件命名模式
|
||
过滤 + 内容变更扩展名白名单 + `isIndexedFile` 兜底;测试 91 → 92。
|
||
15. [x] T1 当前文档局部链 + include 展开(第十二轮,2026-08-03):局部
|
||
overlay(不在任何流里的文件也能解析自身引用)+ `xi:include` 逻辑树展开
|
||
+ Poid 局部作用域补全/悬停/跳转;测试 92 → 98。2026-08-04 补充构建期
|
||
闸门:`getScope` 在重建进行中只返回 parse-only scope,避免与 indexer
|
||
抢盘(版本 0.1.9)。
|
||
16. [x] bit-flag 列表补全修复(第十三轮,2026-08-04):空格触发字符、多行
|
||
未闭合标签的部分标签重解析(`recoveredStartTag` 标记 + 恢复元素父链)、
|
||
闭合值内追加模式与中间插入范围修复;测试 98 → 107(版本 0.1.10)。
|
||
17. [x] 属性补全插入体验(第十四轮,2026-08-04):闭合引号后自动补空格、
|
||
临近属性缩进对齐、标量属性类型化默认值;测试 107 → 111(版本 0.1.11)。
|
||
18. [x] 属性补全缩进叠加修复(第十五轮,2026-08-04):换行前缀只插入 `\n`
|
||
(编辑器自动补基础缩进,避免 3+3=6 式叠加)、完整属性锚点 + 首个独占一行
|
||
属性为规范缩进、`$1` 改为 SnippetString 占位符、输出通道调试日志、尾随
|
||
空格吞除;测试 111 → 113(版本 0.1.12–0.1.13)。
|
||
19. [x] simple-content 文本引用修复(第十六轮,2026-08-04):`<` 后补全范围
|
||
覆盖 `<`、simple type 元素片段 `<Name>$1</Name>`、简单元素内容值补全、
|
||
`refs.ts` 新增 `isReferenceContentType` / `resolveContentReferenceTargets`、
|
||
hover / 定义跳转 / 诊断 / Find All References 内容分支;
|
||
测试 113 → 121(版本 0.1.14)。实机回归补充:`findTagEnd` 引号外遇 `<`
|
||
视为未闭合 + 插入文本不带开括号(range 从 `<` 之后开始);测试 121 → 125,
|
||
再回归 125 → 128(未闭合元素内容 token)。
|
||
20. [x] simple-content 零宽边界与大列表补全(第十七轮,2026-08-04):
|
||
`>` 后光标归入内容区、已闭合元素 `end` 改为开区间;超过 400 条的
|
||
id/define/local/include 候选返回 `isIncomplete` 让 VS Code 随输入重请求;
|
||
当前文档 local 资产优先、候选 top-N 用堆避免全量排序、Include source
|
||
先排序再截断;测试 128 → 136。
|
||
|
||
## 四、验证结果(实测)
|
||
|
||
| 项目 | 规模 | 索引耗时 | 资产数 | 说明 |
|
||
|---|---|---|---|---|
|
||
| AttachTest | 88 文件 | ~1.2s | 35,607(manifest 35,322,w3x 浅扫 62) | 2 个流(static + mapmetadata);二次构建 354ms / 62 缓存命中 |
|
||
| GenEvoTest | 66 文件 | 首次 ~2.6s / 二次 ~0.4s | 35,502(manifest 35,322,w3x 浅扫 38) | 2 个流、73 个 Define;二次构建 0 重扫 / 38 缓存命中 |
|
||
| Corona | 8,976 文件 | 首次 ~250s / 信任二次 ~2s / 强制 ~5-25s | 64,868(manifest 35,322,w3x 浅扫 4,829) | 3 个流、183 个 Define、0 诊断;二次构建 statSync 0、resolveHits 15,333 |
|
||
|
||
单元测试覆盖:XML 解析(自闭合/容错/偏移/未闭合引号行尾恢复 + recovered
|
||
标记)、补全上下文(未闭合引号仍为 attribute-value、多行未闭合引号、list 多值
|
||
分段、闭合引号后为 attribute-name)、补全集成(vscode stub 下
|
||
`LocomotorTemplate@Surfaces` 未闭合引号枚举补全、空格后第二段过滤与替换范围、
|
||
空格后排除已用 flag、中间插入范围止于光标、闭合值末尾追加模式、`CAN_ATTACK`
|
||
前缀保护、多行未闭合 `Disposition` 完整链路、闭合引号后补空格、一行一个属性
|
||
换行缩进、新行缩进对齐、标量类型化默认值)、语义 token(标签/属性/值范围、
|
||
合法文档返回空、malformed 返回兜底 token)、include 解析(BAB 顺序、SDK 根
|
||
优先于 SageXml)、manifest 二进制解析(合成 v5 样本、类型/ID 推导)、索引器
|
||
(资产/Define/流/缺失 include/嵌套 xi:include)、XSD 模型(上下文类型、
|
||
`childTypeOf`、大小写规范化、属性级 refType、外来命名空间判定、`xs:list`
|
||
枚举继承与 `isList` 标记)、引用过滤(`Weapon="X"` 只跳 `WeaponTemplate`、
|
||
模块 `id` 定义点、Poid 局部引用、`xi:include` 不校验、`Side="Allies"` 命中
|
||
manifest 的 `PlayerTemplate`)、simple-content 文本引用(`<` 后补全不产生
|
||
`<<`、`<CreateObject>$1</CreateObject>` 片段、内容值按 refType 过滤、
|
||
内容 hover / Ctrl 跳转 / 诊断)。
|
||
|
||
> 注:D: 盘移动硬盘已恢复连接;Corona 已在第八 / 九轮按上述新数据回归。
|
||
|
||
## 五、假设与开放问题
|
||
|
||
- 假设 SDK 路径默认 `C:\Apps\RA3-MODSDK-X`(与 prompts 一致),可在设置中修改。
|
||
- 假设补全/导航以“文本语义分析”为主,不做完整 XSD 校验(BAB 才是权威校验器)。
|
||
- 开放:是否发布到 VS Code Marketplace(需要 publisher)——本期先保证本地 `vsce package` 可安装。
|
||
- 开放:**“宏展开”式虚拟合并**(用户提议,方向已确认):`xi:include` 已按逻辑树
|
||
展开(第十二轮,见第六节);顶层 `<Include type="all">` 与 `inheritFrom` +
|
||
`xai:joinAction` 的深合并仍未实现,后续如需要“当前文档视角的全量合并诊断”再继续。
|
||
|
||
## 六、include 展开设计备忘(2026-08-01;xi:include 部分已实施于第十二轮)
|
||
|
||
> 目的:集中记录 include 处理相关的现状、结论与设计,下次遇到 include 问题时从这里继续,
|
||
> 并在实施后把结果回写本节。
|
||
|
||
### 1. 现状(截至第五轮,已实现)
|
||
|
||
| 能力 | 状态 |
|
||
|---|---|
|
||
| `<Include type="all">` / `instance` 递归索引(顶层资产、流、Define) | 已实现(indexer `walk`) |
|
||
| `reference` → builtmods manifest 解析 / 缺失回退占位 XML | 已实现 |
|
||
| 嵌套 `xi:include`(任意层级):目标可索引、缺失报 `include-not-found`、Ctrl+点击跳转、`href` hover 解析目标 | 已实现(第二轮 + 第五轮) |
|
||
| `xi:include` 及其属性不参与 XSD 校验(外来命名空间守卫 `isXsdElementName` / `isXsdAttributeName`) | 已实现(第五轮) |
|
||
| include 目标内容“虚拟合并”进父文档的逻辑树 | 已实现 `xi:include`(第十二轮);顶层 `<Include type="all">` 仍不展开 |
|
||
|
||
### 2. 已确认的方向
|
||
|
||
先展开成不含 include 的文档树(类比 C++ 宏展开),再对展开后的树做 mod XML 解析。
|
||
BAB(`defaultscript.cs`)编译时正是这样把整个 Mod 合并成一份大 XML 的。
|
||
|
||
### 3. 关键设计决策:逻辑树拼接,不做文本拼接
|
||
|
||
- **不要**把 include 目标展开成文本再整体重新解析:源码偏移会断裂,诊断 / 跳转 / hover /
|
||
补全全部无法映射回原始文件。
|
||
- **要做**的是:解析器逐文件解析(现状不变);展开器把目标文件选中节点按 `xpointer`
|
||
子集挂进父元素,节点保留各自的源文件与原始偏移(来源追溯)。后续分析跑在逻辑树上,
|
||
范围映射按节点 `sourceFile` 回到对应文件的 lineMap。
|
||
- 现有 `parseXml` 已记录标签 / 属性 / 值的起止偏移,`XmlElement` 结构可直接复用;拼接时
|
||
用浅拷贝节点壳并重建 parent 链,避免破坏目标文件缓存树自身的 parent 指针。
|
||
|
||
### 4. 展开范围
|
||
|
||
| 构造 | 拼入逻辑树 | 理由 |
|
||
|---|---|---|
|
||
| `xi:include` | ✅ | 内容并入父元素(HeadlightDraw2 场景) |
|
||
| EA `<Include type="all">` | ✅ | BAB“内容合并”,等价于复制进来 |
|
||
| `type="instance"` | ❌ | 只影响编译可见性;拼树会把 BaseVehicle 的顶层资产错误塞进当前文档 |
|
||
| `type="reference"` | ❌ | manifest 编译产物,无文本内容 |
|
||
| `inheritFrom` + `xai:joinAction` | 单独一轮 | 元素级继承深合并(Replace/Remove),不是宏展开 |
|
||
|
||
### 5. 落地位置与接入点
|
||
|
||
- 新纯模块(与编辑器解耦,呼应 P1):输入 `(parse 树, resolveSource 回调, readDocument 回调)`,
|
||
输出逻辑树(root + elements 扁平列表,沿用 diagnostics 的遍历形态)。
|
||
- 接入:diagnostics / hover / navigation / completion 目前各自 `parseXml(text)`;
|
||
改为 parse 后过 expander 取逻辑树;范围映射按节点 `sourceFile` 选对应文件的 lineMap。
|
||
- 按需展开(当前打开文档)+ 按文件缓存(复用 indexer `readDocument` 的 LRU);
|
||
环 / 深度守卫复用现有 `visitedAll` 与深度限制思路(建议最大深度 64)。
|
||
- `xpointer`:仅支持 mod 实际使用的 `xmlns(n=...) xpointer(/n:Name/child::*)` 子集
|
||
(现有 `findXPointerContainer` 正则已覆盖);完整 XPath 暂不支持,遇到新形式先记录到本节。
|
||
|
||
### 6. 后续收益(承接第四 / 五轮遗留)
|
||
|
||
- **GameObject 内模块 id 局部作用域**:展开后 include 进来的兄弟模块(HeadlightDraw2)
|
||
与本体模块同树,`AttachModuleId` / `ModuleId` / `AutoResolveBody` 等 Poid 引用
|
||
才能静态解析与诊断(第四轮遗留);
|
||
- 跨 include 的上下文类型解析与结构校验;
|
||
- 顶层 `type="all"` 合并后,当前文档视角的重复 id / 引用诊断更接近 BAB 结果。
|
||
|
||
### 7. 风险与边界
|
||
|
||
- 大文件性能(Corona 约 7500 文件 / 38MB):只展开当前文档的可达链,不全局展开;
|
||
- include 环 / 深度:visited 集合 + 最大深度;
|
||
- 被包含内容的诊断上报位置:建议按节点源文件 URI 上报(与偏移一致),包含点的 `href`
|
||
上只报“缺失 / 无法解析 / 环”类问题;
|
||
- `inheritFrom` 深合并(`xai:joinAction` 的 Replace/Remove 语义)单独设计,别与宏展开混在一轮。
|
||
|
||
### 8. 下次遇到 include 问题的检查清单
|
||
|
||
1. 现象发生在哪一层:索引(indexer)、诊断(diagnostics)、导航 / hover、还是补全?
|
||
2. 现状能力是否已覆盖(见第 1 节表格);
|
||
3. 涉及内容是否跨文件(需要逻辑树)还是本文件内(当前解析树即可);
|
||
4. 若要展开:先实现第 5 节的纯模块与单测(fixture 增加 HeadlightDraw2 场景),再接入 feature;
|
||
5. 把新结论回写本节与 `docs/analysis-issues.md`。
|