支持 W3X
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
This is a fake DDS payload used to verify that binary assets included from
|
||||
an XML hub are registered as files but never parsed as XML.
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<W3DMesh id="Tank_FP" />
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<W3DContainer id="Tank_SKN" Hierarchy="Tank_SKL" />
|
||||
<W3DHierarchy id="Tank_SKL" />
|
||||
</AssetDeclaration>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Includes>
|
||||
<Include type="all" source="Models/Tank_SKN.w3x" />
|
||||
<Include type="all" source="Models/Tank_FP.w3d" />
|
||||
<Include type="all" source="Models/Tank_Damaged.dds" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
Vendored
+1
@@ -9,5 +9,6 @@
|
||||
<Include type="all" source="Includes/Weapons.xml" />
|
||||
<Include type="all" source="Includes/Shared.xml" />
|
||||
<Include type="all" source="Includes/Refs.xml" />
|
||||
<Include type="all" source="Includes/VehicleArt.xml" />
|
||||
</Includes>
|
||||
</AssetDeclaration>
|
||||
|
||||
@@ -2,8 +2,12 @@ import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import { ModIndexer } from "../out/indexer/indexer.js";
|
||||
import { CachedDirectoryWalker } from "../out/indexer/fileScanner.js";
|
||||
import { DocumentCache, ShallowScanCache } from "../out/indexer/caches.js";
|
||||
import { resolveReferenceTargetsForType } from "../out/indexer/refs.js";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const project = join(root, "test", "fixtures", "minimod");
|
||||
@@ -70,3 +74,120 @@ test("provides include source candidates", async () => {
|
||||
assert.ok(xml.some((c) => c.source === "Includes/Units.xml"));
|
||||
assert.ok(xml.some((c) => c.source === "DATA:static.xml"));
|
||||
});
|
||||
|
||||
test("indexes art-asset XML (.w3x / sniffed .w3d) via shallow scan and skips binary", async () => {
|
||||
const idx = await buildIndex();
|
||||
|
||||
// The .w3x hub chain: Mod.xml -> VehicleArt.xml -> Models/Tank_SKN.w3x.
|
||||
const skn = idx.assetsById.get("tank_skn");
|
||||
assert.ok(skn?.some((d) => d.type === "W3DContainer"), "W3DContainer from .w3x indexed");
|
||||
assert.ok(
|
||||
idx.assetsById.get("tank_skl")?.some((d) => d.type === "W3DHierarchy"),
|
||||
"W3DHierarchy from .w3x indexed",
|
||||
);
|
||||
const container = skn.find((d) => d.type === "W3DContainer");
|
||||
assert.match(container.file, /Tank_SKN\.w3x$/);
|
||||
assert.ok(container.line > 0, "definition line recorded from shallow scan");
|
||||
|
||||
// Unknown extension with XML content is sniffed and indexed.
|
||||
assert.ok(
|
||||
idx.assetsById.get("tank_fp")?.some((d) => d.type === "W3DMesh"),
|
||||
"unknown-extension XML (.w3d) sniffed and indexed",
|
||||
);
|
||||
|
||||
// Binary content is registered as a file but never parsed.
|
||||
assert.equal(idx.assetsById.get("tank_damaged"), undefined, "binary asset never indexed");
|
||||
assert.ok(
|
||||
idx.files.has(
|
||||
join(project, "Data", "Includes", "Models", "Tank_Damaged.dds").toLowerCase(),
|
||||
),
|
||||
"binary include target registered as a file",
|
||||
);
|
||||
|
||||
// The reported Harbinger scenario: <Model Name="Tank_SKN"/> (refType
|
||||
// BaseRenderAssetType) must resolve to the W3DContainer defined in the w3x.
|
||||
const targets = resolveReferenceTargetsForType(
|
||||
idx,
|
||||
"ScriptedModelDrawModel",
|
||||
"Name",
|
||||
"Tank_SKN",
|
||||
);
|
||||
assert.equal(targets.length, 1);
|
||||
assert.equal(targets[0].def.type, "W3DContainer");
|
||||
assert.match(targets[0].def.file, /Tank_SKN\.w3x$/);
|
||||
});
|
||||
|
||||
test("w3x files appear in Include source completion candidates", async () => {
|
||||
const idx = await buildIndex();
|
||||
assert.ok(
|
||||
idx.sourceCandidates.some((c) => c.source === "Includes/Models/Tank_SKN.w3x"),
|
||||
"project-relative w3x candidate",
|
||||
);
|
||||
assert.ok(
|
||||
idx.sourceCandidates.some((c) => c.source === "DATA:Includes/Models/Tank_SKN.w3x"),
|
||||
"DATA: w3x candidate",
|
||||
);
|
||||
});
|
||||
|
||||
test("shallow scans and full parses are cached across rebuilds", async () => {
|
||||
const documentCache = new DocumentCache();
|
||||
const shallowCache = new ShallowScanCache();
|
||||
const makeIndexer = () =>
|
||||
new ModIndexer({
|
||||
projectDir: project,
|
||||
sdkDir: sdk,
|
||||
builtmodsDirs: [join(sdk, "builtmods")],
|
||||
indexSageXml: true,
|
||||
additionalDataSearchPaths: [],
|
||||
walker: new CachedDirectoryWalker(),
|
||||
documentCache,
|
||||
shallowCache,
|
||||
});
|
||||
|
||||
const first = await makeIndexer().build();
|
||||
const second = await makeIndexer().build();
|
||||
|
||||
assert.equal(first.stats.shallowScannedFiles, 2, "w3x + sniffed w3d scanned on first build");
|
||||
assert.equal(second.stats.shallowScannedFiles, 0, "unchanged art assets are not re-scanned");
|
||||
assert.equal(second.stats.shallowCacheHits, 2);
|
||||
assert.equal(second.stats.assetCount, first.stats.assetCount);
|
||||
assert.equal(second.stats.indexedFiles, first.stats.indexedFiles);
|
||||
});
|
||||
|
||||
test("w3x with a UTF-8 BOM is indexed with correct offsets", async (t) => {
|
||||
const tmp = fs.mkdtempSync(join(os.tmpdir(), "ra3modxml-bom-"));
|
||||
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
const projectDir = join(tmp, "project");
|
||||
fs.mkdirSync(join(projectDir, "Data", "Models"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(projectDir, "Data", "Mod.xml"),
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<AssetDeclaration>
|
||||
<Includes>
|
||||
<Include type="all" source="Models/Tank_BOM.w3x"/>
|
||||
</Includes>
|
||||
</AssetDeclaration>`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(projectDir, "Data", "Models", "Tank_BOM.w3x"),
|
||||
"\uFEFF<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<AssetDeclaration>\n" +
|
||||
" <W3DContainer id=\"Tank_BOM\" />\n" +
|
||||
"</AssetDeclaration>\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const idx = await new ModIndexer({
|
||||
projectDir,
|
||||
sdkDir: sdk,
|
||||
builtmodsDirs: [join(sdk, "builtmods")],
|
||||
indexSageXml: false,
|
||||
additionalDataSearchPaths: [],
|
||||
walker: new CachedDirectoryWalker(),
|
||||
}).build();
|
||||
|
||||
const def = idx.assetsById.get("tank_bom")?.find((d) => d.type === "W3DContainer");
|
||||
assert.ok(def, "BOM-prefixed w3x asset indexed");
|
||||
assert.equal(def.line, 3, "id line is correct despite the BOM");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { scanXmlShallow } from "../out/indexer/shallowScan.js";
|
||||
|
||||
test("extracts top-level assets, includes, defines and xi:include", () => {
|
||||
const text = `<?xml version="1.0"?>
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<Defines>
|
||||
<Define name="MODEL_SCALE" value="1.0"/>
|
||||
</Defines>
|
||||
<Includes>
|
||||
<Include type="all" source="Art/Model_SKN.w3x"/>
|
||||
<Include type="reference" source="DATA:static.xml"/>
|
||||
</Includes>
|
||||
<W3DContainer id="Model_SKN" Hierarchy="Model_SKL">
|
||||
<SubObject SubObjectID="GUN">
|
||||
<RenderObject><Mesh>Model_SKN.GUN</Mesh></RenderObject>
|
||||
</SubObject>
|
||||
</W3DContainer>
|
||||
<W3DMesh id="Model_SKN.GUN"/>
|
||||
<xi:include href="DATA:Includes/Extra.xml" xpointer="xmlns(n=uri:ea.com:eala:asset) xpointer(/n:Extra/child::*)"/>
|
||||
</AssetDeclaration>`;
|
||||
const doc = scanXmlShallow(text);
|
||||
assert.equal(doc.errors.length, 0);
|
||||
assert.deepEqual(
|
||||
doc.assets.map((a) => `${a.name}:${a.id}`),
|
||||
["W3DContainer:Model_SKN", "W3DMesh:Model_SKN.GUN"],
|
||||
);
|
||||
assert.equal(doc.includes.length, 2);
|
||||
assert.equal(doc.includes[0].type, "all");
|
||||
assert.equal(doc.includes[0].source, "Art/Model_SKN.w3x");
|
||||
assert.equal(doc.includes[1].type, "reference");
|
||||
assert.equal(doc.defines.length, 1);
|
||||
assert.equal(doc.defines[0].name, "MODEL_SCALE");
|
||||
assert.equal(doc.defines[0].value, "1.0");
|
||||
assert.equal(doc.xiIncludes.length, 1);
|
||||
assert.equal(doc.xiIncludes[0].href, "DATA:Includes/Extra.xml");
|
||||
assert.match(doc.xiIncludes[0].xpointer, /xpointer\(/);
|
||||
|
||||
// Recorded offsets must slice the original text back out.
|
||||
const container = doc.assets[0];
|
||||
assert.equal(text.slice(container.idValueStart, container.idValueEnd), "Model_SKN");
|
||||
assert.equal(text.slice(container.start, container.startTagEnd).startsWith("<W3DContainer"), true);
|
||||
const mesh = doc.assets[1];
|
||||
assert.equal(text.slice(mesh.idValueStart, mesh.idValueEnd), "Model_SKN.GUN");
|
||||
});
|
||||
|
||||
test("handles > and / inside quoted values, self-closing tags and comments", () => {
|
||||
const text = `<?xml version="1.0"?>
|
||||
<!-- a > comment with < inside -->
|
||||
<AssetDeclaration xmlns="uri:ea.com:eala:asset">
|
||||
<W3DContainer id="Weird" Description="a > b < c" />
|
||||
<W3DMesh id="X" VertexData="a / b"/>
|
||||
</AssetDeclaration>`;
|
||||
const doc = scanXmlShallow(text);
|
||||
assert.equal(doc.errors.length, 0);
|
||||
assert.deepEqual(
|
||||
doc.assets.map((a) => a.id),
|
||||
["Weird", "X"],
|
||||
);
|
||||
});
|
||||
|
||||
test("skips CDATA and processing instructions", () => {
|
||||
const text = `<AssetDeclaration>
|
||||
<!-- <W3DMesh id="FAKE"/> -->
|
||||
<![CDATA[ <W3DMesh id="FAKE2"/> ]]>
|
||||
<?xml-stylesheet href="x"?>
|
||||
<W3DContainer id="REAL"/>
|
||||
</AssetDeclaration>`;
|
||||
const doc = scanXmlShallow(text);
|
||||
assert.equal(doc.errors.length, 0);
|
||||
assert.deepEqual(
|
||||
doc.assets.map((a) => a.id),
|
||||
["REAL"],
|
||||
);
|
||||
});
|
||||
|
||||
test("reports unterminated tags and keeps partial results", () => {
|
||||
const doc = scanXmlShallow('<AssetDeclaration><W3DMesh id="A"/><W3DMesh id="B"');
|
||||
assert.ok(doc.errors.length >= 1);
|
||||
assert.deepEqual(
|
||||
doc.assets.map((a) => a.id),
|
||||
["A"],
|
||||
);
|
||||
});
|
||||
|
||||
test("nested module payload does not create asset records", () => {
|
||||
// Only top-level elements with an id are assets; the hundreds of thousands
|
||||
// of numeric V/T elements inside a mesh are not.
|
||||
const doc = scanXmlShallow(`<AssetDeclaration>
|
||||
<W3DMesh id="MESH">
|
||||
<Vertices><V X="1" Y="2" Z="3"/><V X="4" Y="5" Z="6"/></Vertices>
|
||||
<Triangles><T A="0" B="1" C="2"/></Triangles>
|
||||
</W3DMesh>
|
||||
</AssetDeclaration>`);
|
||||
assert.deepEqual(
|
||||
doc.assets.map((a) => a.id),
|
||||
["MESH"],
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseXml, findElementAt } from "../out/language/xmlParser.js";
|
||||
import { parseXml, findElementAt, stripBom } from "../out/language/xmlParser.js";
|
||||
|
||||
test("stripBom removes a leading UTF-8 byte-order mark", () => {
|
||||
assert.equal(stripBom("\uFEFF<A/>"), "<A/>");
|
||||
assert.equal(stripBom("<A/>"), "<A/>");
|
||||
});
|
||||
|
||||
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>`;
|
||||
|
||||
Reference in New Issue
Block a user