Release Healer Man 0.1.12

This commit is contained in:
phenom committed 2026-09-01 14:35:56 -04:00
1 parent 13970d03c8
commit 65e24f644e
201 files changed
+5061 -123

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
export interface OriginalCombatMeshTexture {
readonly type: number;
readonly flags: number;
readonly filename: string;
readonly sourcePath: string;
readonly sourceSha256: string;
readonly url: string;
}
export interface OriginalCombatMeshLayer {
readonly submeshIndex: number;
readonly indexStart: number;
readonly indexCount: number;
readonly priorityPlane: number;
readonly shaderId: number;
readonly colorIndex: number;
readonly materialLayer: number;
readonly materialFlags: number;
readonly blendMode: number;
readonly textureIndex: number;
readonly textureCoordinateIndex: number;
readonly textureWeightIndex: number;
readonly textureTransformIndex: number;
}
export interface OriginalCombatMeshGraph {
readonly schemaVersion: 1;
readonly kind: "wow-m2-static-mesh";
readonly source: "wow335";
readonly sourceModel: string;
readonly normalizedSourceModel: string;
readonly sourceSha256: string;
readonly sourceSkin: string;
readonly sourceSkinSha256: string;
readonly bounds: {
readonly minimum: readonly [number, number, number];
readonly maximum: readonly [number, number, number];
readonly radius: number;
};
readonly geometry: {
readonly positions: readonly number[];
readonly normals: readonly number[];
readonly uvs: readonly number[];
readonly indices: readonly number[];
};
readonly textures: readonly OriginalCombatMeshTexture[];
readonly layers: readonly OriginalCombatMeshLayer[];
readonly runtime: {
readonly renderer: "wow-m2-static-mesh-v1";
readonly coordinateSpace: "wow-m2-source";
readonly staticFrame: "source-bind-pose";
};
}
type JsonRecord = Record<string, unknown>;
function exactObject(value: unknown, label: string, keys: readonly string[]): JsonRecord {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${label} must be an object.`);
}
const record = value as JsonRecord;
const expected = new Set(keys);
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(record, key)) throw new TypeError(`${label}.${key} is required.`);
}
for (const key of Object.keys(record)) {
if (!expected.has(key)) throw new TypeError(`${label}.${key} is not part of the static-mesh schema.`);
}
return record;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${label} must be finite.`);
return value;
}
function integerValue(value: unknown, label: string, minimum = 0): number {
const resolved = numberValue(value, label);
if (!Number.isSafeInteger(resolved) || resolved < minimum) throw new TypeError(`${label} must be an integer >= ${minimum}.`);
return resolved;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${label} must be a non-empty string.`);
return value;
}
function digest(value: unknown, label: string): string {
const resolved = stringValue(value, label);
if (!/^[0-9a-f]{64}$/.test(resolved)) throw new TypeError(`${label} must be a lowercase SHA-256 digest.`);
return resolved;
}
function literal<T extends string | number>(value: unknown, expected: T, label: string): T {
if (value !== expected) throw new TypeError(`${label} must be ${JSON.stringify(expected)}.`);
return expected;
}
function numberArray(value: unknown, label: string): readonly number[] {
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
return value.map((entry, index) => numberValue(entry, `${label}[${index}]`));
}
function vector3(value: unknown, label: string): readonly [number, number, number] {
const resolved = numberArray(value, label);
if (resolved.length !== 3) throw new TypeError(`${label} must contain three numbers.`);
return [resolved[0], resolved[1], resolved[2]];
}
function parseTexture(value: unknown, index: number): OriginalCombatMeshTexture {
const label = `static mesh.textures[${index}]`;
const record = exactObject(value, label, [
"type", "flags", "filename", "sourcePath", "sourceSha256", "url",
]);
return {
type: integerValue(record.type, `${label}.type`),
flags: integerValue(record.flags, `${label}.flags`),
filename: stringValue(record.filename, `${label}.filename`),
sourcePath: stringValue(record.sourcePath, `${label}.sourcePath`),
sourceSha256: digest(record.sourceSha256, `${label}.sourceSha256`),
url: stringValue(record.url, `${label}.url`),
};
}
function parseLayer(
value: unknown,
index: number,
indices: readonly number[],
textureCount: number,
): OriginalCombatMeshLayer {
const label = `static mesh.layers[${index}]`;
const record = exactObject(value, label, [
"submeshIndex", "indexStart", "indexCount", "priorityPlane", "shaderId", "colorIndex",
"materialLayer", "materialFlags", "blendMode", "textureIndex", "textureCoordinateIndex",
"textureWeightIndex", "textureTransformIndex",
]);
const indexStart = integerValue(record.indexStart, `${label}.indexStart`);
const indexCount = integerValue(record.indexCount, `${label}.indexCount`);
if (indexStart + indexCount > indices.length) throw new TypeError(`${label} draw range exceeds geometry indices.`);
const textureIndex = integerValue(record.textureIndex, `${label}.textureIndex`);
if (textureIndex >= textureCount) throw new TypeError(`${label}.textureIndex has no matching texture.`);
return {
submeshIndex: integerValue(record.submeshIndex, `${label}.submeshIndex`),
indexStart,
indexCount,
priorityPlane: integerValue(record.priorityPlane, `${label}.priorityPlane`, -128),
shaderId: integerValue(record.shaderId, `${label}.shaderId`),
colorIndex: integerValue(record.colorIndex, `${label}.colorIndex`, -32_768),
materialLayer: integerValue(record.materialLayer, `${label}.materialLayer`),
materialFlags: integerValue(record.materialFlags, `${label}.materialFlags`),
blendMode: integerValue(record.blendMode, `${label}.blendMode`),
textureIndex,
textureCoordinateIndex: integerValue(record.textureCoordinateIndex, `${label}.textureCoordinateIndex`, -32_768),
textureWeightIndex: integerValue(record.textureWeightIndex, `${label}.textureWeightIndex`, -32_768),
textureTransformIndex: integerValue(record.textureTransformIndex, `${label}.textureTransformIndex`, -32_768),
};
}
export function parseOriginalCombatMeshGraph(value: unknown): OriginalCombatMeshGraph {
const graph = exactObject(value, "static mesh", [
"schemaVersion", "kind", "source", "sourceModel", "normalizedSourceModel", "sourceSha256",
"sourceSkin", "sourceSkinSha256", "bounds", "geometry", "textures", "layers", "runtime",
]);
literal(graph.schemaVersion, 1, "static mesh.schemaVersion");
literal(graph.kind, "wow-m2-static-mesh", "static mesh.kind");
literal(graph.source, "wow335", "static mesh.source");
const bounds = exactObject(graph.bounds, "static mesh.bounds", ["minimum", "maximum", "radius"]);
const geometry = exactObject(graph.geometry, "static mesh.geometry", ["positions", "normals", "uvs", "indices"]);
const positions = numberArray(geometry.positions, "static mesh.geometry.positions");
const normals = numberArray(geometry.normals, "static mesh.geometry.normals");
const uvs = numberArray(geometry.uvs, "static mesh.geometry.uvs");
if (!positions.length || positions.length % 3 !== 0) throw new TypeError("static mesh positions must contain vertices.");
if (normals.length !== positions.length) throw new TypeError("static mesh normals must match positions.");
if (uvs.length !== positions.length / 3 * 2) throw new TypeError("static mesh UVs must match positions.");
if (!Array.isArray(geometry.indices) || !geometry.indices.length) throw new TypeError("static mesh indices must be populated.");
const vertexCount = positions.length / 3;
const indices = geometry.indices.map((entry, index) => {
const resolved = integerValue(entry, `static mesh.geometry.indices[${index}]`);
if (resolved >= vertexCount) throw new TypeError(`static mesh.geometry.indices[${index}] exceeds its vertices.`);
return resolved;
});
if (!Array.isArray(graph.textures) || !graph.textures.length) throw new TypeError("static mesh textures must be populated.");
const textures = graph.textures.map(parseTexture);
if (!Array.isArray(graph.layers) || !graph.layers.length) throw new TypeError("static mesh layers must be populated.");
const layers = graph.layers.map((layer, index) => parseLayer(layer, index, indices, textures.length));
const runtime = exactObject(graph.runtime, "static mesh.runtime", [
"renderer", "coordinateSpace", "staticFrame",
]);
literal(runtime.renderer, "wow-m2-static-mesh-v1", "static mesh.runtime.renderer");
literal(runtime.coordinateSpace, "wow-m2-source", "static mesh.runtime.coordinateSpace");
literal(runtime.staticFrame, "source-bind-pose", "static mesh.runtime.staticFrame");
return {
schemaVersion: 1,
kind: "wow-m2-static-mesh",
source: "wow335",
sourceModel: stringValue(graph.sourceModel, "static mesh.sourceModel"),
normalizedSourceModel: stringValue(graph.normalizedSourceModel, "static mesh.normalizedSourceModel"),
sourceSha256: digest(graph.sourceSha256, "static mesh.sourceSha256"),
sourceSkin: stringValue(graph.sourceSkin, "static mesh.sourceSkin"),
sourceSkinSha256: digest(graph.sourceSkinSha256, "static mesh.sourceSkinSha256"),
bounds: {
minimum: vector3(bounds.minimum, "static mesh.bounds.minimum"),
maximum: vector3(bounds.maximum, "static mesh.bounds.maximum"),
radius: numberValue(bounds.radius, "static mesh.bounds.radius"),
},
geometry: { positions, normals, uvs, indices },
textures,
layers,
runtime: {
renderer: "wow-m2-static-mesh-v1",
coordinateSpace: "wow-m2-source",
staticFrame: "source-bind-pose",
},
};
}