@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
396 lines (323 loc) • 10.7 kB
JavaScript
import { assert } from "../../../core/assert.js";
import { dec2hex } from "../../../core/binary/dec2hex.js";
import { hex2dec } from "../../../core/binary/hex2dec.js";
import { array_copy } from "../../../core/collection/array/array_copy.js";
import { randomBytes } from "../../../core/math/random/randomBytes.js";
import { randomUint8 } from "../../../core/math/random/randomUint8.js";
import { seededRandom } from "../../../core/math/random/seededRandom.js";
// Previous uuid creation time
let _last_milliseconds = 0;
let _last_nanoseconds = 0;
// seed random with current time to lower potential correlation
const random = seededRandom(Date.now());
// node and clockseq need to be initialized to random values.
const _node_id = new Uint8Array(6);
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
randomBytes(_node_id, 0, random, 6);
_node_id[0] |= 0x01;
// Per 4.2.2, randomize (14 bit) clockseq
let _clock_seq = ((randomUint8(random) << 8) | randomUint8(random)) & 0x3fff;
/**
* Universally Unique Identifier (UUID), a 128 bit label used to uniquely identify resources.
* The default is Nil UUID, where all bits are set to 0, see {@link UUID.nil}.
* Also known as GUID ( Globally Unique IDentifier).
*
* @see IETF RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122
* @example
* class Student{
* id: UUID
* name: string
* age: number
* }
* @example
* const id_a = UUID.v1();
* const id_b = UUID.v1();
*
* id_a.equals(id_b); // false
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class UUID {
#data = new Uint8Array(16);
/**
*
* @param {number[]|Uint8Array|ArrayLike<number>} bytes
*/
set data(bytes) {
assert.isArrayLike(bytes, 'bytes');
assert.greaterThanOrEqual(bytes.length, 16, 'bytes.length < 16');
array_copy(bytes, 0, this.#data, 0, 16);
}
/**
*
* @returns {Uint8Array}
*/
get data() {
return this.#data;
}
/**
* The version number is in the most significant 4 bits of the timestamp (bits 4 through 7 of the time_hi_and_version field).
*
* @see https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.3
* @return {number}
* @example
* UUID.v1().version === 1
* UUID.v4().version === 4
*/
get version() {
const bits = this.#data[6];
// using bits 4 through 7
return (bits >> 4) & 15;
}
/**
* The nil UUID is special form of UUID that is specified to have all 128 bits set to zero.
* Clears all bits of this UUID.
* @returns {void}
*/
nil() {
this.#data.fill(0);
}
/**
* Generate Variant 1 UUID
* @returns {void}
*/
v1() {
// @see https://github.com/uuidjs/uuid/blob/8f028c4ea42ce41a9a9dc5fa634abe525b2e2066/src/v1.js#L17
const b = this.#data;
let clock_seq = _clock_seq;
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
let msecs = Date.now();
// Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
let nanoseconds = _last_nanoseconds + 1;
// Time since last uuid creation (in msecs)
const dt = msecs - _last_milliseconds + (nanoseconds - _last_nanoseconds) / 10000;
// Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0) {
clock_seq = (clock_seq + 1) & 0x3fff;
}
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _last_milliseconds)) {
nanoseconds = 0;
}
// Per 4.2.1.2 Throw error if too many uuids are requested
if (nanoseconds >= 10000) {
throw new Error(".v1(): Can't create more than 10M uuids/sec");
}
_last_milliseconds = msecs;
_last_nanoseconds = nanoseconds;
_clock_seq = clock_seq;
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000;
let i = 0
// `time_low`
const tl = ((msecs & 0xfffffff) * 10000 + nanoseconds) % 0x100000000;
b[i++] = (tl >>> 24) & 0xff;
b[i++] = (tl >>> 16) & 0xff;
b[i++] = (tl >>> 8) & 0xff;
b[i++] = tl & 0xff;
// `time_mid`
const tmh = ((msecs / 0x100000000) * 10000) & 0xfffffff;
b[i++] = (tmh >>> 8) & 0xff;
b[i++] = tmh & 0xff;
// `time_high_and_version`
b[i++] = ((tmh >>> 24) & 0xf) | 0x10; // include version
b[i++] = (tmh >>> 16) & 0xff;
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = (clock_seq >>> 8) | 0x80;
// `clock_seq_low`
b[i++] = clock_seq & 0xff;
// `node`
for (let n = 0; n < 6; ++n) {
b[i + n] = _node_id[n];
}
}
/**
*
* @return {UUID}
*/
static v1() {
const uuid = new UUID();
uuid.v1();
return uuid;
}
/**
* Generate Variant 4 UUID (fully random)
* @returns {void}
*/
v4() {
const data = this.data;
for (let i = 0; i < 16; i += 4) {
const r = (random() * 4294967296) >>> 0;
data[i] = r & 0xff;
data[i + 1] = (r >>> 8) & 0xff;
data[i + 2] = (r >>> 16) & 0xff;
data[i + 3] = (r >>> 24) & 0xff;
}
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
data[6] = (data[6] & 0x0f) | 0x40;
data[8] = (data[8] & 0x3f) | 0x80;
}
/**
* Variant 4 UUID generator (fully random)
* @returns {UUID}
*/
static v4() {
const r = new UUID();
r.v4();
return r;
}
/**
* Shortcut to generate string-form ID
* Uses v4(random) UUID
* @return {string}
*/
static string() {
return UUID.v4().toString();
}
/***
* Parses standard UUID string of form AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
* @param {string} string String in UUID format
*/
parse(string) {
assert.isString(string, 'string');
assert.greaterThanOrEqual(string.length, 36, 'string.length');
const arr = this.#data;
// Parse ########-....-....-....-............
const v0 = hex2dec(string.slice(0, 8));
arr[0] = (v0) >>> 24;
arr[1] = (v0 >>> 16) & 0xff;
arr[2] = (v0 >>> 8) & 0xff;
arr[3] = v0 & 0xff;
// Parse ........-####-....-....-............
const v1 = hex2dec(string.slice(9, 13));
arr[4] = (v1) >>> 8;
arr[5] = v1 & 0xff;
// Parse ........-....-####-....-............
const v2 = hex2dec(string.slice(14, 18));
arr[6] = (v2) >>> 8;
arr[7] = v2 & 0xff;
// Parse ........-....-....-####-............
const v3 = hex2dec(string.slice(19, 23));
arr[8] = (v3) >>> 8;
arr[9] = v3 & 0xff;
// Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
const v4 = hex2dec(string.slice(24, 36));
arr[10] = (v4 / 0x10000000000) & 0xff;
arr[11] = (v4 / 0x100000000) & 0xff;
arr[12] = (v4 >>> 24) & 0xff;
arr[13] = (v4 >>> 16) & 0xff;
arr[14] = (v4 >>> 8) & 0xff;
arr[15] = v4 & 0xff;
}
/**
*
* @param {string} string
* @return {UUID}
*/
static parse(string) {
const r = new UUID();
r.parse(string);
return r;
}
/**
* Standard UUID string in from: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
* Result is compatible with the {@link UUID.parse} method.
* @example
* a88bb73a-c89f-11ed-afa1-0242ac120002
* @returns {string}
*/
toString() {
const bytes = this.#data;
return (
dec2hex(bytes[0]) +
dec2hex(bytes[1]) +
dec2hex(bytes[2]) +
dec2hex(bytes[3]) +
'-' +
dec2hex(bytes[4]) +
dec2hex(bytes[5]) +
'-' +
dec2hex(bytes[6]) +
dec2hex(bytes[7]) +
'-' +
dec2hex(bytes[8]) +
dec2hex(bytes[9]) +
'-' +
dec2hex(bytes[10]) +
dec2hex(bytes[11]) +
dec2hex(bytes[12]) +
dec2hex(bytes[13]) +
dec2hex(bytes[14]) +
dec2hex(bytes[15])
);
}
/**
* Sets this UUID to hold the same value as the other
* @param {UUID} other
*/
copy(other) {
this.#data.set(other.#data);
}
/**
*
* @return {UUID}
*/
clone() {
const r = new UUID();
r.copy(this);
return r;
}
/**
*
* @param {UUID} other
* @returns {boolean}
*/
equals(other) {
const this_data = this.#data;
const other_data = other.#data;
for (let i = 0; i < 16; i++) {
if (this_data[i] !== other_data[i]) {
return false;
}
}
return true;
}
/**
* Compute hash sum
* @returns {number} 32bit integer value
*/
hash() {
const data = this.#data;
// use some non-metadata bytes as a hash, favoring speed as this is expected to be used a lot
return data[3] // low byte of "time_low", very likely to differ between two UUIDs
| data[5] << 8 // low byte of "time_mid"
| data[9] << 16 // "clock_seq_low"
| data[15] << 24 // low byte of "node"
;
}
}
/**
* @readonly
*/
UUID.prototype.toJSON = UUID.prototype.toString;
/**
* @readonly
*/
UUID.prototype.fromJSON = UUID.prototype.parse;
/**
* @readonly
* @type {string}
*/
UUID.typeName = "UUID";
/**
* @readonly
* @type {boolean}
*/
UUID.prototype.isUUID = true;