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,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",
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user