fix inherit from
This commit is contained in:
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user