fixed manifest references with same id
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.22 — 2026-08-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Manifest assets that share the same id under different types are now all kept in the by-id index. Previously the first same-id entry (e.g. `W3DHierarchy:AUMCV_HOVER`) could shadow later definitions (e.g. `W3DContainer:AUMCV_HOVER`), causing `Model@Name` and other `BaseRenderAssetType` references to be reported as unresolved even though the asset existed in `Static.manifest`.
|
||||||
|
|
||||||
## 0.1.21 — 2026-08-11
|
## 0.1.21 — 2026-08-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1813,3 +1813,96 @@ manifest 候选就会被劫持到 mod 文件。
|
|||||||
|
|
||||||
> 备注:ART/AUDIO 源映射按用户意见不作为本轮目标;`buildVanillaSearchPaths`
|
> 备注:ART/AUDIO 源映射按用户意见不作为本轮目标;`buildVanillaSearchPaths`
|
||||||
> 已包含对应 SDK 目录,将来若有源码可直接复用。
|
> 已包含对应 SDK 目录,将来若有源码可直接复用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二十九、问题分析(2026-08-11):manifest 同名不同类型资产被 `assetsById` 去重丢弃
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
|
||||||
|
Corona `Data\Allied\Units\AlliedMCV.xml` 的
|
||||||
|
`ScriptedModelDraw → ModelConditionState → Model Name="AUMCV_Hover"`
|
||||||
|
报 unresolved-reference:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ScriptedModelDraw id="ModuleTag_Draw_Hover" OkToChangeModelColor="true">
|
||||||
|
<ModelConditionState ParseCondStateType="PARSE_DEFAULT">
|
||||||
|
<Model Name="AUMCV_Hover" />
|
||||||
|
</ModelConditionState>
|
||||||
|
</ScriptedModelDraw>
|
||||||
|
```
|
||||||
|
|
||||||
|
提示为“没有类型为 `BaseRenderAssetType` 的定义(其他类型存在同名 id)”,
|
||||||
|
但 `Static.manifest` 中确实存在 `W3DContainer:AUMCV_HOVER`。
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
|
||||||
|
`src/indexer/indexer.ts` 的 `addAsset()` 在维护两个索引时用了同一套去重:
|
||||||
|
|
||||||
|
- `assets`:`类型 -> id -> 定义`,按 `(file, line)` 去重;
|
||||||
|
- `assetsById`:`id -> 所有类型定义`,也按 `(file, line)` 去重。
|
||||||
|
|
||||||
|
XML 定义的行号各不相同,所以 `(file, line)` 足够;但 manifest 资产入库时
|
||||||
|
`line` 固定为 0,于是同一个 manifest 里 id 相同、类型不同的多个资产会被
|
||||||
|
当成同一条定义,只保留最先出现的类型。
|
||||||
|
|
||||||
|
`Static.manifest` 中 `AUMCV_HOVER` 的实际顺序是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
W3DHierarchy:AUMCV_HOVER
|
||||||
|
W3DAnimation:AUMCV_HOVER
|
||||||
|
W3DContainer:AUMCV_HOVER
|
||||||
|
```
|
||||||
|
|
||||||
|
`W3DContainer` 因此被 `W3DHierarchy` 挤掉。`Model@Name` 的 `refType` 是
|
||||||
|
`BaseRenderAssetType`,`W3DHierarchy` 按 XSD 继承链不是渲染资产,所以
|
||||||
|
`assetsById` 里“有同名 id”但“没有匹配类型”,正好产生上述提示。
|
||||||
|
|
||||||
|
### 为什么以前没暴露 / 不是回归
|
||||||
|
|
||||||
|
第三轮修复的 `AUAntiVehicleVehicleTech1_SKN` 在 static.manifest 里只有一个
|
||||||
|
同名定义(`W3DContainer`),没有类型竞争,因此当时测不到该分支。git blame
|
||||||
|
显示 `addAsset` 的 `(file, line)` 去重从首个提交就存在,所以这是潜在缺陷被
|
||||||
|
新数据形态首次触发,不是近期改动造成的回归。
|
||||||
|
|
||||||
|
### 影响面(真实 manifest 扫描)
|
||||||
|
|
||||||
|
对 `Static / Global / Audio` 三个 manifest 模拟当前入库逻辑:
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|---|---:|
|
||||||
|
| 同名 id 跨类型的 ID | 1318 |
|
||||||
|
| 被丢弃的类型定义 | 1470 |
|
||||||
|
| `W3DContainer` / `W3DMesh` 被丢弃的 id | 412 |
|
||||||
|
|
||||||
|
`Audio.manifest` 无此类碰撞。受影响的不止诊断和 hover:
|
||||||
|
`resolveReferenceTargetsForType`、语义 FAR / CodeLens 引用计数、未类型化补全
|
||||||
|
都经 `assetsById` 查找,因此 manifest 中的模型引用普遍可能误报或漏计。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
`assetsById` 是“按 id 汇总所有类型定义”的索引,去重身份必须包含类型:
|
||||||
|
|
||||||
|
1. `src/indexer/indexer.ts` 的 `addAsset()`:`assets` 与 `assetsById` 的去重
|
||||||
|
都改为 `(type, file, line)`;
|
||||||
|
2. `src/indexer/localScope.ts` 的 `addAsset()`:同样的去重修正,避免局部
|
||||||
|
overlay 未来遇到同构数据时重复踩坑。
|
||||||
|
|
||||||
|
`mergeLocalAndGlobalDefs`、`assetDefKey` 本来已按 `(type, id, file, line)`
|
||||||
|
区分定义,修复后三处语义一致。
|
||||||
|
|
||||||
|
### 测试(新增 1 个集成测试,全量 198 个通过)
|
||||||
|
|
||||||
|
`test/indexer.test.mjs` 新增自包含用例:
|
||||||
|
|
||||||
|
- 用最小 version-5 manifest 构造 `W3DHierarchy / W3DAnimation / W3DContainer`
|
||||||
|
三个同 id 资产,顺序刻意让渲染类型排在最后;
|
||||||
|
- 再构造 `Texture:ABAirfield` 在前、`W3DContainer:ABAIRFIELD` 在后的常见形态;
|
||||||
|
- 断言 `assetsById` 保留全部类型;
|
||||||
|
- 断言 `Model@Name` 经 `resolveReferenceTargetsForType` 命中 `W3DContainer`;
|
||||||
|
- 断言反向引用索引把该引用记到 `W3DContainer` 名下。
|
||||||
|
|
||||||
|
### 文档同步
|
||||||
|
|
||||||
|
`docs/plan.md` 的 manifest 建模小节补充:`assetsById` 必须保留同 id 的不同
|
||||||
|
类型定义,去重身份为 `(type, file, line)`。
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# 调研结论与实施计划(已按最新代码同步更新)
|
# 调研结论与实施计划(已按最新代码同步更新)
|
||||||
|
|
||||||
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-10)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)、simple-content 元素文本引用(补全 / hover / 跳转 / 诊断 / Find All References)、语义引用索引 / CodeLens 引用计数 / 未引用资产命令、属性补全换行判定与按 id 去重、manifest 源地址按 vanilla-only 解析(避免 mod 同名 DATA 路径遮蔽)等。
|
> 说明:本文档随实现演进持续同步。最近一次同步(2026-08-11)对齐了实现过程中新增的模块与设计变更:BAB 精确搜索路径、manifest 类型/ID 推导、上下文感知元素类型、属性级 refType / Poid 局部引用(`id` 定义点)、精确跳转范围、嵌套 `xi:include`、注入式语法高亮、bit-flag 列表补全(空格触发 / 排除已用 / 追加模式)、simple-content 元素文本引用(补全 / hover / 跳转 / 诊断 / Find All References)、语义引用索引 / CodeLens 引用计数 / 未引用资产命令、属性补全换行判定与按 id 去重、manifest 源地址按 vanilla-only 解析(避免 mod 同名 DATA 路径遮蔽)、`assetsById` 保留同 id 的不同类型 manifest 定义等。
|
||||||
|
|
||||||
## 一、调研结论(带证据)
|
## 一、调研结论(带证据)
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ test/
|
|||||||
`W3DHierarchy` / `W3DCollisionBox` 等),使 `Model@Name`、`Hierarchy`、`Mesh`
|
`W3DHierarchy` / `W3DCollisionBox` 等),使 `Model@Name`、`Hierarchy`、`Mesh`
|
||||||
等引用可解析;大模型文件**浅扫描**(不建 DOM),结果缓存在 workspace 级、
|
等引用可解析;大模型文件**浅扫描**(不建 DOM),结果缓存在 workspace 级、
|
||||||
跨重建复用(详见设计决策 14)。
|
跨重建复用(详见设计决策 14)。
|
||||||
3. **manifest 资产建模**:类型优先用哈希表,未知时从名称前缀推导;可引用 ID 取最后冒号段;类型名统一走大小写规范化(`W3dContainer` ↔ `W3DContainer`),类型匹配严格遵循 XSD 继承链。
|
3. **manifest 资产建模**:类型优先用哈希表,未知时从名称前缀推导;可引用 ID 取最后冒号段;类型名统一走大小写规范化(`W3dContainer` ↔ `W3DContainer`),类型匹配严格遵循 XSD 继承链。`assetsById` 按 id 汇总**全部类型**的定义,去重身份为 `(type, file, line)`,同一 manifest 中同名但不同类型的美术资产(如 `W3DHierarchy:AUMCV_HOVER` 与 `W3DContainer:AUMCV_HOVER`)必须全部保留,避免 `Model@Name` 这类 `BaseRenderAssetType` 引用因先到的非渲染类型而被误判为未解析。
|
||||||
4. **上下文感知元素类型**:同名元素按父元素类型解析(`resolveElementType` 沿解析树逐层 `childTypeOf`,失败回退全局映射),保证 `<Weapon>` 等元素的属性/引用判定正确。
|
4. **上下文感知元素类型**:同名元素按父元素类型解析(`resolveElementType` 沿解析树逐层 `childTypeOf`,失败回退全局映射),保证 `<Weapon>` 等元素的属性/引用判定正确。
|
||||||
5. **引用判定与解析**:`refType` 或 `isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);`inheritFrom` 按可继承类型过滤。**局部作用域例外**(`isLocalReferenceAttribute`):`id` 是元素自身的定义点——无 refType 或 refType 与自身类型兼容时不检查、不解析(`RoadObject@id→Road` 这类跨类型 id 引用保留检查);Poid 类型属性是管线局部引用,全局索引无法判定,不检查、不解析。
|
5. **引用判定与解析**:`refType` 或 `isRef` 均视为引用;带 `refType` 时严格按类型过滤(同名 ID 不串类型);`inheritFrom` 按可继承类型过滤。**局部作用域例外**(`isLocalReferenceAttribute`):`id` 是元素自身的定义点——无 refType 或 refType 与自身类型兼容时不检查、不解析(`RoadObject@id→Road` 这类跨类型 id 引用保留检查);Poid 类型属性是管线局部引用,全局索引无法判定,不检查、不解析。
|
||||||
6. **重复 ID 诊断**:与 `check_duplicate_ids.py` 一致——SageXml 不参与冲突判定,mod 覆盖原版视为正常。
|
6. **重复 ID 诊断**:与 `check_duplicate_ids.py` 一致——SageXml 不参与冲突判定,mod 覆盖原版视为正常。
|
||||||
|
|||||||
+20
-2
@@ -1014,14 +1014,32 @@ export class ModIndexer {
|
|||||||
}
|
}
|
||||||
const arr = byId.get(idKey);
|
const arr = byId.get(idKey);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
if (arr.some((a) => a.file === def.file && a.line === def.line)) return;
|
if (
|
||||||
|
arr.some(
|
||||||
|
(a) =>
|
||||||
|
a.type === def.type &&
|
||||||
|
a.file === def.file &&
|
||||||
|
a.line === def.line,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
arr.push(def);
|
arr.push(def);
|
||||||
} else {
|
} else {
|
||||||
byId.set(idKey, [def]);
|
byId.set(idKey, [def]);
|
||||||
}
|
}
|
||||||
const all = this.assetsById.get(idKey);
|
const all = this.assetsById.get(idKey);
|
||||||
if (all) {
|
if (all) {
|
||||||
if (all.some((a) => a.file === def.file && a.line === def.line)) return;
|
if (
|
||||||
|
all.some(
|
||||||
|
(a) =>
|
||||||
|
a.type === def.type &&
|
||||||
|
a.file === def.file &&
|
||||||
|
a.line === def.line,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
all.push(def);
|
all.push(def);
|
||||||
} else {
|
} else {
|
||||||
this.assetsById.set(idKey, [def]);
|
this.assetsById.set(idKey, [def]);
|
||||||
|
|||||||
@@ -244,14 +244,32 @@ class OverlayBuilder {
|
|||||||
}
|
}
|
||||||
const arr = byId.get(idKey);
|
const arr = byId.get(idKey);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
if (arr.some((a) => a.file === def.file && a.line === def.line)) return;
|
if (
|
||||||
|
arr.some(
|
||||||
|
(a) =>
|
||||||
|
a.type === def.type &&
|
||||||
|
a.file === def.file &&
|
||||||
|
a.line === def.line,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
arr.push(def);
|
arr.push(def);
|
||||||
} else {
|
} else {
|
||||||
byId.set(idKey, [def]);
|
byId.set(idKey, [def]);
|
||||||
}
|
}
|
||||||
const all = this.overlay.assetsById.get(idKey);
|
const all = this.overlay.assetsById.get(idKey);
|
||||||
if (all) {
|
if (all) {
|
||||||
if (all.some((a) => a.file === def.file && a.line === def.line)) return;
|
if (
|
||||||
|
all.some(
|
||||||
|
(a) =>
|
||||||
|
a.type === def.type &&
|
||||||
|
a.file === def.file &&
|
||||||
|
a.line === def.line,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
all.push(def);
|
all.push(def);
|
||||||
} else {
|
} else {
|
||||||
this.overlay.assetsById.set(idKey, [def]);
|
this.overlay.assetsById.set(idKey, [def]);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
IndexRecordsCache,
|
IndexRecordsCache,
|
||||||
} from "../out/indexer/caches.js";
|
} from "../out/indexer/caches.js";
|
||||||
import { resolveReferenceTargetsForType } from "../out/indexer/refs.js";
|
import { resolveReferenceTargetsForType } from "../out/indexer/refs.js";
|
||||||
|
import { assetDefKey } from "../out/indexer/referenceIndex.js";
|
||||||
|
|
||||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
const project = join(root, "test", "fixtures", "minimod");
|
const project = join(root, "test", "fixtures", "minimod");
|
||||||
@@ -29,6 +30,75 @@ async function buildIndex() {
|
|||||||
return indexer.build();
|
return indexer.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function u32(value) {
|
||||||
|
const b = Buffer.alloc(4);
|
||||||
|
b.writeUInt32LE(value >>> 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function u16(value) {
|
||||||
|
const b = Buffer.alloc(2);
|
||||||
|
b.writeUInt16LE(value >>> 0);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal version-5 manifest with one asset entry per supplied descriptor. */
|
||||||
|
function minimalManifestV5(assets) {
|
||||||
|
const nameParts = [];
|
||||||
|
const sourceParts = [];
|
||||||
|
let nameOffset = 0;
|
||||||
|
let sourceOffset = 0;
|
||||||
|
const entries = assets.map((asset) => {
|
||||||
|
const name = Buffer.from(`${asset.name}\0`, "ascii");
|
||||||
|
const source = Buffer.from(`${asset.source ?? ""}\0`, "ascii");
|
||||||
|
const entry = {
|
||||||
|
typeId: asset.typeId,
|
||||||
|
nameOffset,
|
||||||
|
sourceFileNameOffset: sourceOffset,
|
||||||
|
};
|
||||||
|
nameParts.push(name);
|
||||||
|
sourceParts.push(source);
|
||||||
|
nameOffset += name.length;
|
||||||
|
sourceOffset += source.length;
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
const names = Buffer.concat(nameParts);
|
||||||
|
const sources = Buffer.concat(sourceParts);
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
Buffer.from([0, 1]), // isBigEndian=false, isLinked=true
|
||||||
|
u16(5), // version
|
||||||
|
u32(0), // streamChecksum
|
||||||
|
u32(0), // allTypesHash
|
||||||
|
u32(assets.length), // assetCount
|
||||||
|
u32(0), // totalInstanceDataSize
|
||||||
|
u32(0), // maxInstanceChunkSize
|
||||||
|
u32(0), // maxRelocationChunkSize
|
||||||
|
u32(0), // maxImportsChunkSize
|
||||||
|
u32(0), // assetReferenceBufferSize
|
||||||
|
u32(0), // referencedManifestNameBufferSize
|
||||||
|
u32(names.length), // assetNameBufferSize
|
||||||
|
u32(sources.length), // sourceFileNameBufferSize
|
||||||
|
];
|
||||||
|
for (const entry of entries) {
|
||||||
|
parts.push(
|
||||||
|
u32(entry.typeId),
|
||||||
|
u32(0), // instanceId
|
||||||
|
u32(0), // typeHash
|
||||||
|
u32(0), // instanceHash
|
||||||
|
u32(0), // assetReferenceOffset
|
||||||
|
u32(0), // assetReferenceCount
|
||||||
|
u32(entry.nameOffset),
|
||||||
|
u32(entry.sourceFileNameOffset),
|
||||||
|
u32(0), // instanceDataSize
|
||||||
|
u32(0), // relocationDataSize
|
||||||
|
u32(0), // importsDataSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parts.push(names, sources);
|
||||||
|
return Buffer.concat(parts);
|
||||||
|
}
|
||||||
|
|
||||||
test("indexes assets, defines, streams and include errors", async () => {
|
test("indexes assets, defines, streams and include errors", async () => {
|
||||||
const idx = await buildIndex();
|
const idx = await buildIndex();
|
||||||
|
|
||||||
@@ -133,6 +203,102 @@ test("w3x files appear in Include source completion candidates", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("manifest assets sharing an id keep every type in assetsById", async () => {
|
||||||
|
const tmp = fs.mkdtempSync(join(os.tmpdir(), "ra3-manifest-multitype-"));
|
||||||
|
const projectDir = join(tmp, "project");
|
||||||
|
const sdkDir = join(tmp, "sdk");
|
||||||
|
const builtmodsDir = join(sdkDir, "builtmods");
|
||||||
|
fs.mkdirSync(join(projectDir, "Data"), { recursive: true });
|
||||||
|
fs.mkdirSync(builtmodsDir, { recursive: true });
|
||||||
|
fs.writeFileSync(join(sdkDir, "Static.xml"), "<AssetDeclaration/>");
|
||||||
|
fs.writeFileSync(
|
||||||
|
join(projectDir, "Data", "Mod.xml"),
|
||||||
|
`<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||||
|
<Includes>
|
||||||
|
<Include type="reference" source="DATA:static.xml" />
|
||||||
|
</Includes>
|
||||||
|
<GameObject id="AlliedMCV">
|
||||||
|
<Draws>
|
||||||
|
<ScriptedModelDraw id="ModuleTag_Draw_Hover">
|
||||||
|
<ModelConditionState ParseCondStateType="PARSE_DEFAULT">
|
||||||
|
<Model Name="AUMCV_Hover" />
|
||||||
|
</ModelConditionState>
|
||||||
|
</ScriptedModelDraw>
|
||||||
|
</Draws>
|
||||||
|
</GameObject>
|
||||||
|
</AssetDeclaration>`,
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
join(builtmodsDir, "static.manifest"),
|
||||||
|
minimalManifestV5([
|
||||||
|
{
|
||||||
|
typeId: 0x11111111,
|
||||||
|
name: "W3DHierarchy:AUMCV_HOVER",
|
||||||
|
source: "ART:aumcv_hover.w3x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeId: 0x22222222,
|
||||||
|
name: "W3DAnimation:AUMCV_HOVER",
|
||||||
|
source: "ART:aumcv_hover.w3x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeId: 0x33333333,
|
||||||
|
name: "W3DContainer:AUMCV_HOVER",
|
||||||
|
source: "ART:aumcv_hover.w3x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeId: 0x44444444,
|
||||||
|
name: "Texture:ABAirfield",
|
||||||
|
source: "ART:abairfield.tga",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeId: 0x55555555,
|
||||||
|
name: "W3DContainer:ABAIRFIELD",
|
||||||
|
source: "ART:abairfield.w3x",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const indexer = new ModIndexer({
|
||||||
|
projectDir,
|
||||||
|
sdkDir,
|
||||||
|
builtmodsDirs: [builtmodsDir],
|
||||||
|
indexSageXml: false,
|
||||||
|
additionalDataSearchPaths: [],
|
||||||
|
walker: new CachedDirectoryWalker(),
|
||||||
|
});
|
||||||
|
const idx = await indexer.build();
|
||||||
|
|
||||||
|
// The reported AUMCV_HOVER shape: Hierarchy/Animation precede the
|
||||||
|
// W3DContainer, so the by-id index must not drop the render asset.
|
||||||
|
const hover = idx.assetsById.get("aumcv_hover");
|
||||||
|
assert.ok(hover?.some((d) => d.type === "W3DContainer"), "W3DContainer retained");
|
||||||
|
assert.ok(hover?.some((d) => d.type === "W3DHierarchy"), "W3DHierarchy retained");
|
||||||
|
assert.ok(hover?.some((d) => d.type === "W3DAnimation"), "W3DAnimation retained");
|
||||||
|
|
||||||
|
const targets = resolveReferenceTargetsForType(
|
||||||
|
idx,
|
||||||
|
"ScriptedModelDrawModel",
|
||||||
|
"Name",
|
||||||
|
"AUMCV_Hover",
|
||||||
|
);
|
||||||
|
assert.equal(targets.length, 1);
|
||||||
|
assert.equal(targets[0].def.type, "W3DContainer");
|
||||||
|
|
||||||
|
const container = hover.find((d) => d.type === "W3DContainer");
|
||||||
|
const sites = idx.references.get(assetDefKey(container));
|
||||||
|
assert.ok(
|
||||||
|
sites?.some((s) => s.kind === "attr" && /Mod\.xml$/.test(s.file)),
|
||||||
|
"Model reference is attributed to the W3DContainer definition",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Common Texture-first shape must also keep the render definition.
|
||||||
|
const airfield = idx.assetsById.get("abairfield");
|
||||||
|
assert.ok(airfield?.some((d) => d.type === "Texture"), "Texture retained");
|
||||||
|
assert.ok(airfield?.some((d) => d.type === "W3DContainer"), "W3DContainer retained");
|
||||||
|
});
|
||||||
|
|
||||||
test("build publishes an immutable XML phase before art scanning", async () => {
|
test("build publishes an immutable XML phase before art scanning", async () => {
|
||||||
let phaseA;
|
let phaseA;
|
||||||
const indexer = new ModIndexer({
|
const indexer = new ModIndexer({
|
||||||
|
|||||||
Reference in New Issue
Block a user