forked from phenom/healer-man
Release Healer Man 0.1.12
This commit is contained in:
1 parent
13970d03c8
commit
65e24f644e
201 files changed
+5061
-123
No files matched your search
@@ -0,0 +1,206 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { decodeWotlkM2Mesh } from "./wotlk-m2-mesh.mjs";
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
function syntheticM2AndSkin() {
|
||||
const m2 = Buffer.alloc(640);
|
||||
const skin = Buffer.alloc(144);
|
||||
const vertexOffset = 304;
|
||||
const materialOffset = 448;
|
||||
const textureLookupOffset = 452;
|
||||
const textureCoordinateLookupOffset = 454;
|
||||
const textureWeightLookupOffset = 456;
|
||||
const textureTransformLookupOffset = 458;
|
||||
const modelNameOffset = 464;
|
||||
const textureOffset = 496;
|
||||
const textureFilenameOffset = 512;
|
||||
|
||||
m2.write("MD20", 0, "ascii");
|
||||
m2.writeUInt32LE(264, 4);
|
||||
const modelName = Buffer.from("SyntheticShield\0", "utf8");
|
||||
m2.writeUInt32LE(modelName.length, 8);
|
||||
m2.writeUInt32LE(modelNameOffset, 12);
|
||||
modelName.copy(m2, modelNameOffset);
|
||||
m2.writeUInt32LE(3, 60);
|
||||
m2.writeUInt32LE(vertexOffset, 64);
|
||||
m2.writeUInt32LE(1, 80);
|
||||
m2.writeUInt32LE(textureOffset, 84);
|
||||
m2.writeUInt32LE(1, 112);
|
||||
m2.writeUInt32LE(materialOffset, 116);
|
||||
m2.writeUInt32LE(1, 128);
|
||||
m2.writeUInt32LE(textureLookupOffset, 132);
|
||||
m2.writeUInt32LE(1, 136);
|
||||
m2.writeUInt32LE(textureCoordinateLookupOffset, 140);
|
||||
m2.writeUInt32LE(1, 144);
|
||||
m2.writeUInt32LE(textureWeightLookupOffset, 148);
|
||||
m2.writeUInt32LE(1, 152);
|
||||
m2.writeUInt32LE(textureTransformLookupOffset, 156);
|
||||
[-1, -2, -3].forEach((value, index) => m2.writeFloatLE(value, 160 + index * 4));
|
||||
[1, 2, 3].forEach((value, index) => m2.writeFloatLE(value, 172 + index * 4));
|
||||
m2.writeFloatLE(4, 184);
|
||||
|
||||
const vertices = [
|
||||
{ position: [0, 0, 0], normal: [0, 0, 1], uv: [0, 0] },
|
||||
{ position: [1, 0, 0], normal: [0, 0, 1], uv: [1, 0] },
|
||||
{ position: [0, 1, 0], normal: [0, 0, 1], uv: [0, 1] },
|
||||
];
|
||||
vertices.forEach((vertex, index) => {
|
||||
const offset = vertexOffset + index * 48;
|
||||
vertex.position.forEach((value, component) => m2.writeFloatLE(value, offset + component * 4));
|
||||
m2.writeUInt8(255, offset + 12);
|
||||
m2.writeUInt8(2, offset + 16);
|
||||
vertex.normal.forEach((value, component) => m2.writeFloatLE(value, offset + 20 + component * 4));
|
||||
vertex.uv.forEach((value, component) => m2.writeFloatLE(value, offset + 32 + component * 4));
|
||||
vertex.uv.forEach((value, component) => m2.writeFloatLE(value, offset + 40 + component * 4));
|
||||
});
|
||||
|
||||
m2.writeUInt16LE(19, materialOffset);
|
||||
m2.writeUInt16LE(4, materialOffset + 2);
|
||||
m2.writeUInt16LE(0, textureLookupOffset);
|
||||
m2.writeInt16LE(0, textureCoordinateLookupOffset);
|
||||
m2.writeUInt16LE(0, textureWeightLookupOffset);
|
||||
m2.writeInt16LE(-1, textureTransformLookupOffset);
|
||||
const textureFilename = Buffer.from("SPELLS\\SYNTHETIC.BLP\0", "utf8");
|
||||
m2.writeUInt32LE(0, textureOffset);
|
||||
m2.writeUInt32LE(0, textureOffset + 4);
|
||||
m2.writeUInt32LE(textureFilename.length, textureOffset + 8);
|
||||
m2.writeUInt32LE(textureFilenameOffset, textureOffset + 12);
|
||||
textureFilename.copy(m2, textureFilenameOffset);
|
||||
|
||||
skin.write("SKIN", 0, "ascii");
|
||||
skin.writeUInt32LE(3, 4);
|
||||
skin.writeUInt32LE(48, 8);
|
||||
skin.writeUInt32LE(3, 12);
|
||||
skin.writeUInt32LE(54, 16);
|
||||
skin.writeUInt32LE(3, 20);
|
||||
skin.writeUInt32LE(60, 24);
|
||||
skin.writeUInt32LE(1, 28);
|
||||
skin.writeUInt32LE(72, 32);
|
||||
skin.writeUInt32LE(1, 36);
|
||||
skin.writeUInt32LE(120, 40);
|
||||
skin.writeUInt32LE(1, 44);
|
||||
[0, 1, 2].forEach((value, index) => skin.writeUInt16LE(value, 48 + index * 2));
|
||||
[0, 1, 2].forEach((value, index) => skin.writeUInt16LE(value, 54 + index * 2));
|
||||
for (let index = 0; index < 3; index += 1) skin.writeUInt8(2, 60 + index * 4);
|
||||
|
||||
const submeshOffset = 72;
|
||||
skin.writeUInt16LE(0, submeshOffset);
|
||||
skin.writeUInt16LE(0, submeshOffset + 2);
|
||||
skin.writeUInt16LE(0, submeshOffset + 4);
|
||||
skin.writeUInt16LE(3, submeshOffset + 6);
|
||||
skin.writeUInt16LE(0, submeshOffset + 8);
|
||||
skin.writeUInt16LE(3, submeshOffset + 10);
|
||||
skin.writeUInt16LE(1, submeshOffset + 12);
|
||||
skin.writeUInt16LE(0, submeshOffset + 14);
|
||||
skin.writeUInt16LE(1, submeshOffset + 16);
|
||||
skin.writeUInt16LE(2, submeshOffset + 18);
|
||||
[0.5, 0.5, 0].forEach((value, index) => skin.writeFloatLE(value, submeshOffset + 20 + index * 4));
|
||||
[0.5, 0.5, 0].forEach((value, index) => skin.writeFloatLE(value, submeshOffset + 32 + index * 4));
|
||||
skin.writeFloatLE(1, submeshOffset + 44);
|
||||
|
||||
const unitOffset = 120;
|
||||
skin.writeUInt8(8, unitOffset);
|
||||
skin.writeInt8(0, unitOffset + 1);
|
||||
skin.writeUInt16LE(0, unitOffset + 2);
|
||||
skin.writeUInt16LE(0, unitOffset + 4);
|
||||
skin.writeUInt16LE(0, unitOffset + 6);
|
||||
skin.writeInt16LE(-1, unitOffset + 8);
|
||||
skin.writeUInt16LE(0, unitOffset + 10);
|
||||
skin.writeUInt16LE(0, unitOffset + 12);
|
||||
skin.writeUInt16LE(1, unitOffset + 14);
|
||||
skin.writeUInt16LE(0, unitOffset + 16);
|
||||
skin.writeUInt16LE(0, unitOffset + 18);
|
||||
skin.writeUInt16LE(0, unitOffset + 20);
|
||||
skin.writeUInt16LE(0, unitOffset + 22);
|
||||
return { m2, skin };
|
||||
}
|
||||
|
||||
test("decodes browser-neutral M2 vertices and resolved SKIN draw data", () => {
|
||||
const { m2, skin } = syntheticM2AndSkin();
|
||||
const decoded = decodeWotlkM2Mesh(m2, skin, "synthetic shield");
|
||||
assert.equal(decoded.modelName, "SyntheticShield");
|
||||
assert.deepEqual(decoded.bounds, { minimum: [-1, -2, -3], maximum: [1, 2, 3], radius: 4 });
|
||||
assert.deepEqual(decoded.vertices[1], {
|
||||
position: [1, 0, 0],
|
||||
boneWeights: [255, 0, 0, 0],
|
||||
normalizedBoneWeights: [1, 0, 0, 0],
|
||||
boneIndices: [2, 0, 0, 0],
|
||||
normal: [0, 0, 1],
|
||||
uv: [1, 0],
|
||||
uv2: [1, 0],
|
||||
});
|
||||
assert.deepEqual(decoded.indices, [0, 1, 2]);
|
||||
assert.deepEqual(decoded.textures, [{ type: 0, flags: 0, filename: "SPELLS\\SYNTHETIC.BLP" }]);
|
||||
assert.deepEqual(decoded.materials, [{ flags: 19, blendMode: 4 }]);
|
||||
assert.deepEqual(decoded.skin.submeshes[0].indices, [0, 1, 2]);
|
||||
assert.deepEqual(decoded.skin.textureUnits[0], {
|
||||
flags: 8,
|
||||
priorityPlane: 0,
|
||||
shaderId: 0,
|
||||
submeshIndex: 0,
|
||||
geosetIndex: 0,
|
||||
colorIndex: -1,
|
||||
materialIndex: 0,
|
||||
materialLayer: 0,
|
||||
textureCount: 1,
|
||||
textureComboIndex: 0,
|
||||
textureCoordinateComboIndex: 0,
|
||||
textureWeightComboIndex: 0,
|
||||
textureTransformComboIndex: 0,
|
||||
textureIndices: [0],
|
||||
textureFilenames: ["SPELLS\\SYNTHETIC.BLP"],
|
||||
textureCoordinateIndices: [0],
|
||||
textureWeightIndices: [0],
|
||||
textureTransformIndices: [-1],
|
||||
drawRange: { indexStart: 0, indexCount: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects SKIN triangle references outside the vertex lookup", () => {
|
||||
const { m2, skin } = syntheticM2AndSkin();
|
||||
skin.writeUInt16LE(3, 54 + 4);
|
||||
assert.throws(
|
||||
() => decodeWotlkM2Mesh(m2, skin, "corrupt shield"),
|
||||
/SKIN triangle index 2 exceeds its vertex lookup/,
|
||||
);
|
||||
});
|
||||
|
||||
const localM2 = path.resolve(
|
||||
projectRoot, "..", "LadiksMPQEditor", "common-2.mpq", "SPELLS", "HolyDivineShield_State_Base.m2",
|
||||
);
|
||||
const localSkin = path.resolve(
|
||||
projectRoot, "..", "LadiksMPQEditor", "common-2.mpq", "SPELLS", "HolyDivineShield_State_Base00.skin",
|
||||
);
|
||||
const hasLocalShield = fs.existsSync(localM2) && fs.existsSync(localSkin);
|
||||
test("decodes the real local Power Word: Shield static mesh", {
|
||||
skip: hasLocalShield ? false : `local shield files absent: ${localM2}`,
|
||||
}, () => {
|
||||
const decoded = decodeWotlkM2Mesh(
|
||||
fs.readFileSync(localM2), fs.readFileSync(localSkin), "HolyDivineShield_State_Base",
|
||||
);
|
||||
assert.equal(decoded.modelName, "HolyDivineShield_State_Base");
|
||||
assert.equal(decoded.vertices.length, 12);
|
||||
assert.equal(decoded.indices.length, 18);
|
||||
assert.equal(decoded.skin.submeshes.length, 3);
|
||||
assert.equal(decoded.skin.textureUnits.length, 3);
|
||||
assert.deepEqual(decoded.textures.map((texture) => texture.filename), [
|
||||
"SPELLS\\SHOCKWAVE10D.BLP",
|
||||
"SPELLS\\GENERICGLOW2B1.BLP",
|
||||
"SPELLS\\SHOCKWAVE10B.BLP",
|
||||
]);
|
||||
assert.ok(decoded.materials.every((material) => material.flags === 19 && material.blendMode === 4));
|
||||
assert.ok(decoded.skin.submeshes.every((submesh) => submesh.indexCount === 6));
|
||||
assert.deepEqual(decoded.skin.textureUnits.map((unit) => unit.textureIndices), [[0], [1], [2]]);
|
||||
assert.deepEqual(decoded.skin.textureUnits.map((unit) => unit.drawRange), [
|
||||
{ indexStart: 0, indexCount: 6 },
|
||||
{ indexStart: 6, indexCount: 6 },
|
||||
{ indexStart: 12, indexCount: 6 },
|
||||
]);
|
||||
assert.deepEqual([...new Set(decoded.vertices.map((vertex) => vertex.boneIndices[0]))], [1, 2, 3]);
|
||||
assert.ok(decoded.vertices.every((vertex) => vertex.boneWeights[0] === 255));
|
||||
});
|
||||
Reference in new issue
Block a user