@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,206 lines (956 loc) • 30 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";
/**
*
* @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
*/
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
* @type {EndianType|boolean}
*/
endianness = EndianType.LittleEndian;
/**
*
* @type {number}
*/
position = 0;
get length() {
throw new Error("Deprecated, use 'capacity' instead");
}
set length(v) {
throw new Error("Deprecated, use 'capacity' instead");
}
/**
*
* @type {number}
*/
capacity = DEFAULT_INITIAL_SIZE;
/**
*
* @type {ArrayBuffer}
* @private
*/
data = new ArrayBuffer(DEFAULT_INITIAL_SIZE);
/**
*
* @type {DataView}
* @private
*/
dataView = new DataView(this.data);
/**
*
* @type {Uint8Array}
* @private
*/
__data_uint8 = new Uint8Array(this.data);
/**
*
* @type {number}
* @private
*/
__growFactor = 1.1;
/**
* Access raw underlying bytes attached to the buffer
* @return {Uint8Array}
*/
get raw_bytes() {
return this.__data_uint8;
}
/**
* @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 current position
* @returns {BinaryBuffer}
*/
trim() {
this.setCapacity(this.position);
return this;
}
/**
*
* @param {number} capacity
*/
setCapacity(capacity) {
assert.isNonNegativeInteger(capacity, 'capacity');
if (capacity < this.position) {
throw new Error(`Attempting to set capacity(=${capacity}) below current position(=${this.position})`);
}
const oldData = this.__data_uint8;
const newData = new Uint8Array(capacity);
//copy old data
array_buffer_copy(
oldData.buffer, 0,
newData.buffer, 0,
Math.min(oldData.buffer.byteLength, newData.buffer.byteLength)
);
this.data = newData.buffer;
this.__data_uint8 = newData;
this.dataView = new DataView(this.data);
this.capacity = capacity;
}
/**
*
* @param {number} min_capacity
*/
ensureCapacity(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;
}
readInt8() {
const result = this.dataView.getInt8(this.position);
this.position += 1;
return result;
}
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 {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 of variable length
* NOTE: uses 7-bit encoding with 1 bit used for carry-over flag
* @param {number} value
*/
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;
while (first || value !== 0) {
first = false;
let lower7bits = (value & 0x7f);
value >>= 7;
if (value > 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;
}
/**
* Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
* @licence Original license is Apache 2.0
* @param {String} string
*/
writeUTF8String(string) {
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;
}
/**
* Adapted from https://github.com/samthor/fast-text-encoding/blob/master/text.js
* @licence Original license is Apache 2.0
* @returns {String}
*/
readUTF8String() {
//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;
}
/**
*
* @param {string} string
*/
writeASCIIString(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 > 0xFF) {
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;
}
/**
*
* @param {number} length
* @param {boolean} [null_terminated]
* @returns {string}
*/
readASCIICharacters(length, null_terminated = false) {
let result = "";
for (let i = 0; i < length; i++) {
const code = this.readUint8();
if (null_terminated && code === 0) {
break;
}
result += String.fromCharCode(code);
}
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
*/
static copyBytes(source, target, length) {
const temp = new Uint8Array(length);
source.readBytes(temp, 0, length);
target.writeBytes(temp, 0, length);
return temp;
}
}
/**
* @readonly
* @type {boolean}
*/
BinaryBuffer.prototype.isBinaryBuffer = true;