UNPKG

deadem

Version:

JavaScript (Node.js & Browsers) parser for Deadlock (Valve Source 2 Engine) demo/replay files

608 lines (485 loc) 16.4 kB
import { Buffer } from 'node:buffer'; import VarInt32 from '#data/VarInt32.js'; /** * A class for reading data at the bit level from {@link Buffer} or {@link Uint8Array}. */ class BitBuffer { /** * @constructor * @param {Buffer|Uint8Array} buffer */ constructor(buffer) { this._buffer = buffer; this._pointers = { byte: 0, bit: 0 }; } /** * @static * @returns {number} */ static get BITS_PER_BYTE() { return BITS_PER_BYTE; } /** * Reads an unsigned 32-bit integer from the given buffer, supporting buffers of length 1 to 4 bytes. * * @public * @static * @param {Buffer} buffer - The buffer to read from. Must be between 1 and 4 bytes long. * @returns {number} - The unsigned integer read from the buffer. * @throws {Error} If the buffer size is not between 1 and 4 bytes. */ static readUInt32LE(buffer) { switch (buffer.byteLength) { case 1: return buffer[0] >>> 0; case 2: return (buffer[0] | (buffer[1] << 8)) >>> 0; case 3: return (buffer[0] | (buffer[1] << 8) | (buffer[2] << 16)) >>> 0; case 4: return (buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24)) >>> 0; default: throw new Error(`Unexpected buffer size [ ${buffer.byteLength} ]`); } } /** * Returns the number of read bits. * * @public * @returns {number} */ getReadCount() { return this._pointers.byte * BITS_PER_BYTE + this._pointers.bit; } /** * Returns the number of remaining bits available to read in the buffer. * * @public * @returns {number} */ getUnreadCount() { return this._buffer.length * BITS_PER_BYTE - (this._pointers.byte * BITS_PER_BYTE + this._pointers.bit); } /** * Moves the internal read pointer forward or backward by a given number of bits. * * @public * @param {number} bits - Number of bits to move. */ move(bits) { const abs = Math.abs(bits); if (abs === 0) { return; } if (bits > 0 && bits > this.getUnreadCount()) { throw new Error(`Cannot move pointer forward by ${bits} bits: only [ ${this.getUnreadCount()} ] bits unread`); } if (bits < 0 && abs > this.getReadCount()) { throw new Error(`Cannot move pointer backward by ${abs} bits: only [ ${this.getReadCount()} ] bits read`); } const numberOfBytes = Math.floor(abs / BITS_PER_BYTE); const numberOfBits = abs % BITS_PER_BYTE; if (bits > 0) { this._pointers.byte += numberOfBytes; if (this._pointers.bit + numberOfBits < BITS_PER_BYTE) { this._pointers.bit += numberOfBits; } else { this._pointers.byte += 1; this._pointers.bit = (this._pointers.bit + numberOfBits) % BITS_PER_BYTE; } } if (bits < 0) { this._pointers.byte -= numberOfBytes; if (this._pointers.bit - numberOfBits >= 0) { this._pointers.bit -= numberOfBits; } else { this._pointers.byte -= 1; this._pointers.bit = BITS_PER_BYTE - Math.abs(this._pointers.bit - numberOfBits); } } } /** * Reads the specified number of bits. * * @public * @param {number} numberOfBits - The number of bits to read. * @param {boolean} [allocateNew=false] - Whether to allocate a new memory for returning buffer. * If `true`, a new buffer is allocated. * If `false` (default), a reusable buffer may be returned, which can be overwritten in subsequent operations. * @returns {Buffer|Uint8Array} */ read(numberOfBits, allocateNew = false) { const numberOfBytes = Math.ceil(numberOfBits / BITS_PER_BYTE); if (!allocateNew && numberOfBytes <= REUSABLE_BUFFER_SIZE) { return this._read(numberOfBits, pool[numberOfBytes - 1]); } else { return this._read(numberOfBits, Buffer.allocUnsafe(numberOfBytes)); } } /** * Reads the specified number of bits and writes them into the provided buffer. * * @public * @param {number} numberOfBits - The number of bits to read. * @param {Buffer|Uint8Array} buffer - The buffer to write the results into. * @returns {Buffer|Uint8Array} */ readInBuffer(numberOfBits, buffer) { return this._read(numberOfBits, buffer); } /** * Reads an angle encoded in `n` bits from the buffer. * * @param {number} n - The number of bits. * @returns {number} - The angle. */ readAngle(n) { const buffer = this.read(n); const value = BitBuffer.readUInt32LE(buffer); return (value * 360) / (1 << n); } /** * Reads a single bit from the buffer. * * @public * @returns {boolean} */ readBit() { const value = ((this._buffer[this._pointers.byte] >> this._pointers.bit) & 1) === 1; this.move(1); return value; } /** * Reads a coordinate. * * @public * @returns {number} */ readCoordinate() { let value = 0; const hasInteger = this.readBit(); const hasFractional = this.readBit(); if (hasInteger || hasFractional) { const sign = this.readBit(); let integer = 0; if (hasInteger) { const buffer = this.read(14); integer = buffer.readUInt16LE() + 1; } let fractional = 0; if (hasFractional) { const buffer = this.read(5); fractional = buffer.readUInt8(); } value = integer + fractional * (1 / (1 << 5)); if (sign) { value = -value; } } return value; } /** * Reads a precise coordinate encoded in 20 bits. * * @public * @returns {number} */ readCoordinatePrecise() { const value = BitBuffer.readUInt32LE(this.read(20)); return value * (360 / (1 << 20)) - 180; } /** * Reads 32 bits from the buffer and converts them to a float using little-endian format. * * @public * @returns {number} The float value interpreted from the 32-bit buffer. */ readFloat() { const buffer = this.read(32); return buffer.readFloatLE(); } /** * Reads a normal value (normalized float in the range [-1.0, 1.0]) from the buffer. * The value is encoded using 12 bits, where the first bit indicates the sign * and the remaining 11 bits represent the magnitude. * * @public * @returns {number} The normalized value in the range [-1.0, 1.0]. */ readNormal() { const sign = this.readBit(); const length = this.read(11).readUInt16LE(); const value = length * (1 / ((1 << 11) - 1)); if (sign) { return -value; } else { return value; } } /** * Reads a normal vector from the buffer. * * @public * @returns {Float32Array} An array containing the X, Y, and Z components of the normal vector. */ readNormalVector() { const vector = new Float32Array(3); const hasX = this.readBit(); const hasY = this.readBit(); if (hasX) { vector[0] = this.readNormal(); } if (hasY) { vector[1] = this.readNormal(); } const negativeZ = this.readBit(); const sum = Math.pow(vector[0], 2) + Math.pow(vector[1], 2); if (sum < 1) { vector[2] = Math.sqrt(1 - sum); } else { vector[2] = 0; } if (negativeZ) { vector[2] = -vector[2]; } return vector; } /** * Reads a null-terminated string from the buffer, byte by byte, * until a zero byte is found or the optional length limit is reached. * * @public * @param {number=} length - Maximum number of bytes to read. * @returns {string} The decoded string. */ readString(length) { const bytes = [ ]; while (true) { if (Number.isInteger(length) && bytes.length >= length) { break; } const buffer = this.read(BITS_PER_BYTE); if (buffer[0] === 0) { break; } bytes.push(buffer[0]); } return Buffer.from(bytes).toString(); } /** * Reads an unsigned 8-bit integer (1 byte) from the buffer. * * @public * @returns {number} The read unsigned integer (0–255). */ readUInt8() { const buffer = this.read(BITS_PER_BYTE); return buffer[0] >>> 0; } /** * Reads an unsigned variable-length integer encoded in Source 2's custom bit-packed format. * The initial 6 bits determine how many additional bits to read. * * @public * @returns {number} The decoded unsigned integer. */ readUVarInt() { let result = this.read(6).readUInt8(); switch (result & 48) { case 16: { const value = this.read(4).readUInt8(); result = (result & 15) | (value << 4); break; } case 32: { const value = this.read(8).readUInt8(); result = (result & 15) | (value << 4); break; } case 48: { const value = this.read(28).readUInt32LE(); result = (result & 15) | (value << 4); break; } default: { break; } } return result >>> 0; } /** * Reads a signed variable-length integer from the buffer. * * @public * @returns {number} The decoded signed integer. */ readVarInt32() { const unsigned = this.readUVarInt32(); return (unsigned >> 1) ^ -(unsigned & 1); } /** * Reads a signed variable-length 64-bit integer from the buffer. * * @public * @returns {BigInt} */ readVarInt64() { const unsigned = this.readUVarInt64(); return (unsigned >> 1n) ^ -(unsigned & 1n); } /** * Reads an unsigned variable-length integer from the buffer. * Each byte contributes 7 bits to the result; the highest bit indicates continuation. * * @public * @returns {number} The decoded unsigned integer. */ readUVarInt32() { let result = 0; let offset = 0; let byte; for (let i = 0; i < VarInt32.MAXIMUM_SIZE_BYTES; i++) { byte = this.readUInt8(); result |= (byte & 0x7F) << (offset * 7); if ((byte & 0x80) === 0) { return result >>> 0; } offset += 1; } } /** * Reads an unsigned variable-length 64-bit integer from the buffer. * Each byte contributes 7 bits to the result; the highest bit indicates continuation. * * The maximum amount of bytes read possible here is 10. Here is why: * 1) 64-bit integer itself takes 8 bytes; * 2) Each byte includes a continuation bit (the highest bit). Therefore, * a 9th byte is required. Since the 9th byte also has a continuation bit, * it adds up to 9 bytes + 1 continuation bit, resulting in a total of 10 bytes * to represent the maximum possible 64-bit integer. * * @public * @returns {BigInt} */ readUVarInt64() { let continuation = true; let iterations = 0n; let value = 0n; while (continuation) { const byte = BigInt(this.readUInt8()); if (iterations > 9n || (iterations === 9n && byte > 1n)) { throw new Error('Overflow'); } value |= (byte & 127n) << (7n * iterations); continuation = (byte & 128n) === 128n; iterations++; } return value; } /** * Reads a variable-length unsigned integer representing a part of the {@link FieldPath}. * The number of bits read depends on a series of flags (prefix bits). * * @public * @returns {number} */ readUVarIntFieldPath() { let flag; flag = this.readBit(); if (flag) { return this.read(2).readUInt8(); } flag = this.readBit(); if (flag) { return this.read(4).readUInt8(); } flag = this.readBit(); if (flag) { return this.read(10).readUInt16LE(); } flag = this.readBit(); if (flag) { return BitBuffer.readUInt32LE(this.read(17)); } return this.read(31).readUInt32LE(); } /** * Resets internal byte and bit pointers to the beginning of the buffer. * * @public */ reset() { this._pointers.byte = 0; this._pointers.bit = 0; } /** * Reads the specified number of bits and writes them into the provided buffer. * * @protected * @param {number} numberOfBits - The number of bits to read. * @param {Buffer|Uint8Array} buffer - The buffer to write the results into. * @returns {Buffer|Uint8Array} - A buffer containing the read data. */ _read(numberOfBits, buffer) { const unread = this.getUnreadCount(); if (numberOfBits > unread) { throw new Error(`Unable to read [ ${numberOfBits} ] bit(s) - only [ ${unread} ] bit(s) left`); } const numberOfRequestedBytes = Math.ceil(numberOfBits / BITS_PER_BYTE); const numberOfAffectedBytes = Math.ceil((this._pointers.bit + numberOfBits) / BITS_PER_BYTE); let extraByte; if (numberOfAffectedBytes > numberOfRequestedBytes) { extraByte = this._buffer[this._pointers.byte + numberOfAffectedBytes - 1]; } else { extraByte = 0; } for (let i = 0; i < numberOfRequestedBytes; i++) { buffer[i] = this._buffer[this._pointers.byte + i]; } const zeroBitsOffset = this._pointers.bit; const zeroBitsIgnored = numberOfAffectedBytes * BITS_PER_BYTE - (this._pointers.bit + numberOfBits); buffer[0] &= MASK[MASK_DIRECTION.RIGHT][zeroBitsOffset]; if (numberOfAffectedBytes > numberOfRequestedBytes) { extraByte &= MASK[MASK_DIRECTION.LEFT][zeroBitsIgnored]; } else { buffer[buffer.length - 1] &= MASK[MASK_DIRECTION.LEFT][zeroBitsIgnored]; } if (zeroBitsOffset > 0) { buffer[0] = buffer[0] >>> zeroBitsOffset; for (let i = 0; i < numberOfRequestedBytes; i++) { let next; if (i < numberOfRequestedBytes - 1) { next = buffer[i + 1]; } else { next = extraByte; } buffer[i] |= (next & MASK[MASK_DIRECTION.LEFT][BITS_PER_BYTE - zeroBitsOffset]) << (BITS_PER_BYTE - zeroBitsOffset); if (i < numberOfRequestedBytes) { buffer[i + 1] = buffer[i + 1] >>> zeroBitsOffset; } } } this._pointers.byte += Math.floor((this._pointers.bit + numberOfBits) / BITS_PER_BYTE); this._pointers.bit = (this._pointers.bit + numberOfBits) % BITS_PER_BYTE; return buffer; } } const BITS_PER_BYTE = 8; const MASK_DIRECTION = { LEFT: 'left', RIGHT: 'right' }; const MASK = { [MASK_DIRECTION.LEFT]: [ 255, 127, 63, 31, 15, 7, 3, 1 ], [MASK_DIRECTION.RIGHT]: [ 255, 254, 252, 248, 240, 224, 192, 128 ] }; const REUSABLE_BUFFER_SIZE = 4; const reusable = Buffer.allocUnsafe(REUSABLE_BUFFER_SIZE); const pool = [ ]; for (let i = 0; i < REUSABLE_BUFFER_SIZE; i++) { pool.push(reusable.subarray(0, i + 1)); } export default BitBuffer;