first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<GameObject id="BaseVehicle" Side="Allies" />
|
||||
</AssetDeclaration>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<GameObject id="SageStaticMarker" />
|
||||
</AssetDeclaration>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<GameObject id="VanillaTank" />
|
||||
<WeaponTemplate id="VanillaCannon" />
|
||||
</AssetDeclaration>
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset"></AssetDeclaration>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset"></AssetDeclaration>
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Includes>
|
||||
<Include type="all" source="SageXml/Vanilla.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<GameObject id="BaseVehicle" Side="Allies" />
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<HeadlightDraw2 id="FragLight">
|
||||
<Model Name="frag.mdl" />
|
||||
</HeadlightDraw2>
|
||||
</AssetDeclaration>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<!-- Same id on two different asset types: navigation must filter by the
|
||||
attribute's reference type. -->
|
||||
<WeaponTemplate id="SharedThing" />
|
||||
<GameObject id="SharedThing" />
|
||||
|
||||
<!-- Nested xi:include: content is inlined into the parent element. -->
|
||||
<GameObject id="XiNestedGameObject">
|
||||
<Draws>
|
||||
<xi:include
|
||||
href="DATA:Includes/Fragment.xml"
|
||||
xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:HeadlightDraw2/child::*)" />
|
||||
<xi:include href="DATA:Missing/Nested.xml" />
|
||||
</Draws>
|
||||
</GameObject>
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Texture id="TestTexture" File="test.dds" />
|
||||
</AssetDeclaration>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Defines>
|
||||
<Define name="TEST_HEALTH" value="100.0" />
|
||||
</Defines>
|
||||
<GameObject
|
||||
id="TestTank"
|
||||
inheritFrom="BaseVehicle"
|
||||
CommandSet="TestTankCommandSet"
|
||||
KindOf="SELECTABLE">
|
||||
</GameObject>
|
||||
<GameObject
|
||||
id="TestTank"
|
||||
Side="Allies">
|
||||
</GameObject>
|
||||
<Includes>
|
||||
<Include type="instance" source="BaseVehicle.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<WeaponTemplate id="TestTankCannon" AttackRange="100.0" />
|
||||
<LogicCommandSet id="TestTankCommandSet" />
|
||||
<Includes>
|
||||
<Include type="all" source="Missing/File.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Tags />
|
||||
<Includes>
|
||||
<Include type="reference" source="DATA:static.xml" />
|
||||
<Include type="reference" source="DATA:global.xml" />
|
||||
<Include type="reference" source="DATA:audio.xml" />
|
||||
<Include type="all" source="Includes/Units.xml" />
|
||||
<Include type="all" source="Includes/Weapons.xml" />
|
||||
<Include type="all" source="Includes/Shared.xml" />
|
||||
<Include type="all" source="Includes/Refs.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<MpGameRules id="TheMpGameRules">
|
||||
<SkirmishStartCash LoCash="10000" HiCash="50000" />
|
||||
</MpGameRules>
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,89 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
buildSearchPaths,
|
||||
resolveSource,
|
||||
manifestPathForReference,
|
||||
} from "../out/indexer/includeResolver.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const project = join(root, "test", "fixtures", "minimod");
|
||||
const sdk = join(root, "test", "fixtures", "fakesdk");
|
||||
const paths = buildSearchPaths(sdk, project);
|
||||
|
||||
test("resolves relative includes against the current file directory", () => {
|
||||
const current = join(project, "Data", "Includes");
|
||||
const r = resolveSource("BaseVehicle.xml", current, paths);
|
||||
assert.equal(r.path, join(project, "Data", "Includes", "BaseVehicle.xml"));
|
||||
});
|
||||
|
||||
test("resolves DATA: includes through search paths", () => {
|
||||
const r = resolveSource("DATA:static.xml", null, paths);
|
||||
assert.equal(r.path, join(sdk, "static.xml"));
|
||||
const r2 = resolveSource("DATA:SageXml/Vanilla.xml", null, paths);
|
||||
assert.equal(r2.path, join(sdk, "SageXml", "Vanilla.xml"));
|
||||
});
|
||||
|
||||
test("case-insensitive prefix", () => {
|
||||
const r = resolveSource("data:static.xml", null, paths);
|
||||
assert.equal(r.path, join(sdk, "static.xml"));
|
||||
});
|
||||
|
||||
test("missing target returns null", () => {
|
||||
const r = resolveSource("DATA:nope/nothere.xml", null, paths);
|
||||
assert.equal(r.path, null);
|
||||
assert.equal(r.prefix, "DATA");
|
||||
});
|
||||
|
||||
test("search paths follow the BinaryAssetBuilder /data /art /audio order", () => {
|
||||
// defaultscript.cs getIncludePaths():
|
||||
// /data: ".;{modGranParent};{0}\Data;.\Mods;{1};.\SageXml"
|
||||
// /art: ".;{modGranParent};{0}\Art1;{0}\Art;.\Mods;{1};.\Art"
|
||||
// /audio: ".;{modGranParent};{0}\Audio1;{0}\Audio;.\Mods;{1};.\Audio"
|
||||
// where "." = sdkDir, {0} = projectDir, {1} = modParentPath,
|
||||
// {2} = modGranParent.
|
||||
const modParent = dirname(project);
|
||||
const granParent = dirname(modParent);
|
||||
assert.deepEqual(paths.DATA, [
|
||||
sdk,
|
||||
granParent,
|
||||
join(project, "Data"),
|
||||
join(sdk, "Mods"),
|
||||
modParent,
|
||||
join(sdk, "SageXml"),
|
||||
]);
|
||||
assert.deepEqual(paths.ART, [
|
||||
sdk,
|
||||
granParent,
|
||||
join(project, "Art1"),
|
||||
join(project, "Art"),
|
||||
join(sdk, "Mods"),
|
||||
modParent,
|
||||
join(sdk, "Art"),
|
||||
]);
|
||||
assert.deepEqual(paths.AUDIO, [
|
||||
sdk,
|
||||
granParent,
|
||||
join(project, "Audio1"),
|
||||
join(project, "Audio"),
|
||||
join(sdk, "Mods"),
|
||||
modParent,
|
||||
join(sdk, "Audio"),
|
||||
]);
|
||||
});
|
||||
|
||||
test("DATA:Static.xml prefers the SDK root over SageXml", () => {
|
||||
// Both the SDK root and SageXml contain a Static.xml; BAB order resolves to
|
||||
// the SDK root placeholder first.
|
||||
const r = resolveSource("DATA:Static.xml", null, paths);
|
||||
assert.equal(r.path, join(sdk, "Static.xml"));
|
||||
});
|
||||
|
||||
test("manifest mapping strips the prefix", () => {
|
||||
const dirs = [join(sdk, "builtmods")];
|
||||
assert.equal(manifestPathForReference("DATA:static.xml", dirs), join(dirs[0], "static.manifest"));
|
||||
assert.equal(manifestPathForReference("DATA:audio.xml", dirs), join(dirs[0], "audio.manifest"));
|
||||
assert.equal(manifestPathForReference("DATA:missing.xml", dirs), null);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { ModIndexer } from "../out/indexer/indexer.js";
|
||||
import { CachedDirectoryWalker } from "../out/indexer/fileScanner.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const project = join(root, "test", "fixtures", "minimod");
|
||||
const sdk = join(root, "test", "fixtures", "fakesdk");
|
||||
|
||||
async function buildIndex() {
|
||||
const indexer = new ModIndexer({
|
||||
projectDir: project,
|
||||
sdkDir: sdk,
|
||||
builtmodsDirs: [join(sdk, "builtmods")],
|
||||
indexSageXml: true,
|
||||
additionalDataSearchPaths: [],
|
||||
walker: new CachedDirectoryWalker(),
|
||||
});
|
||||
return indexer.build();
|
||||
}
|
||||
|
||||
test("indexes assets, defines, streams and include errors", async () => {
|
||||
const idx = await buildIndex();
|
||||
|
||||
assert.equal(idx.stats.streams, 2); // static + global
|
||||
assert.ok(idx.assetsById.has("testtank"));
|
||||
assert.ok(idx.assetsById.has("testtankcannon"));
|
||||
assert.ok(idx.assetsById.has("testtankcommandset"));
|
||||
assert.ok(idx.assetsById.has("testtexture"));
|
||||
assert.ok(idx.assetsById.has("thempgamerules"));
|
||||
assert.ok(idx.assetsById.has("vanillatank"), "vanilla data via reference include fallback");
|
||||
assert.ok(idx.assetsById.has("basevehicle"));
|
||||
|
||||
const baseVehicle = idx.assetsById.get("basevehicle");
|
||||
assert.ok(baseVehicle.some((d) => d.viaInstance), "instance-included asset marked");
|
||||
|
||||
const defs = idx.defines.get("test_health");
|
||||
assert.equal(defs?.[0].value, "100.0");
|
||||
|
||||
const missing = idx.diagnostics.find((d) => d.code === "include-not-found");
|
||||
assert.ok(missing, "missing include reported");
|
||||
assert.match(missing.message, /Missing\/File\.xml/);
|
||||
|
||||
// Nested xi:include: existing target is walked, missing target is reported.
|
||||
assert.ok(
|
||||
idx.diagnostics.some((d) => /Missing\/Nested\.xml/.test(d.message)),
|
||||
"nested missing xi:include reported",
|
||||
);
|
||||
assert.ok(
|
||||
idx.files.has(
|
||||
join(project, "Data", "Includes", "Fragment.xml").toLowerCase(),
|
||||
),
|
||||
"nested xi:include target indexed",
|
||||
);
|
||||
assert.ok(
|
||||
idx.assetsById.has("fraglight"),
|
||||
"nested xi:include target assets available",
|
||||
);
|
||||
|
||||
const staticStream = idx.streams.find((s) => s.name === "static");
|
||||
assert.ok(staticStream.files.size >= 4);
|
||||
});
|
||||
|
||||
test("provides include source candidates", async () => {
|
||||
const idx = await buildIndex();
|
||||
const xml = idx.sourceCandidates.filter((c) => c.source.endsWith(".xml"));
|
||||
assert.ok(xml.length >= 4);
|
||||
assert.ok(xml.some((c) => c.source === "Includes/Units.xml"));
|
||||
assert.ok(xml.some((c) => c.source === "DATA:static.xml"));
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseManifest } from "../out/indexer/manifestParser.js";
|
||||
|
||||
function u32(v) {
|
||||
const b = Buffer.alloc(4);
|
||||
b.writeUInt32LE(v >>> 0);
|
||||
return b;
|
||||
}
|
||||
function u16(v) {
|
||||
const b = Buffer.alloc(2);
|
||||
b.writeUInt16LE(v >>> 0);
|
||||
return b;
|
||||
}
|
||||
|
||||
test("parses a minimal version 5 manifest", () => {
|
||||
const name = Buffer.from("VanillaTank\0", "ascii");
|
||||
const src = Buffer.from("DATA:globaldata/armor.xml\0", "ascii");
|
||||
const parts = [
|
||||
Buffer.from([0, 1]), // isBigEndian=false, isLinked=true (one byte each)
|
||||
u16(5), // version
|
||||
u32(0x1234), // streamChecksum
|
||||
u32(0), // allTypesHash
|
||||
u32(1), // assetCount
|
||||
u32(0), // totalInstanceDataSize
|
||||
u32(0), // maxInstanceChunkSize
|
||||
u32(0), // maxRelocationChunkSize
|
||||
u32(0), // maxImportsChunkSize
|
||||
u32(0), // assetReferenceBufferSize
|
||||
u32(0), // referencedManifestNameBufferSize
|
||||
u32(name.length), // assetNameBufferSize
|
||||
u32(src.length), // sourceFileNameBufferSize
|
||||
// one asset entry (version 5 = 44 bytes, no tokenized flag)
|
||||
u32(0x942fff2d), // typeId (GameObject)
|
||||
u32(0), // instanceId
|
||||
u32(0), // typeHash
|
||||
u32(0), // instanceHash
|
||||
u32(0), // assetReferenceOffset
|
||||
u32(0), // assetReferenceCount
|
||||
u32(0), // nameOffset
|
||||
u32(0), // sourceFileNameOffset
|
||||
u32(0), // instanceDataSize
|
||||
u32(0), // relocationDataSize
|
||||
u32(0), // importsDataSize
|
||||
name,
|
||||
src,
|
||||
];
|
||||
const info = parseManifest(new Uint8Array(Buffer.concat(parts)));
|
||||
assert.equal(info.version, 5);
|
||||
assert.equal(info.assetCount, 1);
|
||||
assert.equal(info.assets.length, 1);
|
||||
assert.equal(info.assets[0].name, "VanillaTank");
|
||||
assert.equal(info.assets[0].typeName, "GameObject");
|
||||
assert.equal(info.assets[0].sourceFileName, "DATA:globaldata/armor.xml");
|
||||
});
|
||||
|
||||
test("reports an error for garbage input", () => {
|
||||
const info = parseManifest(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
|
||||
assert.ok(info.error);
|
||||
assert.equal(info.assets.length, 0);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { deriveAssetId, deriveAssetType } from "../out/indexer/manifestParser.js";
|
||||
import { isAssignableTo } from "../out/model/schemaModel.js";
|
||||
import {
|
||||
isReferenceAttributeOfType,
|
||||
resolveReferenceTargetsForType,
|
||||
} from "../out/indexer/refs.js";
|
||||
|
||||
test("deriveAssetType falls back to the TypeName:Id prefix", () => {
|
||||
assert.equal(deriveAssetType(null, "PlayerTemplate:Allies"), "PlayerTemplate");
|
||||
assert.equal(deriveAssetType(null, "AudioFile:A06_Loop"), "AudioFile");
|
||||
assert.equal(deriveAssetType("GameObject", "GameObject:X"), "GameObject");
|
||||
assert.equal(deriveAssetType(null, "NoColonHere"), null);
|
||||
assert.equal(deriveAssetType(null, ":LeadingColon"), null);
|
||||
});
|
||||
|
||||
test("deriveAssetId uses the last colon segment", () => {
|
||||
// Art assets carry an extra subtype segment: Type:SubType:FileName.
|
||||
assert.equal(
|
||||
deriveAssetId("W3dContainer:W3DContainer:AUANTIVEHICLEVEHICLETECH1_SKN"),
|
||||
"AUANTIVEHICLEVEHICLETECH1_SKN",
|
||||
);
|
||||
assert.equal(deriveAssetId("PlayerTemplate:Allies"), "Allies");
|
||||
assert.equal(deriveAssetId("AudioFile:A06_Loop"), "A06_Loop");
|
||||
assert.equal(deriveAssetId("Texture:Texture:AUAntiVehicleVehicleTech1"), "AUAntiVehicleVehicleTech1");
|
||||
assert.equal(deriveAssetId("NoColon"), "NoColon");
|
||||
});
|
||||
|
||||
test("case-normalized type matching follows the XSD inheritance chain", () => {
|
||||
// Manifest hashes use "W3d*" casing while the XSD types are "W3D*"; the
|
||||
// canonical lookup must bridge the two and respect the real chain.
|
||||
assert.ok(
|
||||
isAssignableTo("W3dContainer", "BaseRenderAssetType"),
|
||||
"W3dContainer is a render asset",
|
||||
);
|
||||
assert.ok(
|
||||
isAssignableTo("W3DMesh", "BaseRenderAssetType"),
|
||||
"W3DMesh is a render asset",
|
||||
);
|
||||
assert.ok(
|
||||
!isAssignableTo("W3dHierarchy", "BaseRenderAssetType"),
|
||||
"W3dHierarchy is NOT a render asset (per XSD)",
|
||||
);
|
||||
});
|
||||
|
||||
test("manifest-derived PlayerTemplate ids satisfy Side references", () => {
|
||||
// Simulate a manifest asset whose TypeId hash is unknown but whose name
|
||||
// carries the type prefix (exactly what global.manifest does for
|
||||
// PlayerTemplate:Allies).
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
[
|
||||
"allies",
|
||||
[
|
||||
{
|
||||
type: "PlayerTemplate",
|
||||
id: "Allies",
|
||||
file: "global.manifest",
|
||||
line: 0,
|
||||
origin: "manifest",
|
||||
},
|
||||
],
|
||||
],
|
||||
]),
|
||||
assets: new Map([["PlayerTemplate", new Map([["allies", []]])]]),
|
||||
projectDir: ".",
|
||||
sdkDir: ".",
|
||||
defines: new Map(),
|
||||
files: new Map(),
|
||||
streams: [],
|
||||
manifests: new Map(),
|
||||
sourceCandidates: [],
|
||||
diagnostics: [],
|
||||
stats: {},
|
||||
};
|
||||
|
||||
assert.ok(isAssignableTo("PlayerTemplate", "PlayerTemplate"));
|
||||
assert.equal(isReferenceAttributeOfType("GameObject", "Side"), true);
|
||||
const targets = resolveReferenceTargetsForType(idx, "GameObject", "Side", "Allies");
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.type, "PlayerTemplate");
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { ModIndexer } from "../out/indexer/indexer.js";
|
||||
import { CachedDirectoryWalker } from "../out/indexer/fileScanner.js";
|
||||
import { isReferenceAttribute, resolveReferenceTargets } from "../out/indexer/refs.js";
|
||||
import * as model from "../out/model/schemaModel.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const project = join(root, "test", "fixtures", "minimod");
|
||||
const sdk = join(root, "test", "fixtures", "fakesdk");
|
||||
|
||||
async function buildIndex() {
|
||||
const indexer = new ModIndexer({
|
||||
projectDir: project,
|
||||
sdkDir: sdk,
|
||||
builtmodsDirs: [join(sdk, "builtmods")],
|
||||
indexSageXml: true,
|
||||
additionalDataSearchPaths: [],
|
||||
walker: new CachedDirectoryWalker(),
|
||||
});
|
||||
return indexer.build();
|
||||
}
|
||||
|
||||
test("definition resolution filters by the attribute's ref type", async () => {
|
||||
const idx = await buildIndex();
|
||||
const defs = idx.assetsById.get("sharedthing");
|
||||
assert.equal(defs.length, 2);
|
||||
const types = defs.map((d) => d.type).sort();
|
||||
assert.deepEqual(types, ["GameObject", "WeaponTemplate"]);
|
||||
|
||||
// WeaponName="SharedThing" on a WeaponTemplateRef attribute must only resolve
|
||||
// to the WeaponTemplate definition.
|
||||
const targets = resolveReferenceTargets(idx, "FireWeaponNugget", "WeaponName", "SharedThing");
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.type, "WeaponTemplate");
|
||||
assert.match(targets[0].def.file, /Refs\.xml/);
|
||||
|
||||
// A non-reference attribute must never resolve anything.
|
||||
const none = resolveReferenceTargets(idx, "FireWeaponNugget", "EditorName", "SharedThing");
|
||||
assert.equal(none.length, 0);
|
||||
|
||||
// inheritFrom filters to types assignable to the element itself.
|
||||
const inherit = resolveReferenceTargets(idx, "GameObject", "inheritFrom", "SharedThing");
|
||||
assert.equal(inherit.length, 1);
|
||||
assert.equal(inherit[0].def.type, "GameObject");
|
||||
});
|
||||
|
||||
test("the model knows a real WeaponTemplateRef attribute pair", () => {
|
||||
const attrs = model.attributesOfElement("FireWeaponNugget");
|
||||
const weapon = attrs.find((a) => a.name === "WeaponName");
|
||||
assert.equal(weapon?.refType, "WeaponTemplate");
|
||||
});
|
||||
|
||||
test("isReferenceAttribute distinguishes references from enums/paths", () => {
|
||||
// Include/@type and Include/@source must never be treated as references.
|
||||
assert.equal(isReferenceAttribute("Include", "type"), false);
|
||||
assert.equal(isReferenceAttribute("Include", "source"), false);
|
||||
// Typed references and inheritFrom are references.
|
||||
assert.equal(isReferenceAttribute("GameObject", "CommandSet"), true);
|
||||
assert.equal(isReferenceAttribute("GameObject", "inheritFrom"), true);
|
||||
assert.equal(isReferenceAttribute("FireWeaponNugget", "WeaponName"), true);
|
||||
});
|
||||
|
||||
test("untyped references (isRef without refType) resolve to any declared id", () => {
|
||||
// LocomotorSet/@Locomotor has type AssetReference: xas:isRef="true" with
|
||||
// no refType. It must be treated as a reference and match the id regardless
|
||||
// of the target asset type.
|
||||
const idx = {
|
||||
assetsById: new Map([
|
||||
[
|
||||
"alliedantivehiclevehicletech1locomotor",
|
||||
[
|
||||
{
|
||||
type: "LocomotorTemplate",
|
||||
id: "AlliedAntiVehicleVehicleTech1Locomotor",
|
||||
file: "Locomotor.xml",
|
||||
line: 5,
|
||||
origin: "project",
|
||||
},
|
||||
],
|
||||
],
|
||||
]),
|
||||
assets: new Map(),
|
||||
projectDir: ".",
|
||||
sdkDir: ".",
|
||||
defines: new Map(),
|
||||
files: new Map(),
|
||||
streams: [],
|
||||
manifests: new Map(),
|
||||
sourceCandidates: [],
|
||||
diagnostics: [],
|
||||
stats: {},
|
||||
};
|
||||
assert.equal(isReferenceAttribute("LocomotorSet", "Locomotor"), true);
|
||||
const targets = resolveReferenceTargets(idx, "LocomotorSet", "Locomotor", "AlliedAntiVehicleVehicleTech1Locomotor");
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.type, "LocomotorTemplate");
|
||||
// A non-reference attribute still returns nothing.
|
||||
assert.equal(resolveReferenceTargets(idx, "LocomotorSet", "EditorName", "X").length, 0);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as model from "../out/model/schemaModel.js";
|
||||
|
||||
test("top-level elements include core asset types", () => {
|
||||
for (const name of ["GameObject", "WeaponTemplate", "Texture", "LogicCommandSet", "SpecialPowerTemplate"]) {
|
||||
assert.ok(model.isTopLevelElement(name), name);
|
||||
}
|
||||
});
|
||||
|
||||
test("GameObject has expected attributes", () => {
|
||||
const attrs = model.attributesOfElement("GameObject");
|
||||
const id = attrs.find((a) => a.name === "id");
|
||||
assert.ok(id?.required);
|
||||
const cmd = attrs.find((a) => a.name === "CommandSet");
|
||||
assert.equal(cmd?.refType, "LogicCommandSet");
|
||||
assert.ok(attrs.some((a) => a.name === "inheritFrom"));
|
||||
});
|
||||
|
||||
test("Include has reference/instance/all enum", () => {
|
||||
const attrs = model.attributesOfElement("Include");
|
||||
const type = attrs.find((a) => a.name === "type");
|
||||
assert.deepEqual(type?.enumValues, ["reference", "instance", "all"]);
|
||||
});
|
||||
|
||||
test("type assignability follows inheritance", () => {
|
||||
assert.ok(model.isAssignableTo("GameObject", "BaseInheritableAsset"));
|
||||
assert.ok(model.isAssignableTo("GameObject", "GameObject"));
|
||||
assert.ok(!model.isAssignableTo("WeaponTemplate", "GameObject"));
|
||||
});
|
||||
|
||||
test("asset type hashes resolve", () => {
|
||||
assert.equal(model.assetTypeNameFromHash(0x942fff2d), "GameObject");
|
||||
assert.equal(model.assetTypeNameFromHash(0x166b084d), "AudioFile");
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseXml } from "../out/language/xmlParser.js";
|
||||
import { resolveElementType } from "../out/language/typeContext.js";
|
||||
import * as model from "../out/model/schemaModel.js";
|
||||
|
||||
test("context-aware type resolution follows the parent chain", () => {
|
||||
const text = `<?xml version="1.0"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<GameObject id="Tank">
|
||||
<BehaviorModules>
|
||||
<WeaponSetUpdate>
|
||||
<WeaponSlotTurret ID="1">
|
||||
<Weapon Ordering="PRIMARY_WEAPON" />
|
||||
</WeaponSlotTurret>
|
||||
</WeaponSetUpdate>
|
||||
</BehaviorModules>
|
||||
</GameObject>
|
||||
</AssetDeclaration>`;
|
||||
const doc = parseXml(text);
|
||||
const weapon = doc.elements.find((e) => e.name === "Weapon");
|
||||
assert.ok(weapon, "Weapon element parsed");
|
||||
|
||||
const weaponType = resolveElementType(weapon);
|
||||
assert.equal(weaponType, "WeaponSlot_WeaponData");
|
||||
|
||||
const attrs = model.attributesOfType(weaponType);
|
||||
assert.ok(attrs.some((a) => a.name === "Ordering"), "Ordering attribute present");
|
||||
|
||||
// The same element name in a non-slot context resolves differently
|
||||
// (e.g. a plain WeaponRef) and must not leak the slot attributes.
|
||||
const slot = doc.elements.find((e) => e.name === "WeaponSlotTurret");
|
||||
assert.equal(resolveElementType(slot), "WeaponSlot_Turret");
|
||||
});
|
||||
|
||||
test("model childTypeOf primitives", () => {
|
||||
assert.equal(model.childTypeOf("WeaponSetUpdateModuleData", "WeaponSlotTurret"), "WeaponSlot_Turret");
|
||||
assert.equal(model.childTypeOf("WeaponSlot_Turret", "Weapon"), "WeaponSlot_WeaponData");
|
||||
assert.equal(model.childTypeOf("WeaponSlot_WeaponData", "Weapon"), null);
|
||||
assert.equal(model.childTypeOf(null, "Weapon"), null);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseXml, findElementAt } from "../out/language/xmlParser.js";
|
||||
|
||||
test("parses elements, attributes and positions", () => {
|
||||
const text = `<AssetDeclaration xmlns="uri:ea.com:eala:asset">\n\t<GameObject id="X" KindOf="A B"/>\n</AssetDeclaration>`;
|
||||
const doc = parseXml(text);
|
||||
assert.equal(doc.errors.length, 0);
|
||||
assert.equal(doc.root.name, "AssetDeclaration");
|
||||
const go = doc.root.children[0];
|
||||
assert.equal(go.name, "GameObject");
|
||||
assert.equal(go.selfClosing, true);
|
||||
assert.equal(go.attrs.length, 2);
|
||||
assert.equal(go.attrs[0].name, "id");
|
||||
assert.equal(go.attrs[0].value, "X");
|
||||
assert.equal(go.attrs[1].value, "A B");
|
||||
const idAttr = go.attrs[0];
|
||||
assert.equal(text.slice(idAttr.valueStart, idAttr.valueEnd), "X");
|
||||
});
|
||||
|
||||
test("detects mismatched and unclosed tags", () => {
|
||||
const doc = parseXml("<A><B></A>");
|
||||
assert.ok(doc.errors.length >= 1);
|
||||
assert.match(doc.errors[0].message, /Mismatched|never closed/);
|
||||
});
|
||||
|
||||
test("handles CRLF and comments", () => {
|
||||
const text = "<?xml version=\"1.0\"?>\r\n<!-- hello -->\r\n<AssetDeclaration>\r\n</AssetDeclaration>\r\n";
|
||||
const doc = parseXml(text);
|
||||
assert.equal(doc.errors.length, 0);
|
||||
assert.equal(doc.root.name, "AssetDeclaration");
|
||||
});
|
||||
|
||||
test("findElementAt returns innermost element", () => {
|
||||
const text = `<A><B id="1"><C/></B></A>`;
|
||||
const doc = parseXml(text);
|
||||
const c = doc.elements.find((e) => e.name === "C");
|
||||
const at = findElementAt(doc, c.start + 1);
|
||||
assert.equal(at.name, "C");
|
||||
});
|
||||
|
||||
test("tolerates partial input while typing", () => {
|
||||
const text = `<AssetDeclaration>\n\t<GameObject id="TestTank" Com`;
|
||||
const doc = parseXml(text);
|
||||
assert.ok(doc.errors.length >= 1); // unclosed
|
||||
assert.equal(doc.elements.length, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user