Files
Ra3ModXmlExt/docs/features-reference-counts.md

8.1 KiB
Raw Permalink Blame History

功能设计:引用计数、语义 Find All References 与未引用资产

状态:已实现(v0.1.16 起)。本文档记录需求分析结论、语义定义、数据流与 已知边界;问题排查记录仍放在 docs/analysis-issues.md,功能设计单独成文。

一、需求与结论

原始需求:

  1. 在每个顶部 asset 上显示它被引用了多少次,点击后直接打开“查找所有引用”, 而不是必须通过右键菜单;
  2. 能查找“某一种类型中所有没被引用的顶部 asset”。

调研结论(2026-08-05):

  • XSD 模型层面,292 个有模型类型的顶层资产中:73 个有类型化引用指向它们, 28 个只能被 inheritFrom 指向,191 个在模型里根本没有任何引用指向 (设置类、地图元数据、w3x 子结构等)。对这些类型,“0 引用”是唯一正常状态。
  • AttachTest 实测:625 个项目定义中 29% 是 0 引用、48% 是 1 引用。 因此“所有资产一律显示计数”会制造大量噪音,也会让用户误以为资产是孤儿。
  • 结论:计数按类型过滤——只在“设计上应该被引用”的类型上显示 (0 也显示,因为对 GameObject 这类类型 0 是有效信号);自动注册/结构类 类型不显示。
  • 旧版 Find All References 是全文搜索(正则匹配 "id" / >id<),会把 id="X" 定义本身、EditorName="X" 等非引用属性也算进去。计数如果和它 同源,必然误导;如果不同源,点击后的结果又对不上。因此本轮把 FAR 一起 升级为语义引用索引,计数与点击结果共用同一数据源。

二、语义定义:什么算一次“引用”

与补全 / hover / 跳转 / 诊断完全一致(refs.ts 是唯一判定来源):

  • xas:refType 的属性值(如 CommandSetLogicCommandSet);
  • xas:refType 的 simple-content 文本(如 <CreateObject>ID</CreateObject> 含 simpleContent 复杂类型 <Sound>AudioFile</Sound><Subsound>VoiceEvent</Subsound>);
  • inheritFrom(按元素自身类型过滤);
  • refTypeisRef 属性(按同名 ID 匹配任意声明类型)。

引用值本身支持原版/Mod 常用的 manifest 风格全名 类型:ID inheritFrom="AudioEvent:BaseSoundEffect"Sound="AudioEvent:..."Side="PlayerTemplate:Allies"):解析与反向索引先按 normalizeReferenceId 取最后冒号段,再执行上述类型过滤;记录里的原始值与偏移保持不变。

inheritFromBaseAssetType 系资产是通用合法属性(XSD 只在 BaseInheritableAsset 声明,但原版数据在 FXList 等类型上也使用)。这里的 “合法属性”判定与“设计上应显示引用计数”的 referenceTargetTypes() 是分开的: 通用 inheritFrom 不会把每个资产类型都变成 CodeLens / 未引用报告的目标。

不算引用:

  • 元素自己的 id 定义点(除非是 RoadObject@id→Road 这类跨类型 id 引用);
  • Poid 管线局部引用(ModuleIdAttachModuleIdSoundRef 等);
  • $DEFINE / = 常量值;
  • 枚举、文件路径、Include@source / xi:include href
  • w3x 内部父子结构关系(W3DMesh 等靠结构归属,不走全局引用)。

引用索引按“类型 + id + 定义位置”精确归属,同名 ID 的不同类型定义互不串扰 (WeaponTemplate:X 的引用不会计到 GameObject:X 头上)。

三、数据流

parse DOM
   │  records.ts  extractIndexRecords(parse, lineMap, text)
   ▼
IndexRecords.references[]       每文件紧凑记录(refType / selfType / value /
   │                            行号 / 起止偏移),随 records 缓存跨重建复用,
   │                            并持久化到磁盘缓存(缓存版本 v3,full XML
   │                            附带内容哈希;快照另发布每文件 records 哈希)
   ▼
indexer.ts  buildReferences()   只解算本次 build 触及的文件,防止陈旧缓存泄漏
   │  referenceIndex.ts  buildReferenceIndex()
   ▼
ModIndex.references             Map<定义 key, ReferenceSite[]>
   ├── CodeLens 计数(O(1) 查表)
   ├── 语义 Find All References(返回精确位置)
   └── 未引用资产报告(0 引用 = 不在反向索引中)

引用记录在解析期就把 refType / selfType 固化下来,反向索引构建时不需要 再解析 DOM、也不需要上下文类型解析;只需对 assetsById 做一次查找并按类型 过滤。快照发布时(XML 阶段 + art 阶段)各构建一次反向索引,Corona 规模下 开销远小于 include 遍历。

反向索引只从本次构建的 walk 实际消费的 recordsModIndexer.buildRecords 构建,不读共享 recordsCache:中途 watcher 失效、或 feature 通过 readDom 重读文件,都不会让“资产在但引用缺失”的快照出现。打开文档时 CodeLens / FAR 还会比较当前文本的 records 哈希与快照,不一致就定向 invalidate 并触发 records-desync 重建自愈(仅对已保存文档,未保存编辑不触发)。

四、CodeLens 规则

  • 只对根级(AssetDeclaration 直接子元素)带 id 的资产显示;
  • 只对 isReferenceTargetType() 为真的类型显示(类型化引用目标 + inheritFrom 可继承类型;见 refs.tsreferenceTargetTypes());
  • 0 也显示:0 references / 1 reference / N references
  • 点击执行 ra3modxml.showReferenceseditor.action.showReferences 打开 references peek,结果与计数完全一致(不含定义本身);
  • 计数除了当前定义自己的反向索引桶,还并入“manifestSource 可解析到当前 文件”的 manifest 定义桶:manifestSource 按 vanilla-only 搜索路径 SDK 根 + SageXml)解析,mod 同名 DATA 路径不会被视为源码;manifest 资产有对应 SageXml 源码时,引用直接视作 SageXml 源码对该 asset 的引用 Go to Definition 同样把 manifest 定义映射到 SageXml 源码);
  • 索引重建完成后自动 editor.action.codeLens.refresh,计数不会停留在旧值。

五、未引用资产

  • 主入口:命令面板 RA3 Mod XML: Find unreferenced assets… 第一步 QuickPick 选类型(只列“设计上应被引用”的类型,显示每种未引用数量), 第二步列出资产(id — 相对路径:行号),点击跳转到定义;
  • 加速入口:编辑器右键菜单 RA3 Mod XML: Find unreferenced assets of this type (仅 editorLangId == xml && ra3modxml.active 时显示),光标在顶部资产上 时直接预选该类型,否则回退到类型选择;
  • 语义:只统计 origin === "project" 且非 viaInstance 的定义; manifest / SDK 定义永远不参与;如果某个 id 覆盖了原版资产,来自原版/其他 流的引用同样计入(否则会把覆盖件误报成未使用)。

六、已知边界与后续

  • 当前文档局部作用域(不在任何全局流里的文件)的引用不在全局反向索引中: 这类文件打开后 CodeLens / FAR 只反映全局流;局部链内的引用暂不计数。
  • Find All References 不返回定义行本身(即使 VS Code 传入 includeDeclaration),因此结果数量与 CodeLens 计数严格一致。
  • AssetIdList 等“任意资产 ID 列表”的引用语义仍未建模:整个属性值按一条 引用记录处理,与现有补全/诊断保持一致(已知缺口,后续可在 records 层扩展)。
  • w3x 文件本身不提取引用记录(浅扫描无 DOM);对 w3x 资产的引用从引用它的 XML 文件捕获。
  • manifest 二进制里包含 BAB 编译后的完整引用图(AssetReferenceOffset/Count + 8 字节 TypeId+InstanceId 条目),目前仍跳过。将来可解析它给原版/manifest 资产提供“编译器权威”计数;mod 源码资产在编译前没有 manifest,仍需源码级 引用索引。
  • 未引用资产目前是命令 + QuickPick 的“查询”形态;如果之后想要常驻浏览, 可以再加 Tree View(更重,暂不计划)。