fix inherit from
This commit is contained in:
@@ -11,6 +11,8 @@ debug.log
|
||||
docs/**
|
||||
OpenSAGE/**
|
||||
test/**
|
||||
images/**
|
||||
!images/icon.png
|
||||
**/*.map
|
||||
tsconfig.json
|
||||
esbuild.mjs
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.23 — 2026-08-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- `inheritFrom` is now accepted on all `BaseAssetType`-derived assets (e.g. `FXList`, `AIMicroManagerData`, `ObjectCreationList`, `OnDemandTextureImage`, `AITargetingHeuristic`). The XSD only declares it on `BaseInheritableAsset`, but vanilla and Corona data use it more broadly. Attribute legality is now separate from the CodeLens / Find All References “reference target by design” filter, so the universal attribute does not widen the code-lens type list.
|
||||
- `simpleContent` complex types (`AudioFileRefWithWeight`, `MultisoundSubsoundRef`) keep their XSD attributes (`Weight`, `Volume`, `PitchShiftLow/High`, ...) and their text content (`<Sound>AudioFile</Sound>`, `<Subsound>VoiceEvent</Subsound>`) is now handled as a typed asset reference by completion, hover, navigation, diagnostics, the semantic reference index, and Find All References.
|
||||
- Fragment roots whose name also appears as a nested child type (e.g. `<EvaEvent>`, `<UpgradeTemplate>`) now resolve to the top-level `AssetDeclaration` type instead of the colliding child type.
|
||||
- Element-name completion for simple-content children now re-triggers value suggestions after inserting the `<Name>$1</Name>` snippet.
|
||||
|
||||
### Added
|
||||
|
||||
- 128×128 PNG extension icon (converted from `images/icon.webp`). The `.vsix` no longer bundles the `images` folder, so the large GIF demos stay out of the package.
|
||||
|
||||
## 0.1.22 — 2026-08-11
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1958,3 +1958,173 @@ id”),wrapper 根不在 XSD 里的文件报 `unknown-element`,引用在
|
||||
define / 子元素结构检查。多上下文取并集去重。
|
||||
- `<Include type="all|instance|reference">` 与 `xi:include` 语义不同:前者的目标
|
||||
是完整 `AssetDeclaration`,不进入片段模式;后者才允许片段文件。
|
||||
|
||||
---
|
||||
|
||||
## 三十、问题分析(2026-08-11):`FXList inheritFrom` 误报未知属性与模型修正
|
||||
|
||||
### 现象
|
||||
|
||||
`GlobalData/FX_List.xml` 中:
|
||||
|
||||
```xml
|
||||
<FXList id="FX_LargeEMCannonHitCrit" inheritFrom="FX_LargeEMCannonHit">
|
||||
<NuggetList>
|
||||
<ParticleSystem Particle="CritHit" OrientToObject="true" Ricochet="true"/>
|
||||
</NuggetList>
|
||||
</FXList>
|
||||
```
|
||||
|
||||
报 `Unknown attribute "inheritFrom" for <FXList>`。
|
||||
|
||||
### 根因(三个独立问题)
|
||||
|
||||
**A. `inheritFrom` 的 XSD 白名单落后于 BAB 实际语义**
|
||||
|
||||
- SDK 与 Corona 的 `AssetTypeFXList.xsd` 都写 `FXList extends BaseAssetType`;
|
||||
- `BaseAssetType` 只有 `id` / `typeHashCode` / `buildRule`,`inheritFrom` 只挂在
|
||||
`BaseInheritableAsset` 上;
|
||||
- 内置模型因此认为 `FXList` 没有 `inheritFrom`,diagnostics 的 unknown-attribute
|
||||
直接按模型属性表判断;
|
||||
- 但引用 / hover / 跳转层早就把 `inheritFrom` 当作通用引用属性处理,只有“属性名
|
||||
合法性”这一层还在用 XSD 白名单,所以表现为局部不一致。
|
||||
|
||||
证据(用插件同一套解析器 + 模型扫描):
|
||||
|
||||
| 类型 | 原版 SageXml 顶层使用 `inheritFrom` | Corona 顶层使用 |
|
||||
|---|---|---|
|
||||
| `AIMicroManagerData` | 233 | 128 |
|
||||
| `FXList` | 142 | 10 |
|
||||
| `AITargetingHeuristic` | 10 | 5 |
|
||||
| `ObjectCreationList` | 2 | 0 |
|
||||
| `OnDemandTextureImage` | 0 | 9 |
|
||||
|
||||
原版 `FXListSoviet.xml` / `FXListJapan.xml` 大量使用该写法,说明这是 BAB 接受的
|
||||
真实语义,不是用户笔误。
|
||||
|
||||
**B. `simpleContent` 复杂类型的属性被生成器丢弃**
|
||||
|
||||
`xsd-to-model.mjs` 的 `expandComplexType` 只读 `complexContent/extension`,没有读
|
||||
`simpleContent/extension`。因此:
|
||||
|
||||
- `AudioFileRefWithWeight`(XSD 有 `Weight` / `Volume`)在模型里属性为空;
|
||||
- `MultisoundSubsoundRef`(XSD 有 `Weight` / `PitchShiftLow/High` / `Volume` /
|
||||
`PlayPercent` / `VolumeShift`)同样为空。
|
||||
|
||||
Corona 实测 `<Sound Weight="...">` 565 处、`<Subsound Weight="...">` 33 处会被误报。
|
||||
simpleContent 复杂类型的**文本内容**引用语义(如 `<Sound>AudioFile</Sound>` 的补全 /
|
||||
hover / 跳转 / 诊断 / FAR)在第三十一轮补齐,见下。
|
||||
|
||||
**C. 片段根元素仍受“元素名→类型”全局单映射影响**
|
||||
|
||||
`EvaEvent` 既是顶层资产,也是 `FXNuggetTypes` 的子元素
|
||||
(`EvaEventFXNugget`)。全局 `elementTypeName("EvaEvent")` 取到的是先注册的
|
||||
`EvaEventFXNugget`。完整 `AssetDeclaration` 文档有父上下文可以纠正;但
|
||||
`additionalmaps/ALLC.xml` 这类根元素就是 `<EvaEvent>` 的片段没有父上下文,于是
|
||||
`Priority`、`TimeBetweenEvents`、`ExpirationTime` 等合法属性被当成未知。
|
||||
|
||||
### 修复
|
||||
|
||||
1. **通用属性合法性集中到模型层**:`schemaModel.ts` 新增
|
||||
`isAssetType()`(`BaseAssetType` 及其后代)与通用 `inheritFrom` 属性;
|
||||
`attributesOfType()` 对资产类型统一返回它。诊断、属性补全、hover 自动一致。
|
||||
2. **CodeLens / FAR 的“设计目标”判定保持窄口径**:`refs.ts` 的
|
||||
`referenceTargetTypes()` 仍只看 XSD 显式声明的 `inheritFrom` 与类型化引用,
|
||||
不会因为通用属性把全部 317 个资产类型变成计数目标。`isReferenceAttributeOfType`
|
||||
与 `resolveReferenceTargetsForType` 同步改为只对资产类型接受 `inheritFrom`。
|
||||
3. **生成器支持 `simpleContent/extension`**:`expandComplexType` 现在同时读
|
||||
`complexContent` 与 `simpleContent` 的 extension,重新生成模型后
|
||||
`AudioFileRefWithWeight` / `MultisoundSubsoundRef` 属性齐全。
|
||||
4. **片段根优先取顶层类型**:`schemaModel.topLevelElementType()` 从
|
||||
`AssetDeclaration` 的子元素声明解析类型;`resolveElementType()` 对文档根先用它,
|
||||
再回退全局映射;hover 的元素名展示也使用已解析类型。
|
||||
|
||||
### 测试(举一反三,全量 210 通过)
|
||||
|
||||
- `schemaModel.test.mjs`:`FXList` / `AIMicroManagerData` / `ObjectCreationList` /
|
||||
`OnDemandTextureImage` / `AITargetingHeuristic` 均接受 `inheritFrom`,
|
||||
`Include` 不接受;`AudioFileRefWithWeight` / `MultisoundSubsoundRef` 属性齐全;
|
||||
- `refs.test.mjs`:`FXList inheritFrom` 是引用,`Include inheritFrom` 不是;
|
||||
`Credits` 接受通用 `inheritFrom` 但仍是 `isReferenceTargetType() === false`,
|
||||
证明两个判定已分离;
|
||||
- `typeContext.test.mjs`:片段根 `<EvaEvent>` / `<UpgradeTemplate>` 解析为顶层
|
||||
类型,`<Weapon>` 仍回退到 `WeaponRef`;
|
||||
- `completion.test.mjs`:`FXList` 属性补全出现 `inheritFrom`;
|
||||
- `contentFeatures.test.mjs`:`FXList inheritFrom`、`<Sound Weight>`、片段根
|
||||
`<EvaEvent>` 不再报 unknown-attribute,真实拼写错误仍报。
|
||||
|
||||
### 文档同步
|
||||
|
||||
- `docs/requirements.md`:继承机制补充“对资产类型通用”;
|
||||
- `docs/plan.md`:XSD 结构说明补充实测差异与两个判定的分离;
|
||||
- `docs/features-reference-counts.md`:说明通用 `inheritFrom` 不扩大 CodeLens
|
||||
目标集合。
|
||||
|
||||
---
|
||||
|
||||
## 三十一、问题分析(2026-08-11):补齐所有“内容即引用”的语义(含 simpleContent 复杂类型)
|
||||
|
||||
### 目标
|
||||
|
||||
上一轮只恢复了 `AudioFileRefWithWeight` / `MultisoundSubsoundRef` 的属性,但它们
|
||||
的文本内容(`<Sound>AudioFile</Sound>`、`<Subsound>VoiceEvent</Subsound>`)仍然
|
||||
没有按引用处理。本轮把 simple-content 的内容语义统一到一条管线:**凡是元素文本
|
||||
内容带 `xas:refType` 的,无论底层是 simple type 还是 simpleContent complexType,
|
||||
都参与补全 / hover / 跳转 / 诊断 / 引用索引 / FAR**。
|
||||
|
||||
### 真实项目验证
|
||||
|
||||
用插件同一套解析器 + 模型扫描 Corona `Data`(7540 个 XML,跳过 w3x):
|
||||
|
||||
| 类别 | 唯一元素/类型组合 | 出现次数 | 典型元素 |
|
||||
|---|---|---|---|
|
||||
| 带 `refType` 的内容引用 | 62 | 20,813 | `Sound`→AudioFile、`Subsound`→BaseAudioEventInfo、`CreateObject`→GameObject、`TriggeredBy`→UpgradeTemplate |
|
||||
| 无 `refType` 的 `isRef` 内容 | 5 | 1,150 | `Value`/`AddEmotion`/`Compare`/`Campaign`/`Mission`→AssetReference |
|
||||
| 普通标量/枚举内容 | 6 | 9,012 | `IncludeThing`/`ExcludeThing`→WeakReference、`Script`、`SpecificBarrelOverride` |
|
||||
|
||||
结论:
|
||||
|
||||
- `Sound` / `Attack` / `Decay`(`AudioFileRefWithWeight`)内容确实是 `AudioFile`
|
||||
引用;`Subsound`(`MultisoundSubsoundRef`)内容确实是 `BaseAudioEventInfo`
|
||||
引用,必须纳入全局引用语义。
|
||||
- `AssetReference` 系的无类型内容(`Value` 等)是着色器常量、脚本参数等,
|
||||
**不应**按全局资产 ID 解析;保持上一轮“只处理带 refType 的内容”的边界。
|
||||
- `IncludeThing` / `ExcludeThing` 等 `WeakReference` 内容是对象过滤/局部语义,
|
||||
也没有 refType,不参与全局引用。
|
||||
- 内联 simpleContent(如 w3x 的 `Frame`)是 `xs:float` 标量,`contentInfoOfType`
|
||||
能识别但 `refType === null`,不会误报。
|
||||
|
||||
### 实现
|
||||
|
||||
1. **模型层**:`schemaModel.ts` 新增 `SimpleContentInfo` / `ContentTypeInfo` 和
|
||||
`contentInfoOfType()`;`ComplexTypeInfo` 增加可选 `content` 字段。
|
||||
`xsd-to-model.mjs` 对 `simpleContent/extension` 记录 base 类型的
|
||||
`refType` / `isRef` / 枚举 / list / `$DEFINE` 能力。
|
||||
2. **引用判定**:`refs.ts` 的 `isReferenceContentType()` 与
|
||||
`resolveContentReferenceTargets()` 改用统一内容描述;simpleContent 复杂类型
|
||||
的 `refType` 也进入 `referenceTargetTypes()`,保证 CodeLens 类型过滤正确。
|
||||
3. **索引**:`records.ts` 内容记录的 `refType` 从统一内容描述提取,FAR 与
|
||||
引用计数不再丢 `Sound` / `Subsound`。
|
||||
4. **补全**:`completion.ts` 的元素片段、内容值补全、子元素补全触发 suggest 均
|
||||
统一走 `contentInfoOfType()`,`<Sound>` 会生成 `<Sound>$1</Sound>` 并弹
|
||||
AudioFile 候选。
|
||||
5. **hover / 导航 / 诊断**:全部改为从 `contentInfoOfType()` 取 `refType`。
|
||||
|
||||
### 测试(全量 219 通过)
|
||||
|
||||
- `schemaModel.test.mjs`:`contentInfoOfType` 对 simple 与 simpleContent 统一;
|
||||
- `refs.test.mjs`:`AudioFileRefWithWeight` / `MultisoundSubsoundRef` 是内容引用,
|
||||
`@inline:Frame` 不是;同名 AudioFile / AudioEvent 严格按 refType 过滤;
|
||||
- `records.test.mjs`:`Sound` / `Subsound` 文本进入引用索引;
|
||||
- `completion.test.mjs`:`<Sound>` 补全成值对并触发 suggest,内容值只补
|
||||
AudioFile;
|
||||
- `contentFeatures.test.mjs`:`<Sound>` hover / Ctrl+点击 / `<Subsound>` 未解析
|
||||
诊断;
|
||||
- `referenceProvider.test.mjs`:FAR 返回 `Sound` 文本引用。
|
||||
|
||||
### 文档同步
|
||||
|
||||
- `docs/requirements.md`:simple-content 元素补充 simpleContent 复杂类型示例;
|
||||
- `docs/plan.md`:simple-content 文本引用说明补充第三十一轮扩展;
|
||||
- `docs/features-reference-counts.md`:引用语义说明补充“含 simpleContent 复杂
|
||||
类型”。
|
||||
|
||||
@@ -31,10 +31,16 @@
|
||||
与补全 / hover / 跳转 / 诊断完全一致(`refs.ts` 是唯一判定来源):
|
||||
|
||||
- 带 `xas:refType` 的属性值(如 `CommandSet` → `LogicCommandSet`);
|
||||
- 带 `xas:refType` 的 simple-content 文本(如 `<CreateObject>ID</CreateObject>`);
|
||||
- 带 `xas:refType` 的 simple-content 文本(如 `<CreateObject>ID</CreateObject>`,
|
||||
含 simpleContent 复杂类型 `<Sound>AudioFile</Sound>`、`<Subsound>VoiceEvent</Subsound>`);
|
||||
- `inheritFrom`(按元素自身类型过滤);
|
||||
- 无 `refType` 的 `isRef` 属性(按同名 ID 匹配任意声明类型)。
|
||||
|
||||
`inheritFrom` 对 `BaseAssetType` 系资产是通用合法属性(XSD 只在
|
||||
`BaseInheritableAsset` 声明,但原版数据在 `FXList` 等类型上也使用)。这里的
|
||||
“合法属性”判定与“设计上应显示引用计数”的 `referenceTargetTypes()` 是分开的:
|
||||
通用 `inheritFrom` 不会把每个资产类型都变成 CodeLens / 未引用报告的目标。
|
||||
|
||||
不算引用:
|
||||
|
||||
- 元素自己的 `id` 定义点(除非是 `RoadObject@id→Road` 这类跨类型 id 引用);
|
||||
|
||||
+14
-11
@@ -24,7 +24,7 @@
|
||||
|
||||
- 共 **821 个 XSD / 1.5 MB**,入口 `CnC3Types.xsd`。
|
||||
- 根元素 `AssetDeclaration`:`Tags` / `Includes` / `Defines` + **295 个顶层资产元素**(含内联声明)。
|
||||
- 每个元素名对应一个 `complexType`,子元素用 `xs:sequence` / `xs:choice` 定义,属性用 `xs:attribute` 定义;复杂类型通过 `xs:extension` 继承(如 `BaseInheritableAsset` 提供 `inheritFrom`)。
|
||||
- 每个元素名对应一个 `complexType`,子元素用 `xs:sequence` / `xs:choice` 定义,属性用 `xs:attribute` 定义;复杂类型通过 `xs:extension` 继承(XSD 中 `BaseInheritableAsset` 提供 `inheritFrom`)。实测 BAB / 原版数据也接受 `FXList`、`AIMicroManagerData`、`AITargetingHeuristic`、`ObjectCreationList` 等 `BaseAssetType` 系资产使用 `inheritFrom`,因此插件把 `inheritFrom` 作为所有资产类型的通用属性处理;“设计上应显示引用计数”的判定仍按 XSD 显式声明的可继承类型,两者分开。
|
||||
- `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 判定。
|
||||
@@ -252,16 +252,19 @@ test/
|
||||
第一个独占一行的完整属性作为规范缩进,插入换行时顺带吞掉触发补全留下的
|
||||
尾随空格;属性名补全改用 `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` 不参与全局解析,避免误报。
|
||||
27. **simple-content 元素文本引用(第十六轮,2026-08-04;第三十一轮扩展)**:
|
||||
simple type 子元素(如 `ObjectCreationList` 内嵌套 `<CreateObject>`,类型
|
||||
`GameObjectWeakRef`)以及 simpleContent 复杂类型(如 `<Sound>` →
|
||||
`AudioFileRefWithWeight`、`<Subsound>` → `MultisoundSubsoundRef`)的
|
||||
**标签间文本**就是资产引用。内容区补全现在区分“复杂元素 → 子元素名”与
|
||||
“内容元素 → 值补全”;用户已输入 `<` 时替换范围从 `<` 开始,杜绝 `<<`;
|
||||
内容元素片段固定为 `<Name>$1</Name>`(可填值)并自动触发值补全。hover /
|
||||
Ctrl 跳转 / 诊断 / Find All References 均增加内容 token 分支。只有**带
|
||||
`xas:refType`** 的内容按全局引用处理(291 处 simple type 子元素声明 +
|
||||
`Sound` / `Attack` / `Decay` / `Subsound` 等 simpleContent 复杂类型);
|
||||
无类型 `AssetReference`(`FXShaderConstantTexture@Value`、
|
||||
`RenderSubObjectReference@Mesh` 等真实数据是贴图/子对象名)与 `Poid`
|
||||
不参与全局解析,避免误报。
|
||||
补充:真实文件中 `<` 后还有 `</…>` 时,`findTagEnd` 曾把闭合标签的 `>` 误
|
||||
当成残缺开始标签的结束,生成空名/半截名伪元素,补全走 element-name 分支
|
||||
导致 `<<`。修复为引号外遇到 `<` 即视为未闭合(行尾恢复),且
|
||||
|
||||
@@ -23,7 +23,7 @@ XML 之间的组织靠 `<Include>` 标签,共有三种语义:
|
||||
- 无前缀的路径相对于当前文件所在目录;
|
||||
- `ART:` 路径支持“文件名前两个小写字母作为子目录”的匹配(如 `JUAntiShip` → `ju/JUAntiShip`)。
|
||||
|
||||
继承机制:`inheritFrom` 让一个元素默认获得目标元素的所有内容;具体合并行为由 `xai:joinAction`(`uri:ea.com:eala:asset:instance` 命名空间)控制,实际项目中出现的取值为 `Replace`、`Remove`。
|
||||
继承机制:`inheritFrom` 让一个元素默认获得目标元素的所有内容;具体合并行为由 `xai:joinAction`(`uri:ea.com:eala:asset:instance` 命名空间)控制,实际项目中出现的取值为 `Replace`、`Remove`。XSD 只在 `BaseInheritableAsset` 上显式声明 `inheritFrom`,但原版与 Corona 数据也在 `FXList`、`AIMicroManagerData` 等 `BaseAssetType` 系资产上使用它,插件按“所有资产类型的通用属性”处理。
|
||||
|
||||
全部 XML 语法由 XSD 定义:SDK 自带 `Schemas/xsd/CnC3Types.xsd`(及其 800+ 个子 XSD)。大型 Mod 项目(如 Corona)还会携带自己修改过的 XSD 副本。
|
||||
|
||||
@@ -41,10 +41,11 @@ XML 之间的组织靠 `<Include>` 标签,共有三种语义:
|
||||
- 枚举值(XSD `xs:enumeration`);
|
||||
- `$DEFINE` 常量(如 `$CIV_HEALTH_SMALL`);
|
||||
- `<Include source>` 补全可解析的文件路径(`DATA:` / `ART:` / `AUDIO:`)。
|
||||
- **元素文本内容(simple content)**:带 `xas:refType` 的简单内容元素
|
||||
(如 `<CreateObject>ID</CreateObject>`)在标签间补全对应类型的资产 ID、
|
||||
枚举或 `$DEFINE`;补全出的 simple-content 元素必须是可填值的成对标签
|
||||
(`<Name></Name>`),且内容区已输入 `<` 时不得产生 `<<`。
|
||||
- **元素文本内容(simple content)**:带 `xas:refType` 的内容元素
|
||||
(simple type 如 `<CreateObject>ID</CreateObject>`,simpleContent 复杂类型
|
||||
如 `<Sound>AudioFile</Sound>`、`<Subsound>VoiceEvent</Subsound>`)在标签间
|
||||
补全对应类型的资产 ID、枚举或 `$DEFINE`;补全出的 simple-content 元素必须
|
||||
是可填值的成对标签(`<Name></Name>`),且内容区已输入 `<` 时不得产生 `<<`。
|
||||
3. **引用提示(Hover)**:元素/属性悬停显示 XSD 文档、类型、默认值;资产 ID 悬停显示定义位置;`$DEFINE` 悬停显示值与定义位置。
|
||||
- 元素文本内容(simple content 引用)悬停同样显示定义位置。
|
||||
4. **引用导航**:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ra3-mod-xml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ra3-mod-xml",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.23",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "^4.5.0"
|
||||
|
||||
+2
-1
@@ -2,9 +2,10 @@
|
||||
"name": "ra3-mod-xml",
|
||||
"displayName": "%ra3modxml.displayName%",
|
||||
"description": "%ra3modxml.description%",
|
||||
"version": "0.1.22",
|
||||
"version": "0.1.23",
|
||||
"publisher": "lanyi",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"icon": "images/icon.png",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.ra3battle.cn/RA3CoronaDevelopers/Ra3ModXmlExt.git"
|
||||
|
||||
+26
-14
@@ -8,7 +8,7 @@ import {
|
||||
} from "../language/context";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import * as model from "../model/schemaModel";
|
||||
import type { AttributeInfo, SimpleTypeInfo } from "../model/schemaModel";
|
||||
import type { AttributeInfo, ContentTypeInfo } from "../model/schemaModel";
|
||||
import { isLocalReferenceAttribute } from "../indexer/refs";
|
||||
import {
|
||||
findContainingGameObject,
|
||||
@@ -84,6 +84,13 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
item.documentation = docText ? new vscode.MarkdownString(docText) : undefined;
|
||||
item.detail = type ? t("RA3 XML · {0}", type) : t("RA3 XML");
|
||||
item.insertText = this.elementSnippet(child.name, type, ctx.element == null);
|
||||
const contentInfo = type ? model.contentInfoOfType(type) : undefined;
|
||||
if (contentInfo && this.simpleContentValueKind(contentInfo)) {
|
||||
item.command = {
|
||||
command: "editor.action.triggerSuggest",
|
||||
title: t("Suggest content value"),
|
||||
};
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
@@ -120,13 +127,14 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
if (model.isTopLevelElement(name)) {
|
||||
return new vscode.SnippetString(`${open}${name} id="$1">\n\t$0\n</${name}>`);
|
||||
}
|
||||
const info = type ? model.typeInfo(type) : undefined;
|
||||
// Simple types hold text content (asset id / enum / define / string), so
|
||||
// they need an explicit closing tag and a value placeholder instead of a
|
||||
// self-closing tag that can never contain a value.
|
||||
if (info?.kind === "simple") {
|
||||
// Simple types and simpleContent complex types hold text content (asset
|
||||
// id / enum / define / string), so they need an explicit closing tag and
|
||||
// a value placeholder instead of a self-closing tag that can never
|
||||
// contain a value.
|
||||
if (type && model.contentInfoOfType(type)) {
|
||||
return new vscode.SnippetString(`${open}${name}>$1</${name}>`);
|
||||
}
|
||||
const info = type ? model.typeInfo(type) : undefined;
|
||||
const hasChildren = info?.kind === "complex" && info.children.length > 0;
|
||||
if (hasChildren) {
|
||||
return new vscode.SnippetString(`${open}${name}>\n\t$0\n</${name}>`);
|
||||
@@ -338,6 +346,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
|
||||
// inheritFrom: same element type first, then everything.
|
||||
if (attrName === "inheritfrom") {
|
||||
if (!model.isAssetType(elType)) return [];
|
||||
if (!idx) return [];
|
||||
return this.assetIdItems(idx, el.name, null, prefix, make);
|
||||
}
|
||||
@@ -653,12 +662,14 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
const el = ctx.element;
|
||||
if (!el) return [];
|
||||
const elType = resolveElementType(el);
|
||||
const info = elType ? model.typeInfo(elType) : undefined;
|
||||
const info = elType ? model.contentInfoOfType(elType) : undefined;
|
||||
|
||||
// Simple-content element: the text between the tags is the value itself
|
||||
// (e.g. <CreateObject>CrateDebris_01</CreateObject>), so offer value
|
||||
// completions (asset ids / enums / defines) instead of child elements.
|
||||
if (info?.kind === "simple") {
|
||||
// Simple-content element (simple type or simpleContent complex type):
|
||||
// the text between the tags is the value itself (e.g.
|
||||
// <CreateObject>CrateDebris_01</CreateObject> or
|
||||
// <Sound>AudioFile</Sound>), so offer value completions (asset ids /
|
||||
// enums / defines) instead of child elements.
|
||||
if (info) {
|
||||
return this.simpleContentItems(el, elType, info, document, position, idx);
|
||||
}
|
||||
|
||||
@@ -668,7 +679,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
private simpleContentItems(
|
||||
el: XmlElement,
|
||||
elType: string | null,
|
||||
info: SimpleTypeInfo,
|
||||
info: ContentTypeInfo,
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
idx: ModIndex | null,
|
||||
@@ -756,7 +767,8 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
item.detail = type ? t("RA3 XML · {0}", type) : t("RA3 XML");
|
||||
const doc = child.doc || (info?.kind === "complex" ? info.doc : "");
|
||||
if (doc) item.documentation = new vscode.MarkdownString(doc);
|
||||
if (info?.kind === "simple" && this.simpleContentValueKind(info)) {
|
||||
const contentInfo = type ? model.contentInfoOfType(type) : undefined;
|
||||
if (contentInfo && this.simpleContentValueKind(contentInfo)) {
|
||||
item.command = {
|
||||
command: "editor.action.triggerSuggest",
|
||||
title: t("Suggest content value"),
|
||||
@@ -767,7 +779,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
return items;
|
||||
}
|
||||
|
||||
private simpleContentValueKind(info: SimpleTypeInfo): boolean {
|
||||
private simpleContentValueKind(info: ContentTypeInfo): boolean {
|
||||
return (
|
||||
info.refType != null ||
|
||||
info.enumValues.length > 0 ||
|
||||
|
||||
@@ -451,10 +451,11 @@ export class Ra3Diagnostics {
|
||||
diags: vscode.Diagnostic[],
|
||||
provisional: boolean,
|
||||
): void {
|
||||
// Only simple-content elements carry a text value; complex elements'
|
||||
// "content" is child markup and must not be scanned for value refs.
|
||||
const info = elType ? model.typeInfo(elType) : undefined;
|
||||
if (info?.kind !== "simple") return;
|
||||
// Only simple-content elements carry a text value (simple types and
|
||||
// simpleContent complex types); ordinary complex elements' "content" is
|
||||
// child markup and must not be scanned for value refs.
|
||||
const info = elType ? model.contentInfoOfType(elType) : undefined;
|
||||
if (!info) return;
|
||||
if (el.selfClosing || el.closeTagStart < 0) return;
|
||||
const text = document.getText();
|
||||
const raw = text.slice(el.startTagEnd, el.closeTagStart);
|
||||
|
||||
+11
-6
@@ -59,12 +59,15 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
// Element name.
|
||||
const nameStart = el.start + 1;
|
||||
if (offset >= nameStart && offset <= nameStart + el.name.length) {
|
||||
return this.elementHover(el.name);
|
||||
return this.elementHover(el.name, elType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private elementHover(name: string): vscode.Hover | null {
|
||||
private elementHover(
|
||||
name: string,
|
||||
resolvedType: string | null = null,
|
||||
): vscode.Hover | null {
|
||||
if (name.startsWith("xi:")) {
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendCodeblock(`<${name}>`, "xml");
|
||||
@@ -75,7 +78,9 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
const type = model.elementTypeName(name);
|
||||
const type =
|
||||
resolvedType ??
|
||||
(model.topLevelElementType(name) ?? model.elementTypeName(name));
|
||||
const info = type ? model.typeInfo(type) : undefined;
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendCodeblock(`<${name}>`, "xml");
|
||||
@@ -87,7 +92,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
md.appendMarkdown(
|
||||
`${t(
|
||||
"Attributes: {0} · Children: {1}",
|
||||
info.attributes.length,
|
||||
model.attributesOfType(type).length,
|
||||
info.children.length,
|
||||
)} \n`,
|
||||
);
|
||||
@@ -267,8 +272,8 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
}
|
||||
const targets = resolveContentReferenceTargets(idx, elType, value);
|
||||
if (targets.length) return this.definitionsHover(targets, document);
|
||||
const info = elType ? model.typeInfo(elType) : undefined;
|
||||
const refType = info?.kind === "simple" ? info.refType : null;
|
||||
const info = elType ? model.contentInfoOfType(elType) : undefined;
|
||||
const refType = info?.refType ?? null;
|
||||
return this.noDefinitionHover(refType ? "typed" : "untyped", refType ?? undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
textContentTokenAt,
|
||||
} from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import { attributesOfType, typeInfo } from "../model/schemaModel";
|
||||
import { attributesOfType, contentInfoOfType } from "../model/schemaModel";
|
||||
import {
|
||||
filterAndScoreDefs,
|
||||
isReferenceAttributeOfType,
|
||||
@@ -76,10 +76,10 @@ export function referenceContextAt(
|
||||
if (elType && isReferenceContentType(elType)) {
|
||||
const token = textContentTokenAt(text, el, offset);
|
||||
if (token && !token.value.startsWith("$") && !token.value.startsWith("=")) {
|
||||
const info = typeInfo(elType);
|
||||
const info = contentInfoOfType(elType);
|
||||
return {
|
||||
id: token.value,
|
||||
refType: info?.kind === "simple" ? info.refType : null,
|
||||
refType: info?.refType ?? null,
|
||||
selfType: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import type { LineMap, XmlDocument } from "../language/xmlParser";
|
||||
import type { ShallowDocument } from "./shallowScan";
|
||||
import { attributesOfType, typeInfo } from "../model/schemaModel";
|
||||
import { attributesOfType, contentInfoOfType } from "../model/schemaModel";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import {
|
||||
isReferenceAttributeOfType,
|
||||
@@ -246,10 +246,10 @@ function collectReferenceRecords(
|
||||
continue;
|
||||
}
|
||||
const start = el.startTagEnd + raw.indexOf(value);
|
||||
const info = typeInfo(elType);
|
||||
const info = contentInfoOfType(elType);
|
||||
out.push({
|
||||
kind: "content",
|
||||
refType: info?.kind === "simple" ? info.refType : null,
|
||||
refType: info?.refType ?? null,
|
||||
selfType: null,
|
||||
value,
|
||||
line: lineOf(lineMap, start),
|
||||
|
||||
+33
-11
@@ -2,7 +2,9 @@ import {
|
||||
allTypeNames,
|
||||
attributesOfType,
|
||||
canonicalTypeName,
|
||||
contentInfoOfType,
|
||||
elementTypeName,
|
||||
isAssetType,
|
||||
isAssignableTo,
|
||||
typeChain,
|
||||
typeInfo,
|
||||
@@ -74,7 +76,7 @@ export function isReferenceAttributeOfType(
|
||||
typeName: string | null,
|
||||
attrName: string,
|
||||
): boolean {
|
||||
if (attrName.toLowerCase() === "inheritfrom") return true;
|
||||
if (attrName.toLowerCase() === "inheritfrom") return isAssetType(typeName);
|
||||
const attr = attributesOfType(typeName).find((a) => a.name === attrName);
|
||||
if (attr == null || !(attr.refType != null || attr.isRef)) return false;
|
||||
// Definitions (id) and pipeline-local references (Poid) are not references
|
||||
@@ -125,6 +127,7 @@ export function resolveReferenceTargetsForType(
|
||||
let selfType: string | null = null;
|
||||
|
||||
if (nameLower === "inheritfrom") {
|
||||
if (!isAssetType(typeName)) return [];
|
||||
selfType = typeName;
|
||||
} else {
|
||||
const attr = attributesOfType(typeName).find((a) => a.name === attrName);
|
||||
@@ -141,7 +144,9 @@ export function resolveReferenceTargetsForType(
|
||||
/**
|
||||
* True when an element's text content is a typed reference to a global
|
||||
* asset: the element's resolved XSD type is a simple type carrying an
|
||||
* `xas:refType` (e.g. `<CreateObject>` with `GameObjectWeakRef`).
|
||||
* `xas:refType`, or a simpleContent complex type carrying `xas:refType`
|
||||
* (e.g. `<CreateObject>` with `GameObjectWeakRef`, `<Sound>` with
|
||||
* `AudioFileRefWithWeight`).
|
||||
*
|
||||
* Only *typed* refs are treated as content references. Generic untyped
|
||||
* `AssetReference` content is used by real data for shader constants,
|
||||
@@ -152,8 +157,8 @@ export function resolveReferenceTargetsForType(
|
||||
*/
|
||||
export function isReferenceContentType(typeName: string | null): boolean {
|
||||
if (!typeName) return false;
|
||||
const info = typeInfo(typeName);
|
||||
if (info?.kind !== "simple") return false;
|
||||
const info = contentInfoOfType(typeName);
|
||||
if (!info) return false;
|
||||
if (typeChain(typeName).includes("Poid")) return false;
|
||||
return info.refType != null;
|
||||
}
|
||||
@@ -175,8 +180,8 @@ export function resolveContentReferenceTargets(
|
||||
idx.assetsById.get(id.toLowerCase()),
|
||||
);
|
||||
if (!defs.length) return [];
|
||||
const info = typeInfo(typeName);
|
||||
const refType = info?.kind === "simple" ? info.refType : null;
|
||||
const info = contentInfoOfType(typeName);
|
||||
const refType = info?.refType ?? null;
|
||||
return filterAndScoreDefs(defs, refType, null);
|
||||
}
|
||||
|
||||
@@ -223,13 +228,29 @@ export function mergeLocalAndGlobalDefs(
|
||||
|
||||
let referenceTargetTypeSet: Set<string> | null = null;
|
||||
|
||||
/**
|
||||
* True when the XSD itself declares `inheritFrom` for the type. This is the
|
||||
* narrower "designed reference target" signal used by CodeLens / unreferenced
|
||||
* reports; the universal BAB `inheritFrom` attribute must not widen it to
|
||||
* every BaseAssetType descendant.
|
||||
*/
|
||||
function xsdDeclaresInheritFrom(typeName: string): boolean {
|
||||
const info = typeInfo(typeName);
|
||||
return (
|
||||
info?.kind === "complex" &&
|
||||
info.attributes.some((a) => a.name.toLowerCase() === "inheritfrom")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The set of XSD types that are "reference targets by design": at least one
|
||||
* typed reference attribute / simple-content reference points at them, or
|
||||
* they are inheritable (`inheritFrom`). Types outside this set are
|
||||
* auto-registered / structural (settings, map metadata, w3x sub-assets...),
|
||||
* so a zero reference count is their normal state and counts would only be
|
||||
* noise.
|
||||
* the XSD explicitly declares them inheritable (`inheritFrom`). The universal
|
||||
* BAB `inheritFrom` attribute on every BaseAssetType descendant is a separate
|
||||
* legality concern and intentionally does NOT widen this set. Types outside
|
||||
* this set are auto-registered / structural (settings, map metadata, w3x
|
||||
* sub-assets...), so a zero reference count is their normal state and counts
|
||||
* would only be noise.
|
||||
*/
|
||||
export function referenceTargetTypes(): ReadonlySet<string> {
|
||||
if (referenceTargetTypeSet) return referenceTargetTypeSet;
|
||||
@@ -246,7 +267,8 @@ export function referenceTargetTypes(): ReadonlySet<string> {
|
||||
if (isLocalReferenceAttribute(typeName, attr.name)) continue;
|
||||
if (attr.refType) add(attr.refType);
|
||||
}
|
||||
if (info.attributes.some((a) => a.name.toLowerCase() === "inheritfrom")) {
|
||||
if (info.content?.refType) add(info.content.refType);
|
||||
if (xsdDeclaresInheritFrom(typeName)) {
|
||||
add(typeName);
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { childTypeOf, elementTypeName } from "../model/schemaModel";
|
||||
import { childTypeOf, elementTypeName, topLevelElementType } from "../model/schemaModel";
|
||||
import type { XmlElement } from "./xmlParser";
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,12 @@ import type { XmlElement } from "./xmlParser";
|
||||
*/
|
||||
export function resolveElementType(el: XmlElement): string | null {
|
||||
if (!el.parent) {
|
||||
return elementTypeName(el.name);
|
||||
// A document root (fragment or full AssetDeclaration) has no parent to
|
||||
// provide context. When the root is a top-level asset whose name also
|
||||
// appears as a nested child type (EvaEvent, UpgradeTemplate, ...),
|
||||
// prefer the AssetDeclaration declaration over the global single-map
|
||||
// fallback.
|
||||
return topLevelElementType(el.name) ?? elementTypeName(el.name);
|
||||
}
|
||||
const parentType = resolveElementType(el.parent);
|
||||
return childTypeOf(parentType, el.name) ?? elementTypeName(el.name);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+125
-1
@@ -33,6 +33,21 @@ export interface ComplexTypeInfo {
|
||||
attributes: AttributeInfo[];
|
||||
base: string | null;
|
||||
doc: string;
|
||||
/**
|
||||
* Present only for complexType + simpleContent types (e.g.
|
||||
* AudioFileRefWithWeight / MultisoundSubsoundRef). Describes the text
|
||||
* between the tags just like a simple type's value semantics.
|
||||
*/
|
||||
content?: SimpleContentInfo | null;
|
||||
}
|
||||
|
||||
export interface SimpleContentInfo {
|
||||
refType: string | null;
|
||||
isRef: boolean;
|
||||
enumValues: string[];
|
||||
isList: boolean;
|
||||
allowsDefine: boolean;
|
||||
base: string | null;
|
||||
}
|
||||
|
||||
export interface SimpleTypeInfo {
|
||||
@@ -48,6 +63,24 @@ export interface SimpleTypeInfo {
|
||||
|
||||
export type TypeInfo = ComplexTypeInfo | SimpleTypeInfo;
|
||||
|
||||
/**
|
||||
* Unified value semantics for element text content. Both simple types
|
||||
* (`<CreateObject>` -> GameObjectWeakRef) and simpleContent complex types
|
||||
* (`<Sound>` -> AudioFileRefWithWeight) share this shape so the completion /
|
||||
* hover / navigation / diagnostics / indexer pipelines do not have to know
|
||||
* which XSD construct produced the content.
|
||||
*/
|
||||
export interface ContentTypeInfo {
|
||||
kind: "simple" | "simpleContent";
|
||||
refType: string | null;
|
||||
isRef: boolean;
|
||||
enumValues: string[];
|
||||
isList: boolean;
|
||||
allowsDefine: boolean;
|
||||
base: string | null;
|
||||
doc: string;
|
||||
}
|
||||
|
||||
interface RawModel {
|
||||
version: number;
|
||||
rootXsd: string;
|
||||
@@ -59,6 +92,33 @@ interface RawModel {
|
||||
|
||||
const model = schemaModel as unknown as RawModel;
|
||||
|
||||
/**
|
||||
* `inheritFrom` is accepted by BAB / real RA3 data on BaseAssetType-derived
|
||||
* assets even though the XSD only declares it on BaseInheritableAsset
|
||||
* (vanilla SageXml uses it on FXList, AIMicroManagerData,
|
||||
* AITargetingHeuristic, ObjectCreationList, ...). It is therefore exposed as
|
||||
* a universal attribute for every asset type.
|
||||
*
|
||||
* This is deliberately separate from `referenceTargetTypes()` in refs.ts:
|
||||
* "may legally appear in the document" and "is a designed CodeLens / FAR
|
||||
* reference target" are different decisions.
|
||||
*/
|
||||
const UNIVERSAL_INHERIT_FROM: AttributeInfo = {
|
||||
name: "inheritFrom",
|
||||
required: false,
|
||||
default: null,
|
||||
doc: "Inherits another asset of the same type.",
|
||||
kind: "simple",
|
||||
type: "@attr:inheritFrom",
|
||||
refType: null,
|
||||
enumValues: [],
|
||||
isList: false,
|
||||
allowsDefine: false,
|
||||
isRef: false,
|
||||
isBoolean: false,
|
||||
base: "string",
|
||||
};
|
||||
|
||||
/** Lowercase type name -> canonical (XSD) type name. */
|
||||
const typeNameIndex = new Map<string, string>();
|
||||
for (const name of Object.keys(model.types)) {
|
||||
@@ -109,6 +169,43 @@ export function typeInfo(name: string): TypeInfo | undefined {
|
||||
return model.types[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content-value semantics for a type, or null when the element is a
|
||||
* normal complex element (children, not text).
|
||||
*/
|
||||
export function contentInfoOfType(
|
||||
typeName: string | null,
|
||||
): ContentTypeInfo | null {
|
||||
if (!typeName) return null;
|
||||
const info = model.types[canonicalTypeName(typeName) ?? typeName];
|
||||
if (!info) return null;
|
||||
if (info.kind === "simple") {
|
||||
return {
|
||||
kind: "simple",
|
||||
refType: info.refType,
|
||||
isRef: info.isRef,
|
||||
enumValues: info.enumValues,
|
||||
isList: info.isList,
|
||||
allowsDefine: info.allowsDefine,
|
||||
base: info.base,
|
||||
doc: info.doc,
|
||||
};
|
||||
}
|
||||
if (info.kind === "complex" && info.content) {
|
||||
return {
|
||||
kind: "simpleContent",
|
||||
refType: info.content.refType,
|
||||
isRef: info.content.isRef,
|
||||
enumValues: info.content.enumValues,
|
||||
isList: info.content.isList,
|
||||
allowsDefine: info.content.allowsDefine,
|
||||
base: info.content.base,
|
||||
doc: info.doc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function elementTypeName(name: string): string | null {
|
||||
const t = elementToType.get(name);
|
||||
return t ? t : null;
|
||||
@@ -135,7 +232,23 @@ export function attributesOfElement(name: string): AttributeInfo[] {
|
||||
export function attributesOfType(typeName: string | null): AttributeInfo[] {
|
||||
if (!typeName) return [];
|
||||
const info = model.types[canonicalTypeName(typeName) ?? typeName];
|
||||
return info && info.kind === "complex" ? info.attributes : [];
|
||||
if (!info || info.kind !== "complex") return [];
|
||||
if (
|
||||
isAssetType(typeName) &&
|
||||
!info.attributes.some((a) => a.name === "inheritFrom")
|
||||
) {
|
||||
return [...info.attributes, UNIVERSAL_INHERIT_FROM];
|
||||
}
|
||||
return info.attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* True for types in the asset hierarchy (BaseAssetType and its descendants).
|
||||
* These are the types on which BAB accepts the universal `inheritFrom`
|
||||
* attribute even when the XSD does not declare it.
|
||||
*/
|
||||
export function isAssetType(typeName: string | null): boolean {
|
||||
return !!typeName && typeChain(typeName).includes("BaseAssetType");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,6 +283,17 @@ export function elementTypeIn(
|
||||
return elementTypeName(childName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a top-level asset element name to the type declared inside
|
||||
* AssetDeclaration. This is the type a fragment/standalone document root
|
||||
* should use when its name collides with a nested child type (e.g. EvaEvent
|
||||
* is both a top-level asset and an FXNugget child).
|
||||
*/
|
||||
export function topLevelElementType(name: string): string | null {
|
||||
const declType = elementTypeName("AssetDeclaration");
|
||||
return declType ? childTypeOf(declType, name) : null;
|
||||
}
|
||||
|
||||
export function typeDoc(name: string): string {
|
||||
const info = model.types[name];
|
||||
return info?.doc ?? "";
|
||||
|
||||
@@ -308,6 +308,21 @@ test("element and attribute name completions work without an index", async () =>
|
||||
assert.ok(labels.includes("Surfaces"));
|
||||
});
|
||||
|
||||
test("universal inheritFrom is offered on asset attribute completion", async () => {
|
||||
const text = `<AssetDeclaration>\n <FXList `;
|
||||
const line1 = text.split("\n")[1];
|
||||
const pos = new Position(1, line1.length);
|
||||
|
||||
const items = await providerNoIndex.provideCompletionItems(
|
||||
makeDocument(text),
|
||||
pos,
|
||||
token,
|
||||
);
|
||||
const labels = listItems(items).map((i) => i.label);
|
||||
assert.ok(labels.includes("inheritFrom"), "FXList offers universal inheritFrom");
|
||||
assert.ok(labels.includes("id"));
|
||||
});
|
||||
|
||||
test("attribute completion after a closed quote inserts a space", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
@@ -762,6 +777,75 @@ test("simple-content value completion works before the closing tag is typed", as
|
||||
assert.equal(item.range.end.character, pos.character);
|
||||
});
|
||||
|
||||
test("simpleContent complex child inserts a value pair and triggers suggest", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="A">\n` +
|
||||
` <S`;
|
||||
const line = text.split("\n")[2];
|
||||
const pos = new Position(2, line.length);
|
||||
|
||||
const items = await providerNoIndex.provideCompletionItems(
|
||||
makeDocument(text),
|
||||
pos,
|
||||
token,
|
||||
);
|
||||
const sound = listItems(items).find((i) => i.label === "Sound");
|
||||
assert.ok(sound, "Sound child is offered under AudioEvent");
|
||||
assert.equal(sound.insertText.value, "Sound>$1</Sound>");
|
||||
assert.ok(sound.command, "simpleContent child re-triggers value suggest");
|
||||
});
|
||||
|
||||
test("simpleContent complex text offers typed asset ids as the value", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="A">\n` +
|
||||
` <Sound>V</Sound>\n` +
|
||||
` </AudioEvent>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const line = text.split("\n")[2];
|
||||
const pos = new Position(2, line.indexOf(">V") + 2);
|
||||
const audioFile = {
|
||||
type: "AudioFile",
|
||||
id: "VoiceFile",
|
||||
file: "AudioFiles.xml",
|
||||
line: 1,
|
||||
origin: "project",
|
||||
};
|
||||
const audioEvent = {
|
||||
type: "AudioEvent",
|
||||
id: "VoiceEvent",
|
||||
file: "Voice.xml",
|
||||
line: 1,
|
||||
origin: "project",
|
||||
};
|
||||
const idx = {
|
||||
assets: new Map([
|
||||
["AudioFile", new Map([["voicefile", [audioFile]]])],
|
||||
["AudioEvent", new Map([["voiceevent", [audioEvent]]])],
|
||||
]),
|
||||
assetsById: new Map([
|
||||
["voicefile", [audioFile]],
|
||||
["voiceevent", [audioEvent]],
|
||||
]),
|
||||
};
|
||||
|
||||
const items = await makeProvider(idx).provideCompletionItems(
|
||||
makeDocument(text),
|
||||
pos,
|
||||
token,
|
||||
);
|
||||
const labels = items.map((i) => i.label);
|
||||
assert.ok(labels.includes("VoiceFile"));
|
||||
assert.ok(
|
||||
!labels.includes("VoiceEvent"),
|
||||
"Sound content is filtered by AudioFileRefWithWeight refType AudioFile",
|
||||
);
|
||||
const item = items.find((i) => i.label === "VoiceFile");
|
||||
assert.equal(item.range.start.character, line.indexOf(">V") + 1);
|
||||
assert.equal(item.range.end.character, pos.character);
|
||||
});
|
||||
|
||||
test("content start after accepting a simple-content snippet offers values, not attributes", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
|
||||
@@ -239,6 +239,88 @@ test("Ctrl+click on simple-content text jumps to the definition", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("hover on simpleContent complex content shows the referenced definition", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="A">\n` +
|
||||
` <Sound>VoiceFile</Sound>\n` +
|
||||
` </AudioEvent>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const def = {
|
||||
type: "AudioFile",
|
||||
id: "VoiceFile",
|
||||
file: URI,
|
||||
line: 2,
|
||||
origin: "project",
|
||||
};
|
||||
const scope = await makeScope(text, makeIdx([def]));
|
||||
const provider = new Ra3HoverProvider({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
searchPaths: () => null,
|
||||
});
|
||||
const line = text.split("\n")[2];
|
||||
const pos = new Position(2, line.indexOf("VoiceFile") + 3);
|
||||
const hover = await provider.provideHover(makeDocument(text), pos, {});
|
||||
assert.ok(hover, "hover is returned for AudioFileRefWithWeight content");
|
||||
assert.match(hover.contents.value, /1 definition/);
|
||||
assert.match(hover.contents.value, /AudioFile/);
|
||||
});
|
||||
|
||||
test("Ctrl+click on simpleContent complex content jumps to the definition", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="A">\n` +
|
||||
` <Sound>VoiceFile</Sound>\n` +
|
||||
` </AudioEvent>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const def = {
|
||||
type: "AudioFile",
|
||||
id: "VoiceFile",
|
||||
file: URI,
|
||||
line: 2,
|
||||
origin: "project",
|
||||
};
|
||||
const scope = await makeScope(text, makeIdx([def]));
|
||||
const provider = new Ra3DefinitionProvider({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: { definitionMode: "all" },
|
||||
indexer: null,
|
||||
});
|
||||
const line = text.split("\n")[2];
|
||||
const pos = new Position(2, line.indexOf("VoiceFile") + 3);
|
||||
const locations = await provider.provideDefinition(makeDocument(text), pos, {});
|
||||
assert.ok(locations && locations.length === 1);
|
||||
assert.equal(locations[0].uri.fsPath, URI);
|
||||
});
|
||||
|
||||
test("diagnostics report unresolved simpleContent complex content references", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <Multisound id="M">\n` +
|
||||
` <Subsound>MissingEvent</Subsound>\n` +
|
||||
` </Multisound>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: false,
|
||||
reportUnresolvedReferences: "warning",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const messages = collection.last.diags.map((d) => d.message);
|
||||
assert.ok(
|
||||
messages.some((m) => m.includes('Unresolved reference "MissingEvent"')),
|
||||
"MultisoundSubsoundRef text is diagnosed as a typed content reference",
|
||||
);
|
||||
});
|
||||
|
||||
test("Ctrl+click on a manifest definition maps to SageXml even when the mod shadows the DATA path", async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "ra3-nav-manifest-"));
|
||||
try {
|
||||
@@ -518,6 +600,103 @@ test("fragment diagnostics ignore unknown wrapper roots and still report missing
|
||||
);
|
||||
});
|
||||
|
||||
test("diagnostics accept universal inheritFrom on asset types", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <FXList id="FX_A" inheritFrom="FX_Base">\n` +
|
||||
` <NuggetList/>\n` +
|
||||
` </FXList>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "none",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
!codes.includes("unknown-attribute"),
|
||||
"FXList inheritFrom must not be flagged as unknown",
|
||||
);
|
||||
|
||||
const badText =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <FXList id="FX_B" Bogus="x">\n` +
|
||||
` <NuggetList/>\n` +
|
||||
` </FXList>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const badScope = await makeScope(badText, makeIdx([]));
|
||||
const badCollection = new FakeDiagnosticCollection();
|
||||
const badProvider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => badScope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "none",
|
||||
},
|
||||
});
|
||||
badProvider["collection"] = badCollection;
|
||||
await badProvider.update(makeDocument(badText));
|
||||
assert.ok(
|
||||
badCollection.last.diags.map((d) => d.code).includes("unknown-attribute"),
|
||||
"a real unknown attribute is still reported",
|
||||
);
|
||||
});
|
||||
|
||||
test("diagnostics keep simpleContent extension attributes known", async () => {
|
||||
const text =
|
||||
`<AssetDeclaration>\n` +
|
||||
` <AudioEvent id="A">\n` +
|
||||
` <Sound Weight="100">AudioFile</Sound>\n` +
|
||||
` </AudioEvent>\n` +
|
||||
`</AssetDeclaration>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "none",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
!codes.includes("unknown-attribute"),
|
||||
"Weight on AudioFileRefWithWeight must be known",
|
||||
);
|
||||
});
|
||||
|
||||
test("diagnostics use the top-level asset type for colliding fragment roots", async () => {
|
||||
const text =
|
||||
`<EvaEvent id="IncomingTransmission" Priority="100" TimeBetweenEvents="0ms" ExpirationTime="10000ms"/>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "none",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
!codes.includes("unknown-attribute"),
|
||||
"EvaEvent fragment root attributes must resolve against the top-level asset type",
|
||||
);
|
||||
});
|
||||
|
||||
test("full documents still require ids on top-level assets", async () => {
|
||||
const text = `<AssetDeclaration>\n <GameObject/>\n</AssetDeclaration>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
|
||||
@@ -113,3 +113,26 @@ test("extractIndexRecords records typed references and skips non-references", ()
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("extractIndexRecords records simpleContent complex text as references", () => {
|
||||
const text = `<AssetDeclaration>
|
||||
<AudioEvent id="A">
|
||||
<Sound Weight="100">VoiceFile</Sound>
|
||||
</AudioEvent>
|
||||
<Multisound id="M">
|
||||
<Subsound Weight="50">VoiceEvent</Subsound>
|
||||
</Multisound>
|
||||
</AssetDeclaration>`;
|
||||
const lineMap = new LineMap(text);
|
||||
const records = extractIndexRecords(parseXml(text), lineMap, text);
|
||||
const content = records.references.filter((r) => r.kind === "content");
|
||||
|
||||
const sound = content.find((r) => r.value === "VoiceFile");
|
||||
assert.ok(sound, "Sound text is recorded as a content reference");
|
||||
assert.equal(sound.refType, "AudioFile");
|
||||
assert.equal(sound.selfType, null);
|
||||
|
||||
const subsound = content.find((r) => r.value === "VoiceEvent");
|
||||
assert.ok(subsound, "Subsound text is recorded as a content reference");
|
||||
assert.equal(subsound.refType, "BaseAudioEventInfo");
|
||||
});
|
||||
|
||||
@@ -177,3 +177,80 @@ test("FAR from the reference site itself returns the same result", async () => {
|
||||
assert.equal(refs.length, 1);
|
||||
assert.equal(refs[0].range.start.line, 4);
|
||||
});
|
||||
|
||||
test("FAR includes simpleContent complex content references", async () => {
|
||||
const text = `<AssetDeclaration>
|
||||
<AudioFile id="VoiceFile"/>
|
||||
<AudioEvent id="A">
|
||||
<Sound>VoiceFile</Sound>
|
||||
</AudioEvent>
|
||||
</AssetDeclaration>`;
|
||||
const parse = parseXml(text);
|
||||
const lineMap = new LineMap(text);
|
||||
const records = extractIndexRecords(parse, lineMap, text);
|
||||
const def = {
|
||||
type: "AudioFile",
|
||||
id: "VoiceFile",
|
||||
file: FILE,
|
||||
line: 2,
|
||||
origin: "project",
|
||||
};
|
||||
const lookup = {
|
||||
assets: new Map([["AudioFile", new Map([["voicefile", [def]]])]]),
|
||||
assetsById: new Map([["voicefile", [def]]]),
|
||||
};
|
||||
const references = buildReferenceIndex([{ file: FILE, records }], lookup);
|
||||
const idx = {
|
||||
...lookup,
|
||||
references,
|
||||
complete: true,
|
||||
phase: "art",
|
||||
projectDir: "C:/mod",
|
||||
sdkDir: "C:/sdk",
|
||||
defines: new Map(),
|
||||
files: new Map(),
|
||||
streams: [],
|
||||
manifests: new Map(),
|
||||
sourceCandidates: [],
|
||||
diagnostics: [],
|
||||
stats: {},
|
||||
};
|
||||
const scope = { merged: idx };
|
||||
const localParse = parseXml(text);
|
||||
const localLineMap = new LineMap(text);
|
||||
const localIndexer = {
|
||||
readDom: async (path) =>
|
||||
path === FILE
|
||||
? { file: { path: FILE }, parse: localParse, lineMap: localLineMap, records: null }
|
||||
: null,
|
||||
};
|
||||
const localWs = {
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
indexer: localIndexer,
|
||||
indexerForFile: () => localIndexer,
|
||||
activeIndexer: () => localIndexer,
|
||||
recordsSyncSurfaceFor: () => ({
|
||||
get index() {
|
||||
return scope.merged;
|
||||
},
|
||||
invalidate: () => {},
|
||||
scheduleRebuild: () => {},
|
||||
}),
|
||||
};
|
||||
const provider = new Ra3ReferenceProvider(localWs);
|
||||
const document = makeDocument(text);
|
||||
const defLine = text.split("\n")[1];
|
||||
const defPos = new Position(1, defLine.indexOf('id="') + 4);
|
||||
|
||||
const refs = await provider.provideReferences(document, defPos, {
|
||||
includeDeclaration: true,
|
||||
}, {});
|
||||
assert.ok(refs, "references are returned");
|
||||
assert.equal(refs.length, 1);
|
||||
assert.equal(refs[0].range.start.line, 3);
|
||||
assert.equal(
|
||||
text.split("\n")[3].slice(refs[0].range.start.character, refs[0].range.end.character),
|
||||
"VoiceFile",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isReferenceAttribute,
|
||||
isReferenceAttributeOfType,
|
||||
isReferenceContentType,
|
||||
isReferenceTargetType,
|
||||
resolveContentReferenceTargets,
|
||||
resolveReferenceTargets,
|
||||
resolveReferenceTargetsForType,
|
||||
@@ -70,7 +71,25 @@ test("isReferenceAttribute distinguishes references from enums/paths", () => {
|
||||
// Typed references and inheritFrom are references.
|
||||
assert.equal(isReferenceAttribute("GameObject", "CommandSet"), true);
|
||||
assert.equal(isReferenceAttribute("GameObject", "inheritFrom"), true);
|
||||
assert.equal(isReferenceAttribute("FXList", "inheritFrom"), true);
|
||||
assert.equal(isReferenceAttribute("AIMicroManagerData", "inheritFrom"), true);
|
||||
assert.equal(isReferenceAttribute("FireWeaponNugget", "WeaponName"), true);
|
||||
// inheritFrom is an asset-level attribute; non-asset elements stay non-refs.
|
||||
assert.equal(isReferenceAttribute("Include", "inheritFrom"), false);
|
||||
});
|
||||
|
||||
test("universal inheritFrom legality is separate from CodeLens target design", () => {
|
||||
// FXList and other BaseAssetType descendants legally accept inheritFrom,
|
||||
// but the XSD does not declare it there. That must not widen the designed
|
||||
// reference-target set (Credits is still not a CodeLens target).
|
||||
assert.ok(
|
||||
model.attributesOfElement("FXList").some((a) => a.name === "inheritFrom"),
|
||||
);
|
||||
assert.ok(
|
||||
model.attributesOfElement("Credits").some((a) => a.name === "inheritFrom"),
|
||||
);
|
||||
assert.equal(isReferenceTargetType("Credits"), false);
|
||||
assert.equal(isReferenceTargetType("FXList"), true); // via FXListRef, not universal attr
|
||||
});
|
||||
|
||||
test("attribute-level xas:refType is preserved in the model", () => {
|
||||
@@ -287,6 +306,46 @@ test("typed simple content resolves like a typed attribute reference", () => {
|
||||
assert.equal(targets[0].def.type, "GameObject");
|
||||
});
|
||||
|
||||
test("simpleContent complex types resolve as typed content references", () => {
|
||||
// <Sound>AudioFile</Sound> / <Subsound>VoiceEvent</Subsound> use
|
||||
// simpleContent complex types (AudioFileRefWithWeight /
|
||||
// MultisoundSubsoundRef) whose text is still a typed asset reference.
|
||||
assert.equal(isReferenceContentType("AudioFileRefWithWeight"), true);
|
||||
assert.equal(isReferenceContentType("MultisoundSubsoundRef"), true);
|
||||
// Inline Frame's simpleContent is a scalar float, not a reference.
|
||||
assert.equal(isReferenceContentType("@inline:Frame"), false);
|
||||
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
[
|
||||
"shared",
|
||||
[
|
||||
{ type: "AudioFile", id: "Shared", file: "Audio.xml", line: 1, origin: "project" },
|
||||
{ type: "AudioEvent", id: "Shared", file: "Voice.xml", line: 2, origin: "project" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
assets: new Map(),
|
||||
defines: new Map(),
|
||||
};
|
||||
|
||||
const soundTargets = resolveContentReferenceTargets(
|
||||
idx,
|
||||
"AudioFileRefWithWeight",
|
||||
"Shared",
|
||||
);
|
||||
assert.equal(soundTargets.length, 1);
|
||||
assert.equal(soundTargets[0].def.type, "AudioFile");
|
||||
|
||||
const subsoundTargets = resolveContentReferenceTargets(
|
||||
idx,
|
||||
"MultisoundSubsoundRef",
|
||||
"Shared",
|
||||
);
|
||||
assert.equal(subsoundTargets.length, 1);
|
||||
assert.equal(subsoundTargets[0].def.type, "AudioEvent");
|
||||
});
|
||||
|
||||
test("untyped and pipeline-local content is not a global reference", () => {
|
||||
// Generic AssetReference content is used for shader constants and model
|
||||
// sub-object names, not global asset ids; Poid is pipeline-local.
|
||||
|
||||
@@ -17,6 +17,70 @@ test("GameObject has expected attributes", () => {
|
||||
assert.ok(attrs.some((a) => a.name === "inheritFrom"));
|
||||
});
|
||||
|
||||
test("inheritFrom is a universal asset attribute, not only BaseInheritableAsset", () => {
|
||||
// BAB / vanilla data accepts inheritFrom on FXList, AIMicroManagerData,
|
||||
// ObjectCreationList, OnDemandTextureImage and AITargetingHeuristic even
|
||||
// though the XSD only declares it on BaseInheritableAsset.
|
||||
for (const name of [
|
||||
"FXList",
|
||||
"AIMicroManagerData",
|
||||
"ObjectCreationList",
|
||||
"OnDemandTextureImage",
|
||||
"AITargetingHeuristic",
|
||||
]) {
|
||||
assert.ok(
|
||||
model.attributesOfElement(name).some((a) => a.name === "inheritFrom"),
|
||||
`${name} should accept universal inheritFrom`,
|
||||
);
|
||||
}
|
||||
// Structural elements that are not assets must not get the attribute.
|
||||
assert.ok(
|
||||
!model.attributesOfElement("Include").some((a) => a.name === "inheritFrom"),
|
||||
"Include is not an asset and must not accept inheritFrom",
|
||||
);
|
||||
});
|
||||
|
||||
test("simpleContent extension attributes are preserved by the model generator", () => {
|
||||
const sound = model.typeInfo("AudioFileRefWithWeight");
|
||||
assert.equal(sound?.kind, "complex");
|
||||
assert.ok(sound.attributes.some((a) => a.name === "Weight"));
|
||||
assert.ok(sound.attributes.some((a) => a.name === "Volume"));
|
||||
|
||||
const subsound = model.typeInfo("MultisoundSubsoundRef");
|
||||
assert.equal(subsound?.kind, "complex");
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "Weight"));
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "PitchShiftLow"));
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "PitchShiftHigh"));
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "Volume"));
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "PlayPercent"));
|
||||
assert.ok(subsound.attributes.some((a) => a.name === "VolumeShift"));
|
||||
});
|
||||
|
||||
test("contentInfoOfType unifies simple and simpleContent content semantics", () => {
|
||||
const simple = model.contentInfoOfType("GameObjectWeakRef");
|
||||
assert.equal(simple?.kind, "simple");
|
||||
assert.equal(simple?.refType, "GameObject");
|
||||
|
||||
const sound = model.contentInfoOfType("AudioFileRefWithWeight");
|
||||
assert.equal(sound?.kind, "simpleContent");
|
||||
assert.equal(sound?.refType, "AudioFile");
|
||||
assert.equal(sound?.isRef, true);
|
||||
|
||||
const subsound = model.contentInfoOfType("MultisoundSubsoundRef");
|
||||
assert.equal(subsound?.kind, "simpleContent");
|
||||
assert.equal(subsound?.refType, "BaseAudioEventInfo");
|
||||
|
||||
// Ordinary complex elements and structural elements have no content value.
|
||||
assert.equal(model.contentInfoOfType("GameObject"), null);
|
||||
assert.equal(model.contentInfoOfType("Include"), null);
|
||||
|
||||
// Inline simpleContent with a scalar base has content info but no refType.
|
||||
const frame = model.contentInfoOfType("@inline:Frame");
|
||||
assert.ok(frame, "inline simpleContent is exposed through contentInfoOfType");
|
||||
assert.equal(frame?.refType, null);
|
||||
assert.equal(frame?.base, "float");
|
||||
});
|
||||
|
||||
test("attribute-level xas:refType is captured (module ids, map objects)", () => {
|
||||
// ModuleData@id is declared as <xs:attribute name="id" type="Poid"
|
||||
// xas:refType="ModuleData" />; the refType must reach every module subtype.
|
||||
|
||||
@@ -39,3 +39,24 @@ test("model childTypeOf primitives", () => {
|
||||
assert.equal(model.childTypeOf("WeaponSlot_WeaponData", "Weapon"), null);
|
||||
assert.equal(model.childTypeOf(null, "Weapon"), null);
|
||||
});
|
||||
|
||||
test("fragment roots prefer top-level AssetDeclaration types over name collisions", () => {
|
||||
// <EvaEvent> is both a top-level asset and an FXNugget child. A fragment
|
||||
// root has no parent context, so it must resolve to the top-level asset
|
||||
// type (with Priority / TimeBetweenEvents etc.), not to EvaEventFXNugget.
|
||||
const evaDoc = parseXml(
|
||||
`<EvaEvent id="IncomingTransmission" Priority="100" TimeBetweenEvents="0ms" ExpirationTime="10000ms"/>`,
|
||||
);
|
||||
assert.equal(resolveElementType(evaDoc.root), "EvaEvent");
|
||||
assert.ok(
|
||||
model.attributesOfType("EvaEvent").some((a) => a.name === "Priority"),
|
||||
);
|
||||
|
||||
const upgradeDoc = parseXml(`<UpgradeTemplate id="Upgrade_X" inheritFrom="Base"/>`);
|
||||
assert.equal(resolveElementType(upgradeDoc.root), "UpgradeTemplate");
|
||||
|
||||
// Non-top-level fragment roots still fall back to the contextual child
|
||||
// mapping (no AssetDeclaration child exists for Weapon).
|
||||
const weaponDoc = parseXml(`<Weapon Ordering="PRIMARY_WEAPON"/>`);
|
||||
assert.equal(resolveElementType(weaponDoc.root), "WeaponRef");
|
||||
});
|
||||
|
||||
+29
-2
@@ -368,8 +368,13 @@ function expandComplexType(name, chain = []) {
|
||||
const node = complexTypes.get(name);
|
||||
if (!node) return null;
|
||||
|
||||
const extension = node?.complexContent?.extension;
|
||||
const baseName = normalizeTypeName(extension?.["@_base"] ?? node?.simpleContent?.extension?.["@_base"]);
|
||||
// Both `complexContent/extension` and `simpleContent/extension` contribute
|
||||
// attributes. simpleContent types (AudioFileRefWithWeight,
|
||||
// MultisoundSubsoundRef, ...) were previously losing all of theirs because
|
||||
// only the complexContent branch was read.
|
||||
const extension =
|
||||
node?.complexContent?.extension ?? node?.simpleContent?.extension ?? null;
|
||||
const baseName = normalizeTypeName(extension?.["@_base"] ?? null);
|
||||
const base = baseName ? expandComplexType(baseName, [...chain, name]) : null;
|
||||
|
||||
const ownChildren = collectChildren(extension ?? node);
|
||||
@@ -389,6 +394,28 @@ function expandComplexType(name, chain = []) {
|
||||
base: baseName,
|
||||
doc: docOf(node),
|
||||
};
|
||||
if (node?.simpleContent) {
|
||||
const simpleBaseName = normalizeTypeName(
|
||||
node.simpleContent.extension?.["@_base"] ?? null,
|
||||
);
|
||||
const baseDesc = simpleBaseName
|
||||
? resolveTypeDescriptor(simpleBaseName)
|
||||
: null;
|
||||
result.content = {
|
||||
refType:
|
||||
normalizeTypeName(node["@_refType"]) ??
|
||||
normalizeTypeName(baseDesc?.refType) ??
|
||||
null,
|
||||
isRef:
|
||||
node["@_isRef"] === "true" ||
|
||||
node["@_isWeakRef"] === "true" ||
|
||||
baseDesc?.isRef === true,
|
||||
enumValues: baseDesc?.enumValues ?? [],
|
||||
isList: baseDesc?.isList ?? false,
|
||||
allowsDefine: baseDesc?.allowsDefine ?? false,
|
||||
base: simpleBaseName,
|
||||
};
|
||||
}
|
||||
expandedTypes.set(name, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user