@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,392 lines (1,119 loc) • 37.1 kB
JavaScript
/**
*
* @enum {boolean}
*/
import { assert } from "../assert.js";
import { array_copy } from "../collection/array/array_copy.js";
import { array_buffer_copy } from "../collection/array/typed/array_buffer_copy.js";
import { align_4 } from "./align_4.js";
import { EndianType } from "./EndianType.js";
import { half_to_float_uint16 } from "./half_to_float_uint16.js";
import { to_half_float_uint16 } from "./to_half_float_uint16.js";
/**
* Minimum number of bytes to grow the buffer by when the buffer is full.
* @type {number}
*/
const MIN_GROWTH_STEP = 1024;
/**
* 2^31-1, values above this will be cropped incorrectly when bit-shifting
* @type {number}
*/
const MAX_SAFE_UINT_VAR = 2147483647;
/**
* @readonly
* @type {number}
*/
const DEFAULT_INITIAL_SIZE = 1024;
/**
* Utility for reading/writing binary data.
* Mostly useful for serialization/deserialization tasks.
* The buffer is dynamically resized, so you do not need to manage the size manually.
* It is useful to think of this structure as a "stream".
*
* @example
* const buffer = new BinaryBuffer();
*
* buffer.writeUTF8String("Hello World");
*
* buffer.position = 0; // rewind to the beginning
*
* const deserialized = buffer.readUTF8String(); // "Hello World"
*
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class BinaryBuffer {
/**
* Default is little-endian as most platforms operate in little-endian
* The reason this is fixed is to ensure cross-platform compatibility as endianness in JavaScript is platform-dependent.
* @see https://en.wikipedia.org/wiki/Endianness
* @type {EndianType|boolean}
*/
endianness = EndianType.LittleEndian;
/**
* Current position in the buffer, where read and write operations will occur.
* Make sure to set this to the correct value before reading/writing data.
* Typically, this is set to 0 before reading/writing data.
* @type {number}
*/
position = 0;
/**
* @deprecated
*/
get length() {
throw new Error("Deprecated, use 'capacity' instead");
}
/**
* @deprecated
*/
set length(v) {
throw new Error("Deprecated, use 'capacity' instead");
}
/**
* Managed by the buffer, do not modify directly
* @type {number}
*/
capacity = DEFAULT_INITIAL_SIZE;
/**
* Raw underlying bytes attached to the buffer, note that this is managed by the `BinaryBuffer` and can grow/shrink as needed.
* @see setCapacity
* @see trim
* @type {ArrayBuffer}
* @private
*/
data = new ArrayBuffer(DEFAULT_INITIAL_SIZE);
/**
* Bound to the {@link data} buffer, do not modify directly.
* @type {DataView}
* @private
*/
dataView = new DataView(this.data);
/**
* Bound to the {@link data} buffer, do not modify directly.
* @type {Uint8Array}
* @private
*/
__data_uint8 = new Uint8Array(this.data);
/**
* When the buffer grows in size, this is the multiplication factor by which it grows.
* @type {number}
* @private
*/
__growFactor = 1.1;
/**
* Access raw underlying bytes attached to the buffer
* @return {Uint8Array}
*/
get raw_bytes() {
return this.__data_uint8;
}
/**
* Sets {@link capacity} to the size of the input data.
* Sets {@link position} to 0.
*
* Note: if you write to the buffer past the size of the input data {@link ArrayBuffer} - bound data will be re-allocated.
* @param {ArrayBuffer} data
*/
fromArrayBuffer(data) {
assert.defined(data, 'data');
assert.notNull(data, 'data');
this.data = data;
this.dataView = new DataView(data);
this.__data_uint8 = new Uint8Array(data);
this.capacity = data.byteLength;
this.position = 0;
}
/**
* Set capacity to contain data only up to the current `position`.
* This will re-allocate the `data` buffer if necessary.
* @returns {BinaryBuffer}
*/
trim() {
this.setCapacity(this.position);
return this;
}
/**
* Advance `position`(read/write cursor) a certain number of bytes forward.
*
* @param {number} byte_count
*/
skip(byte_count) {
assert.isNonNegativeInteger(byte_count, 'byte_count');
this.position += byte_count;
}
/**
* This will re-allocate {@link data} buffer if necessary.
* Note that all data is retained.
* Cannot shink past the current `position`.
*
* @param {number} capacity
* @throws {Error} if requested capacity is less than current `position`
*
*/
setCapacity(capacity) {
assert.isNonNegativeInteger(capacity, 'capacity');
if (capacity < this.position) {
throw new Error(`Attempting to set capacity(=${capacity}) below current position(=${this.position})`);
}
if (this.capacity === capacity) {
// already the right capacity
return;
}
const oldData = this.__data_uint8;
const newData = new Uint8Array(capacity);
if (this.position > 0) {
//copy old data
array_buffer_copy(
oldData.buffer, 0,
newData.buffer, 0,
Math.min(
oldData.buffer.byteLength,
newData.buffer.byteLength,
this.position
)
);
}
this.data = newData.buffer;
this.__data_uint8 = newData;
this.dataView = new DataView(this.data);
this.capacity = capacity;
}
/**
*
* @param {number} min_capacity
*/
ensureCapacity(min_capacity) {
assert.isNumber(min_capacity, 'min_capacity');
assert.greaterThanOrEqual(min_capacity, 0, 'min_capacity');
assert.notNaN(min_capacity, 'min_capacity');
const existing_capacity = this.capacity;
if (existing_capacity >= min_capacity) {
return;
}
const rough_new_capacity = Math.ceil(Math.max(
min_capacity,
existing_capacity * this.__growFactor,
existing_capacity + MIN_GROWTH_STEP
));
// align for easier memory operations
const aligned_new_capacity = align_4(rough_new_capacity);
this.setCapacity(aligned_new_capacity);
}
/**
*
* @returns {number}
*/
readFloat16() {
const u16 = this.readUint16();
return half_to_float_uint16(u16);
}
/**
*
* @returns {number}
*/
readFloat32() {
const result = this.dataView.getFloat32(this.position, this.endianness);
this.position += 4;
return result;
}
/**
*
* @returns {number}
*/
readFloat64() {
const result = this.dataView.getFloat64(this.position, this.endianness);
this.position += 8;
return result;
}
/**
*
* @return {number}
*/
readInt8() {
const result = this.dataView.getInt8(this.position);
this.position += 1;
return result;
}
/**
*
* @return {number}
*/
readInt16() {
const result = this.dataView.getInt16(this.position, this.endianness);
this.position += 2;
return result;
}
/**
*
* @returns {number}
*/
readInt32() {
const result = this.dataView.getInt32(this.position, this.endianness);
this.position += 4;
return result;
}
/**
*
* @returns {number}
*/
readUint8() {
const result = this.dataView.getUint8(this.position);
this.position += 1;
return result;
}
/**
*
* @returns {number}
*/
readUint16() {
const result = this.dataView.getUint16(this.position, this.endianness);
this.position += 2;
return result;
}
/**
*
* @returns {number}
*/
readUint16LE() {
const result = this.dataView.getUint16(this.position, EndianType.LittleEndian);
this.position += 2;
return result;
}
/**
*
* @returns {number}
*/
readUint16BE() {
const result = this.dataView.getUint16(this.position, EndianType.BigEndian);
this.position += 2;
return result;
}
/**
*
* @returns {number}
*/
readUint24() {
if (this.endianness === EndianType.BigEndian) {
return this.readUint24BE();
} else {
return this.readUint24LE();
}
}
/**
*
* @returns {number}
*/
readUint24LE() {
const b0 = this.dataView.getUint8(this.position);
const b1 = this.dataView.getUint8(this.position + 1);
const b2 = this.dataView.getUint8(this.position + 2);
this.position += 3;
return b0 | (b1 << 8) | (b2 << 16);
}
/**
*
* @returns {number}
*/
readUint24BE() {
const b0 = this.dataView.getUint8(this.position);
const b1 = this.dataView.getUint8(this.position + 1);
const b2 = this.dataView.getUint8(this.position + 2);
this.position += 3;
return b2 | (b1 << 8) | (b0 << 16);
}
/**
*
* @returns {number}
*/
readUint32() {
const result = this.dataView.getUint32(this.position, this.endianness);
this.position += 4;
return result;
}
/**
*
* @returns {number}
*/
readUint32LE() {
const result = this.dataView.getUint32(this.position, EndianType.LittleEndian);
this.position += 4;
return result;
}
/**
*
* @returns {number}
*/
readUint32BE() {
const result = this.dataView.getUint32(this.position, EndianType.BigEndian);
this.position += 4;
return result;
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Uint8Array} destination
*/
readUint8Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readUint8();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Uint16Array} destination
*/
readUint16Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readUint16();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Uint32Array|number[]|ArrayLike<number>} destination
*/
readUint32Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readUint32();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Int8Array} destination
*/
readInt8Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readInt8();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Int16Array} destination
*/
readInt16Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readInt16();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Int32Array} destination
*/
readInt32Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readInt32();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Float32Array|number[]} destination
*/
readFloat32Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readFloat32();
}
}
/**
*
* @param {number} destination_offset starting index in the destination array
* @param {number} length number of elements to read
* @param {Float64Array} destination
*/
readFloat64Array(destination, destination_offset, length) {
for (let i = 0; i < length; i++) {
destination[i + destination_offset] = this.readFloat64();
}
}
/**
*
* @param {number} source_offset starting index in the source array
* @param {number} length number of elements to read
* @param {Float32Array|number[]} source
*/
writeFloat32Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
for (let i = 0; i < length; i++) {
this.writeFloat32(source[i + source_offset]);
}
}
/**
*
* @param {number} value
*/
writeFloat16(value) {
const u16 = to_half_float_uint16(value);
this.writeUint16(u16);
}
/**
*
* @param {number} value
*/
writeFloat32(value) {
const end = this.position + 4;
this.ensureCapacity(end);
this.dataView.setFloat32(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {number} value
*/
writeFloat64(value) {
const end = this.position + 8;
this.ensureCapacity(end);
this.dataView.setFloat64(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {number} value
*/
writeInt8(value) {
const end = this.position + 1;
this.ensureCapacity(end);
this.dataView.setInt8(this.position, value);
this.position = end;
}
/**
*
* @param {number} value
*/
writeInt16(value) {
const end = this.position + 2;
this.ensureCapacity(end);
this.dataView.setInt16(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {number} value
*/
writeInt32(value) {
const end = this.position + 4;
this.ensureCapacity(end);
this.dataView.setInt32(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {Int8Array|number[]|ArrayLike<number>} source
* @param {number} source_offset
* @param {number} length
*/
writeInt8Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
// prevent resizing mid-copy
this.ensureCapacity(this.position + length);
for (let i = 0; i < length; i++) {
this.writeInt8(source[source_offset + i]);
}
}
/**
*
* @param {Int16Array|number[]|ArrayLike<number>} source
* @param {number} source_offset
* @param {number} length
*/
writeInt16Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
// prevent resizing mid-copy
this.ensureCapacity(this.position + 2 * length);
for (let i = 0; i < length; i++) {
this.writeInt16(source[source_offset + i]);
}
}
/**
*
* @param {Int32Array|number[]|ArrayLike<number>} source
* @param {number} source_offset
* @param {number} length
*/
writeInt32Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
// prevent resizing mid-copy
this.ensureCapacity(this.position + 4 * length);
for (let i = 0; i < length; i++) {
this.writeInt32(source[source_offset + i]);
}
}
/**
*
* @param {number} value
*/
writeUint8(value) {
const end = this.position + 1;
this.ensureCapacity(end);
this.dataView.setUint8(this.position, value);
this.position = end;
}
/**
*
* @param {Uint8Array|number[]} source
* @param {number} source_offset
* @param {number} length
*/
writeUint8Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
for (let i = 0; i < length; i++) {
this.writeUint8(source[source_offset + i]);
}
}
/**
*
* @param {number} value
*/
writeUint16(value) {
const end = this.position + 2;
this.ensureCapacity(end);
this.dataView.setUint16(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {number} value
*/
writeUint16BE(value) {
const end = this.position + 2;
this.ensureCapacity(end);
this.dataView.setUint16(this.position, value, EndianType.BigEndian);
this.position = end;
}
/**
*
* @param {number} value
*/
writeUint16LE(value) {
const end = this.position + 2;
this.ensureCapacity(end);
this.dataView.setUint16(this.position, value, EndianType.LittleEndian);
this.position = end;
}
/**
*
* @param {Uint16Array|number[]} source
* @param {number} source_offset
* @param {number} length
*/
writeUint16Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
for (let i = 0; i < length; i++) {
this.writeUint16(source[source_offset + i]);
}
}
/**
*
* @param {number} value
*/
writeUint24(value) {
if (this.endianness === EndianType.BigEndian) {
this.writeUint24BE(value);
} else {
this.writeUint24LE(value);
}
}
/**
*
* @param {number} value
*/
writeUint24BE(value) {
const end = this.position + 3;
this.ensureCapacity(end);
const b0 = value & 0xFF;
const b1 = (value >> 8) & 0xFF;
const b2 = (value >> 16) & 0xFF;
this.dataView.setUint8(this.position, b2);
this.dataView.setUint8(this.position + 1, b1);
this.dataView.setUint8(this.position + 2, b0);
this.position = end;
}
/**
*
* @param {number} value
*/
writeUint24LE(value) {
const end = this.position + 3;
this.ensureCapacity(end);
const b0 = value & 0xFF;
const b1 = (value >> 8) & 0xFF;
const b2 = (value >> 16) & 0xFF;
this.dataView.setUint8(this.position, b0);
this.dataView.setUint8(this.position + 1, b1);
this.dataView.setUint8(this.position + 2, b2);
this.position = end;
}
/**
* Write uint using a minimum number of bytes.
* Compact encoding scheme, if the value is 127 or less - only one byte will be used, if the value is 16383 or less - two bytes will be used, etc.
* NOTE: uses 7-bit encoding with 1 bit used for carry-over flag.
* NOTE: explicitly a little-endian format, {@link endianness} is ignored.
* @param {number} value must be an unsigned integer
*/
writeUintVar(value) {
assert.isNonNegativeInteger(value, 'value');
assert.ok(value <= MAX_SAFE_UINT_VAR, `value=[${value}] exceeds maximum safe limit[=${MAX_SAFE_UINT_VAR}]`);
let first = true;
let _v = value;
while (first || _v !== 0) {
first = false;
let lower7bits = (_v & 0x7f);
_v >>= 7;
if (_v > 0) {
//write carry-over flag
lower7bits |= 128;
}
this.writeUint8(lower7bits);
}
}
/**
* Read Uint of variable length, a compliment to {@link #writeUintVar}
* @returns {number}
*/
readUintVar() {
let more = true;
let value = 0;
let shift = 0;
while (more) {
let lower7bits = this.readUint8();
//read carry-over flag
more = (lower7bits & 128) !== 0;
//read value part of the byte
value |= (lower7bits & 0x7f) << shift;
//increment shift
shift += 7;
}
return value;
}
/**
*
* @param {number} value
*/
writeUint32(value) {
const end = this.position + 4;
this.ensureCapacity(end);
this.dataView.setUint32(this.position, value, this.endianness);
this.position = end;
}
/**
*
* @param {number} value
*/
writeUint32BE(value) {
const end = this.position + 4;
this.ensureCapacity(end);
this.dataView.setUint32(this.position, value, EndianType.BigEndian);
this.position = end;
}
/**
*
* @param {number} value
*/
writeUint32LE(value) {
const end = this.position + 4;
this.ensureCapacity(end);
this.dataView.setUint32(this.position, value, EndianType.LittleEndian);
this.position = end;
}
/**
*
* @param {Uint32Array|number[]|ArrayLike<number>} source
* @param {number} source_offset
* @param {number} length
*/
writeUint32Array(source, source_offset, length) {
assert.isArrayLike(source, 'source');
assert.greaterThanOrEqual(source.length, source_offset + length, "source underflow");
// prevent resizing mid-copy
this.ensureCapacity(this.position + 4 * length);
for (let i = 0; i < length; i++) {
this.writeUint32(source[source_offset + i]);
}
}
/**
*
* @param {Uint8Array|Uint8ClampedArray} array
* @param {number} source_offset
* @param {number} length
*/
writeBytes(array, source_offset, length) {
const source_end = source_offset + length;
assert.greaterThanOrEqual(array.length, source_end, 'source array underflow');
const targetAddress = this.position;
const end = targetAddress + length;
this.ensureCapacity(end);
if (source_offset === 0 && array.length === length) {
// copying entire source array
this.__data_uint8.set(array, targetAddress);
} else if (typeof array.subarray === "function") {
// typed array, use "subarray" method
this.__data_uint8.set(array.subarray(source_offset, source_end), targetAddress);
} else {
// not a typed array, copy byte by byte manually
for (let i = 0; i < length; i++) {
this.__data_uint8[targetAddress + i] = array[source_offset + i];
}
}
this.position = end;
}
/**
*
* @param {Uint8Array} destination
* @param {number} destination_offset
* @param {number} length
*/
readBytes(destination, destination_offset, length) {
const source_position = this.position;
const end = source_position + length;
const uint8 = this.__data_uint8;
if (length < 128) {
// small copy
array_copy(uint8, source_position, destination, destination_offset, length);
} else {
destination.set(uint8.subarray(source_position, end), destination_offset);
}
this.position = end;
}
/**
*
* @param {string} string
*/
writeUTF8String(string) {
// Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
// Original license is Apache 2.0
if (string === null) {
//mark NULL
this.writeUint32(4294967295);
//bail, no string data to write
return;
} else if (string === undefined) {
//mark undefined
this.writeUint32(4294967294);
return;
}
let pos = 0;
const len = string.length;
if (len >= 4294967294) {
throw new Error('String is too long');
}
//mark non-NULL
this.writeUint32(len);
let cursor = this.position; // output position
const expected_byte_size = Math.max(32, len + (len >> 1) + 7); // 1.5x size
this.ensureCapacity(expected_byte_size + cursor);
let target = this.__data_uint8;
let capacity = this.capacity;
while (pos < len) {
let value = string.charCodeAt(pos++);
if (value >= 0xd800 && value <= 0xdbff) {
// high surrogate
if (pos < len) {
const extra = string.charCodeAt(pos);
if ((extra & 0xfc00) === 0xdc00) {
++pos;
value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
}
}
if (value >= 0xd800 && value <= 0xdbff) {
continue; // drop lone surrogate
}
}
// expand the buffer if we couldn't write 4 bytes
if (cursor + 4 > capacity) {
this.ensureCapacity(cursor + 4);
// rebind variables
capacity = this.capacity;
target = this.__data_uint8;
}
if ((value & 0xffffff80) === 0) { // 1-byte
target[cursor++] = value; // ASCII
continue;
} else if ((value & 0xfffff800) === 0) { // 2-byte
target[cursor++] = ((value >> 6) & 0x1f) | 0xc0;
} else if ((value & 0xffff0000) === 0) { // 3-byte
target[cursor++] = ((value >> 12) & 0x0f) | 0xe0;
target[cursor++] = ((value >> 6) & 0x3f) | 0x80;
} else if ((value & 0xffe00000) === 0) { // 4-byte
target[cursor++] = ((value >> 18) & 0x07) | 0xf0;
target[cursor++] = ((value >> 12) & 0x3f) | 0x80;
target[cursor++] = ((value >> 6) & 0x3f) | 0x80;
} else {
// FIXME: do we care?
continue;
}
target[cursor++] = (value & 0x3f) | 0x80;
}
this.position = cursor;
}
/**
*
* @returns {string}
*/
readUTF8String() {
// Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
// Original license is Apache 2.0
//check for null
const stringLength = this.readUint32();
if (stringLength === 4294967295) {
//null string
return null;
} else if (stringLength === 4294967294) {
//undefined string
return undefined;
}
const bytes = this.__data_uint8;
let result = "";
let i = this.position;
let charCount = 0;
while (i < this.capacity && charCount < stringLength) {
const byte1 = bytes[i++];
let codePoint;
if (byte1 === 0) {
break; // NULL
}
if ((byte1 & 0x80) === 0) { // 1-byte
codePoint = byte1;
} else if ((byte1 & 0xe0) === 0xc0) { // 2-byte
const byte2 = bytes[i++] & 0x3f;
codePoint = (((byte1 & 0x1f) << 6) | byte2);
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = bytes[i++] & 0x3f;
const byte3 = bytes[i++] & 0x3f;
codePoint = (((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3);
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = bytes[i++] & 0x3f;
const byte3 = bytes[i++] & 0x3f;
const byte4 = bytes[i++] & 0x3f;
// this can be > 0xffff, so possibly generate surrogates
codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
if (codePoint > 0xffff) {
// codepoint &= ~0x10000;
codePoint -= 0x10000;
result += String.fromCharCode((codePoint >>> 10) & 0x3ff | 0xd800);
charCount++;
codePoint = 0xdc00 | codePoint & 0x3ff;
}
} else {
// FIXME: we're ignoring this
}
charCount++;
result += String.fromCharCode(codePoint);
}
this.position = i;
return result;
}
/**
* Write an ASCII (American Standard Code for Information Interchange) string. If the string contains characters that are not representable by ASCII, an error will be thrown.
* Note that ASCII only has 128 code points (characters), so this method is not suitable for representing UTF-8 strings.
* If the string is not ASCII representable - use {@link writeUTF8String} instead.
*
* @see https://en.wikipedia.org/wiki/ASCII
*
* @param {string} string
*/
writeASCIIString(string) {
assert.isString(string, 'string');
const char_count = string.length;
const start = this.position;
const end = start + char_count;
this.ensureCapacity(end);
for (let i = 0; i < char_count; i++) {
const char_code = string.charCodeAt(i);
if (char_code > 0x80) {
throw new Error(`Character ${String.fromCharCode(char_code)} can\'t be represented by a US-ASCII byte.`);
}
this.__data_uint8[start + i] = char_code;
}
this.position = end;
}
/**
* Read ASCII (American Standard Code for Information Interchange) characters to the buffer.
* Input is not validated, if the string contains non-ASCII characters, the result is unspecified.
*
* @see https://en.wikipedia.org/wiki/ASCII
*
* @param {number} length maximum number of characters to read. If `null_terminated` flag is on, resulting string might be shorter than `length`
* @param {boolean} [null_terminated] if true will stop reading when encountering 0 byte value character (NULL)
* @returns {string}
*/
readASCIICharacters(length, null_terminated = false) {
assert.isNonNegativeInteger(length, 'length');
assert.isBoolean(null_terminated, 'null_terminated');
let result = "";
for (let i = 0; i < length; i++) {
const code = this.readUint8();
if (null_terminated && code === 0) {
// null-termination
break;
}
result += String.fromCharCode(code);
}
return result;
}
/**
* Represent the object as a string. Useful mainly for debugging.
*
* @return {string}
*/
toString() {
return `BinaryBuffer[position=${this.position}, capacity=${this.capacity}, endianness=${this.endianness}]`;
}
/**
* Useful for debugging, outputs contents of the buffer in hex format.
* Only includes data up to the .position
*
* @example
* const b = new BinaryBuffer();
* b.writeUint8(0xCA);
* b.writeUint8(0xFE);
* b.toHexString(); // "CAFE"
*
* @return {string}
*/
toHexString() {
const uint8 = this.__data_uint8;
const end = Math.min(uint8.length, this.position);
let result = '';
for (let i = 0; i < end; i++) {
const byte = uint8[i];
result += byte.toString(16).padStart(2, '0').toUpperCase();
}
return result;
}
/**
*
* @param {EndianType} type
* @return {BinaryBuffer}
*/
static fromEndianness(type) {
assert.enum(type, EndianType, 'type');
const r = new BinaryBuffer();
r.endianness = type;
return r;
}
/**
*
* @param {ArrayBuffer} v
* @return {BinaryBuffer}
*/
static fromArrayBuffer(v) {
const r = new BinaryBuffer();
r.fromArrayBuffer(v);
return r;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {string} Copied value
*/
static copyUTF8String(source, target) {
const v = source.readUTF8String();
target.writeUTF8String(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyUintVar(source, target) {
const v = source.readUintVar();
target.writeUintVar(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyUint8(source, target) {
const v = source.readUint8();
target.writeUint8(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyUint16(source, target) {
const v = source.readUint16();
target.writeUint16(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyUint32(source, target) {
const v = source.readUint32();
target.writeUint32(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyFloat32(source, target) {
const v = source.readFloat32();
target.writeFloat32(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @returns {number} Copied value
*/
static copyFloat64(source, target) {
const v = source.readFloat64();
target.writeFloat64(v);
return v;
}
/**
*
* @param {BinaryBuffer} source
* @param {BinaryBuffer} target
* @param {number} length number of bytes to copy
* @returns {Uint8Array} Copied data
*/
static copyBytes(source, target, length) {
assert.isNonNegativeInteger(length, 'length');
assert.defined(source, 'source');
assert.defined(target, 'target');
const temp = new Uint8Array(length);
source.readBytes(temp, 0, length);
target.writeBytes(temp, 0, length);
return temp;
}
}
/**
* @readonly
* @type {boolean}
*/
BinaryBuffer.prototype.isBinaryBuffer = true;