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

+209 -39
View File
@@ -42,68 +42,227 @@ export function parseDbc(buffer, name = "DBC") {
return { name, buffer, recordCount, fieldCount, recordSize, stringSize, row };
}
function decodeM2TrackHeader(buffer, offset) {
if (offset < 0 || offset + 20 > buffer.length) return null;
const M2_VALUE_TYPES = {
float32: { byteLength: 4, read: (buffer, offset) => buffer.readFloatLE(offset) },
uint32: { byteLength: 4, read: (buffer, offset) => buffer.readUInt32LE(offset) },
uint16: { byteLength: 2, read: (buffer, offset) => buffer.readUInt16LE(offset) },
uint8: { byteLength: 1, read: (buffer, offset) => buffer.readUInt8(offset) },
vector2: {
byteLength: 8,
read: (buffer, offset) => [buffer.readFloatLE(offset), buffer.readFloatLE(offset + 4)],
},
vector3: {
byteLength: 12,
read: (buffer, offset) => [
buffer.readFloatLE(offset),
buffer.readFloatLE(offset + 4),
buffer.readFloatLE(offset + 8),
],
},
};
function assertM2Range(buffer, offset, byteLength, label) {
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(byteLength)
|| offset < 0 || byteLength < 0 || offset > buffer.length - byteLength) {
throw new Error(`${label} points outside the M2 buffer.`);
}
}
function readM2ArrayHeader(buffer, offset, label) {
assertM2Range(buffer, offset, 8, `${label} descriptor`);
return { count: buffer.readUInt32LE(offset), offset: buffer.readUInt32LE(offset + 4) };
}
function decodeM2Array(buffer, offset, valueType, label) {
const descriptor = readM2ArrayHeader(buffer, offset, label);
const byteLength = descriptor.count * valueType.byteLength;
if (!Number.isSafeInteger(byteLength)) throw new Error(`${label} is too large to decode.`);
if (descriptor.count) assertM2Range(buffer, descriptor.offset, byteLength, label);
return {
interpolation: buffer.readUInt16LE(offset),
globalSequence: buffer.readInt16LE(offset + 2),
timeCount: buffer.readUInt32LE(offset + 4),
timeOffset: buffer.readUInt32LE(offset + 8),
keyCount: buffer.readUInt32LE(offset + 12),
keyOffset: buffer.readUInt32LE(offset + 16),
...descriptor,
values: Array.from({ length: descriptor.count }, (_, index) => (
valueType.read(buffer, descriptor.offset + index * valueType.byteLength)
)),
};
}
/** Decode the stable WotLK 3.x particle/ribbon fields used by the browser fallback converter. */
function decodeM2NestedArray(buffer, offset, valueType, label) {
const descriptor = readM2ArrayHeader(buffer, offset, label);
const byteLength = descriptor.count * 8;
if (!Number.isSafeInteger(byteLength)) throw new Error(`${label} is too large to decode.`);
if (descriptor.count) assertM2Range(buffer, descriptor.offset, byteLength, `${label} sequence descriptors`);
return {
...descriptor,
sequences: Array.from({ length: descriptor.count }, (_, index) => (
decodeM2Array(buffer, descriptor.offset + index * 8, valueType, `${label} sequence ${index}`)
)),
};
}
function decodeM2StringArray(buffer, offset, label) {
const descriptor = readM2ArrayHeader(buffer, offset, label);
if (!descriptor.count) return "";
assertM2Range(buffer, descriptor.offset, descriptor.count, label);
const bytes = buffer.subarray(descriptor.offset, descriptor.offset + descriptor.count);
const terminator = bytes.indexOf(0);
return bytes.toString("utf8", 0, terminator < 0 ? bytes.length : terminator);
}
function decodeM2Track(buffer, offset, valueType, label) {
assertM2Range(buffer, offset, 20, `${label} track`);
const interpolation = buffer.readUInt16LE(offset);
if (interpolation > 3) throw new Error(`${label} uses unsupported interpolation ${interpolation}.`);
const spline = interpolation === 2 || interpolation === 3;
const trackValueType = spline ? {
byteLength: valueType.byteLength * 3,
read: (source, valueOffset) => ({
value: valueType.read(source, valueOffset),
inTangent: valueType.read(source, valueOffset + valueType.byteLength),
outTangent: valueType.read(source, valueOffset + valueType.byteLength * 2),
}),
} : valueType;
const timestamps = decodeM2NestedArray(buffer, offset + 4, M2_VALUE_TYPES.uint32, `${label} timestamps`);
const values = decodeM2NestedArray(buffer, offset + 12, trackValueType, `${label} values`);
const firstTimestamps = timestamps.sequences[0]?.values ?? [];
const firstValues = values.sequences[0]?.values ?? [];
return {
kind: "animated",
interpolation,
globalSequence: buffer.readInt16LE(offset + 2),
timestamps,
values,
firstSequence: {
timestamps: firstTimestamps,
values: firstValues,
samples: Array.from({ length: Math.min(firstTimestamps.length, firstValues.length) }, (_, index) => ({
timeMs: firstTimestamps[index],
value: firstValues[index],
})),
},
};
}
function decodeM2ParticleLifetimeTrack(buffer, offset, valueType, label) {
const timestamps = decodeM2Array(buffer, offset, M2_VALUE_TYPES.uint16, `${label} timestamps`);
const values = decodeM2Array(buffer, offset + 8, valueType, `${label} values`);
return {
kind: "particle-lifetime",
timestamps,
values,
samples: Array.from({ length: Math.min(timestamps.values.length, values.values.length) }, (_, index) => ({
timestamp: timestamps.values[index],
normalizedTime: timestamps.values[index] / 32_767,
value: values.values[index],
})),
};
}
/** Decode render-neutral descriptors from the WotLK 3.3.5 (version 264) MD20 layout. */
export function decodeWotlkM2Effects(buffer, name = "M2") {
if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer);
const magic = buffer.toString("ascii", 0, 4);
if ((magic !== "MD20" && magic !== "MD21") || buffer.length < 304) {
throw new Error(`${name} is not a supported WotLK M2.`);
if (magic !== "MD20" || buffer.length < 304 || buffer.readUInt32LE(4) !== 264) {
throw new Error(`${name} is not a supported WotLK 3.3.5 MD20.`);
}
const version = buffer.readUInt32LE(4);
const animationCount = buffer.readUInt32LE(28);
const animationOffset = buffer.readUInt32LE(32);
const textureCount = buffer.readUInt32LE(80);
const textureOffset = buffer.readUInt32LE(84);
const ribbonCount = buffer.readUInt32LE(288);
const ribbonOffset = buffer.readUInt32LE(292);
const particleCount = buffer.readUInt32LE(296);
const particleOffset = buffer.readUInt32LE(300);
const particleRecordSize = 476;
const animationRecordSize = 64;
const textureRecordSize = 16;
const ribbonRecordSize = 176;
if (particleCount && particleOffset + particleCount * particleRecordSize > buffer.length) {
throw new Error(`${name} has truncated particle records.`);
}
if (ribbonCount && ribbonOffset + ribbonCount * ribbonRecordSize > buffer.length) {
throw new Error(`${name} has truncated ribbon records.`);
}
const trackOffsets = {
emissionSpeed: 56,
speedVariation: 76,
verticalRange: 96,
horizontalRange: 116,
gravity: 136,
lifespan: 156,
emissionRate: 180,
const particleRecordSize = 476;
if (animationCount) assertM2Range(buffer, animationOffset, animationCount * animationRecordSize, `${name} animation records`);
if (textureCount) assertM2Range(buffer, textureOffset, textureCount * textureRecordSize, `${name} texture records`);
if (ribbonCount) assertM2Range(buffer, ribbonOffset, ribbonCount * ribbonRecordSize, `${name} ribbon records`);
if (particleCount) assertM2Range(buffer, particleOffset, particleCount * particleRecordSize, `${name} particle records`);
const animations = Array.from({ length: animationCount }, (_, index) => {
const offset = animationOffset + index * animationRecordSize;
return {
index,
id: buffer.readUInt16LE(offset),
variationIndex: buffer.readUInt16LE(offset + 2),
durationMs: buffer.readUInt32LE(offset + 4),
moveSpeed: buffer.readFloatLE(offset + 8),
flags: buffer.readUInt32LE(offset + 12),
};
});
const durationMs = animations.reduce((maximum, animation) => Math.max(maximum, animation.durationMs), 0);
const textures = Array.from({ length: textureCount }, (_, index) => {
const offset = textureOffset + index * textureRecordSize;
return {
type: buffer.readUInt32LE(offset),
flags: buffer.readUInt32LE(offset + 4),
filename: decodeM2StringArray(buffer, offset + 8, `${name} texture ${index} filename`),
};
});
const animatedTrackOffsets = {
emissionSpeed: 52,
speedVariation: 72,
verticalRange: 92,
horizontalRange: 112,
gravity: 132,
lifespan: 152,
emissionRate: 176,
emissionAreaLength: 200,
emissionAreaWidth: 220,
zSource: 240,
color: 260,
alpha: 280,
scale: 300,
};
const particles = Array.from({ length: particleCount }, (_, index) => {
const offset = particleOffset + index * particleRecordSize;
const tracks = Object.fromEntries(Object.entries(animatedTrackOffsets).map(([track, relativeOffset]) => [
track,
decodeM2Track(buffer, offset + relativeOffset, M2_VALUE_TYPES.float32, `${name} particle ${index} ${track}`),
]));
tracks.color = decodeM2ParticleLifetimeTrack(
buffer, offset + 260, M2_VALUE_TYPES.vector3, `${name} particle ${index} color`,
);
tracks.alpha = decodeM2ParticleLifetimeTrack(
buffer, offset + 276, M2_VALUE_TYPES.uint16, `${name} particle ${index} alpha`,
);
tracks.scale = decodeM2ParticleLifetimeTrack(
buffer, offset + 292, M2_VALUE_TYPES.vector2, `${name} particle ${index} scale`,
);
tracks.headCell = decodeM2ParticleLifetimeTrack(
buffer, offset + 316, M2_VALUE_TYPES.uint16, `${name} particle ${index} head cell`,
);
tracks.tailCell = decodeM2ParticleLifetimeTrack(
buffer, offset + 332, M2_VALUE_TYPES.uint16, `${name} particle ${index} tail cell`,
);
tracks.enabled = decodeM2Track(
buffer, offset + 456, M2_VALUE_TYPES.uint8, `${name} particle ${index} enabled`,
);
return {
id: buffer.readUInt32LE(offset),
flags: buffer.readUInt32LE(offset + 4),
position: [buffer.readFloatLE(offset + 8), buffer.readFloatLE(offset + 12), buffer.readFloatLE(offset + 16)],
position: M2_VALUE_TYPES.vector3.read(buffer, offset + 8),
parentBone: buffer.readUInt16LE(offset + 20),
textureIndex: buffer.readUInt16LE(offset + 22),
geometryModel: decodeM2StringArray(buffer, offset + 24, `${name} particle ${index} geometry model`),
recursionModel: decodeM2StringArray(buffer, offset + 32, `${name} particle ${index} recursion model`),
blendMode: buffer.readUInt8(offset + 40),
emitterType: buffer.readUInt8(offset + 41),
particleColorIndex: buffer.readUInt16LE(offset + 42),
particleType: buffer.readUInt8(offset + 44),
headOrTail: buffer.readUInt8(offset + 45),
textureTileRotation: buffer.readUInt16LE(offset + 46),
textureRows: buffer.readUInt16LE(offset + 48),
textureColumns: buffer.readUInt16LE(offset + 50),
tracks: Object.fromEntries(Object.entries(trackOffsets)
.map(([track, relativeOffset]) => [track, decodeM2TrackHeader(buffer, offset + relativeOffset)])),
lifespanVariation: buffer.readFloatLE(offset + 172),
emissionRateVariation: buffer.readFloatLE(offset + 196),
scaleVariation: M2_VALUE_TYPES.vector2.read(buffer, offset + 308),
tailLength: buffer.readFloatLE(offset + 348),
tracks,
samples: Object.fromEntries([
...Object.keys(animatedTrackOffsets).map((track) => [track, tracks[track].firstSequence.samples]),
...["color", "alpha", "scale"].map((track) => [track, tracks[track].samples]),
]),
};
});
const ribbons = Array.from({ length: ribbonCount }, (_, index) => {
@@ -111,15 +270,26 @@ export function decodeWotlkM2Effects(buffer, name = "M2") {
return {
id: buffer.readUInt32LE(offset),
parentBone: buffer.readUInt32LE(offset + 4),
position: [buffer.readFloatLE(offset + 8), buffer.readFloatLE(offset + 12), buffer.readFloatLE(offset + 16)],
textureCount: buffer.readUInt32LE(offset + 20),
textureOffset: buffer.readUInt32LE(offset + 24),
materialCount: buffer.readUInt32LE(offset + 28),
materialOffset: buffer.readUInt32LE(offset + 32),
colorTrack: decodeM2TrackHeader(buffer, offset + 36),
position: M2_VALUE_TYPES.vector3.read(buffer, offset + 8),
textureIndices: decodeM2Array(buffer, offset + 20, M2_VALUE_TYPES.uint16, `${name} ribbon ${index} textures`),
materialIndices: decodeM2Array(buffer, offset + 28, M2_VALUE_TYPES.uint16, `${name} ribbon ${index} materials`),
edgesPerSecond: buffer.readFloatLE(offset + 116),
edgeLifetime: buffer.readFloatLE(offset + 120),
gravity: buffer.readFloatLE(offset + 124),
textureRows: buffer.readUInt16LE(offset + 128),
textureColumns: buffer.readUInt16LE(offset + 130),
priorityPlane: buffer.readInt16LE(offset + 172),
tracks: {
color: decodeM2Track(buffer, offset + 36, M2_VALUE_TYPES.vector3, `${name} ribbon ${index} color`),
alpha: decodeM2Track(buffer, offset + 56, M2_VALUE_TYPES.uint16, `${name} ribbon ${index} alpha`),
heightAbove: decodeM2Track(buffer, offset + 76, M2_VALUE_TYPES.float32, `${name} ribbon ${index} height above`),
heightBelow: decodeM2Track(buffer, offset + 96, M2_VALUE_TYPES.float32, `${name} ribbon ${index} height below`),
textureSlot: decodeM2Track(buffer, offset + 132, M2_VALUE_TYPES.uint16, `${name} ribbon ${index} texture slot`),
visibility: decodeM2Track(buffer, offset + 152, M2_VALUE_TYPES.uint8, `${name} ribbon ${index} visibility`),
},
};
});
return { magic, particles, ribbons };
return { magic, version, durationMs, animations, textures, particles, ribbons };
}
function tableById(table, decode) {