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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user