const WEIGHT_SIZE = 64 * 64; function assertKnownAlphaLayer(layer, chunk) { if (!layer.usesAlphaMap) return; if (!layer.alphaMapAvailable || layer.alphaMapDataOffset == null || !layer.alphaMapBytes) { throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} layer ${layer.index} has an unsupported or invalid MCAL span.`); } } function decodeRle8(data, label) { const output = new Uint8Array(WEIGHT_SIZE); let inputOffset = 0; let outputOffset = 0; while (outputOffset < output.length) { if (inputOffset >= data.length) throw new Error(`${label}: truncated MCAL RLE stream.`); const control = data[inputOffset++]; const count = control & 0x7f; if (!count) throw new Error(`${label}: zero-count MCAL RLE command cannot progress.`); if (outputOffset + count > output.length) throw new Error(`${label}: MCAL RLE expands beyond 4096 samples.`); if (control & 0x80) { if (inputOffset >= data.length) throw new Error(`${label}: truncated MCAL fill command.`); output.fill(data[inputOffset++], outputOffset, outputOffset + count); } else { if (inputOffset + count > data.length) throw new Error(`${label}: truncated MCAL copy command.`); output.set(data.subarray(inputOffset, inputOffset + count), outputOffset); inputOffset += count; } outputOffset += count; } if (inputOffset !== data.length) throw new Error(`${label}: ${data.length - inputOffset} trailing MCAL RLE bytes are unaccounted for.`); return output; } function decodePacked4(data, noEdgeRepair, label) { if (data.length !== 2048) throw new Error(`${label}: packed 4-bit MCAL must be exactly 2048 bytes.`); const output = new Uint8Array(WEIGHT_SIZE); let source = 0; for (let row = 0; row < 64; row += 1) { for (let column = 0; column < 64; column += 2) { const value = data[source++]; output[row * 64 + column] = (value & 0x0f) * 17; output[row * 64 + column + 1] = (value >>> 4) * 17; } } if (!noEdgeRepair) { for (let index = 0; index < 64; index += 1) { output[index * 64 + 63] = output[index * 64 + 62]; output[63 * 64 + index] = output[62 * 64 + index]; } output[63 * 64 + 63] = output[62 * 64 + 62]; } return output; } export function decodeWowAdtAlphaMap(adtBuffer, chunk, layer) { assertKnownAlphaLayer(layer, chunk); if (!layer.usesAlphaMap) return new Uint8Array(WEIGHT_SIZE); const payload = adtBuffer.subarray(layer.alphaMapDataOffset, layer.alphaMapDataOffset + layer.alphaMapBytes); const label = `MCNK ${chunk.indexX},${chunk.indexY} layer ${layer.index}`; switch (layer.alphaMapEncoding) { case "packed-4": return decodePacked4(payload, (chunk.flags & 0x8000) !== 0, label); case "uncompressed-8": if (payload.length !== WEIGHT_SIZE) throw new Error(`${label}: 8-bit MCAL must be exactly 4096 bytes.`); return new Uint8Array(payload); case "rle-8": return decodeRle8(payload, label); default: throw new Error(`${label}: unsupported alpha encoding ${layer.alphaMapEncoding ?? "missing"}.`); } } /** * Decode source overlays into the effective RGB weights expected by the * wow-overlay-residual shader. Classic 4-bit maps are source opacity maps; * their overlay order must be resolved before the shader consumes them. */ export function composeWowAdtTerrainWeights(adtBuffer, chunk) { const overlays = chunk.layers.slice(1); if (overlays.length > 3) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} has more than three overlay layers.`); const encodings = [...new Set(overlays.filter((layer) => layer.usesAlphaMap).map((layer) => layer.alphaMapEncoding))]; if (encodings.length > 1) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} mixes MCAL alpha encodings.`); const sourceEncoding = encodings[0] ?? "none"; const alphaMaps = overlays.map((layer) => decodeWowAdtAlphaMap(adtBuffer, chunk, layer)); const packed = sourceEncoding === "packed-4"; const effective = alphaMaps.map((map) => new Uint8Array(map)); if (packed) { for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) { let remaining = 255; for (let index = effective.length - 1; index >= 0; index -= 1) { const weight = Math.floor((effective[index][pixel] * remaining + 127) / 255); effective[index][pixel] = weight; remaining -= weight; } } } let maxWeightSum = 0; for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) { const sum = effective.reduce((value, map) => value + map[pixel], 0); maxWeightSum = Math.max(maxWeightSum, sum); if (sum > 255) throw new Error(`MCNK ${chunk.indexX},${chunk.indexY} effective overlays exceed 255 (${sum} at sample ${pixel}).`); } const rgba = Buffer.alloc(WEIGHT_SIZE * 4, 255); for (let pixel = 0; pixel < WEIGHT_SIZE; pixel += 1) { for (let channel = 0; channel < 3; channel += 1) rgba[pixel * 4 + channel] = effective[channel]?.[pixel] ?? 0; } return { rgba, sourceEncoding, sourcePolicy: packed ? "classic-nibble-to-big-alpha" : "direct-8bit-overlay-weights", channelLayerIndices: overlays.map((layer) => layer.index), maxWeightSum, }; } export function wowAdtNormalizedUv(row, column) { if (!Number.isInteger(row) || row < 0 || row > 16) throw new RangeError("MCVT row must be in [0,16]."); const columns = row % 2 ? 8 : 9; if (!Number.isInteger(column) || column < 0 || column >= columns) throw new RangeError(`MCVT column must be in [0,${columns - 1}].`); return [(column + (row % 2 ? 0.5 : 0)) / 8, row / 16]; }