UNPKG

@xpgamesllc/ipcs-be

Version:

A set of standardized types and functions for interacting with other Minecraft Bedrock Edition add-ons.

545 lines (536 loc) 16.5 kB
// src/index.ts import { system as system2 } from "@minecraft/server"; // src/v1/net.ts import { system } from "@minecraft/server"; // src/v1/encoding/encoding.ts var MAX_CHUNK = 2047; var SPEC_VERSION = 1; // src/v1/envelope.ts var RS = ""; var ETX = ""; function buildEnvelope(meta, enc, chunk) { const metaPart = `${meta.version}|${meta.encoding}`; return `${metaPart}${RS}${enc}${ETX}${chunk}`; } function getEnvelopeLength(meta) { const metaPart = `${meta.version}|${meta.encoding}`; return `${metaPart}${RS}${ETX}`.length; } // src/utils/uuid.ts function makeUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } function uuidToBytes(uuid) { const hex = uuid.replace(/-/g, ""); if (hex.length !== 32) { throw new Error(`Invalid UUID: expected 36 chars with hyphens, got "${uuid}"`); } const bytes = new Uint8Array(16); for (let i = 0; i < 16; i++) { bytes[i] = parseInt(hex.substr(i * 2, 2), 16); } return bytes; } function bytesToUuid(bytes) { if (bytes.length !== 16) { throw new Error(`Invalid byte array: expected length 16, got ${bytes.length}`); } const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")); return hex.join(""); } // src/v1/encoding/TextPacketEncoder.ts var TextPacketEncoder = class { encoding = "text"; encode(payload) { return JSON.stringify(payload); } encodeHeader(header) { const parts = [header.id]; if (header.frag !== void 0 && header.last !== void 0) { parts.push(header.frag.toString(), (header.last ? 1 : 0).toString()); } return parts.join("|"); } decode(raw) { try { return JSON.parse(raw); } catch { return null; } } decodeHeader(raw) { const parts = raw.split("|"); const id = parts[0]; const header = { id }; if (parts.length === 3) { const frag = parseInt(parts[1], 10); const last = parseInt(parts[2], 10); if (isNaN(frag) || isNaN(last)) return null; header.frag = frag; header.last = last === 1; } else if (parts.length !== 1) { return null; } return header; } }; // src/utils/base64.ts var Base64 = class _Base64 { static _alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static _pad = "="; /* UTF-8 encode/decode helpers for ES2023-only environments */ static utf8Encode(str) { const bytes = []; for (const ch of str) { const cp = ch.codePointAt(0); if (cp <= 127) { bytes.push(cp); } else if (cp <= 2047) { bytes.push(192 | cp >> 6, 128 | cp & 63); } else if (cp <= 65535) { bytes.push( 224 | cp >> 12, 128 | cp >> 6 & 63, 128 | cp & 63 ); } else { bytes.push( 240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63 ); } } return new Uint8Array(bytes); } static utf8Decode(bytes) { let str = ""; for (let i = 0; i < bytes.length; ) { const b1 = bytes[i++]; if (b1 < 128) { str += String.fromCodePoint(b1); } else if (b1 < 224) { const b2 = bytes[i++]; str += String.fromCodePoint((b1 & 31) << 6 | b2 & 63); } else if (b1 < 240) { const b2 = bytes[i++], b3 = bytes[i++]; str += String.fromCodePoint( (b1 & 15) << 12 | (b2 & 63) << 6 | b3 & 63 ); } else { const b2 = bytes[i++], b3 = bytes[i++], b4 = bytes[i++]; str += String.fromCodePoint( (b1 & 7) << 18 | (b2 & 63) << 12 | (b3 & 63) << 6 | b4 & 63 ); } } return str; } /* ------------------------------------------------------------------ */ /* Low-level byte-wise codec */ /* ------------------------------------------------------------------ */ /** Encode raw bytes (Uint8Array) -> Base-64 text. */ static encodeBytes(bytes) { let out = ""; let i = 0; while (i < bytes.length) { const b1 = bytes[i++] ?? NaN; const b2 = bytes[i++] ?? NaN; const b3 = bytes[i++] ?? NaN; const enc1 = b1 >> 2; const enc2 = (b1 & 3) << 4 | b2 >> 4; const enc3 = isNaN(b2) ? 64 : (b2 & 15) << 2 | b3 >> 6; const enc4 = isNaN(b3) ? 64 : b3 & 63; out += _Base64._alphabet.charAt(enc1); out += _Base64._alphabet.charAt(enc2); out += enc3 === 64 ? _Base64._pad : _Base64._alphabet.charAt(enc3); out += enc4 === 64 ? _Base64._pad : _Base64._alphabet.charAt(enc4); } return out; } /** Decode Base-64 text -> raw bytes (Uint8Array). */ static decodeToBytes(base64) { base64 = base64.replace(/[^A-Za-z0-9+/=]/g, ""); const byteLen = base64.length / 4 * 3 - (base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0); const bytes = new Uint8Array(byteLen); let byteIdx = 0, i = 0; while (i < base64.length) { const enc1 = _Base64._alphabet.indexOf(base64.charAt(i++)); const enc2 = _Base64._alphabet.indexOf(base64.charAt(i++)); const enc3 = _Base64._alphabet.indexOf(base64.charAt(i++)); const enc4 = _Base64._alphabet.indexOf(base64.charAt(i++)); const b1 = enc1 << 2 | enc2 >> 4; const b2 = (enc2 & 15) << 4 | enc3 >> 2; const b3 = (enc3 & 3) << 6 | enc4; bytes[byteIdx++] = b1; if (enc3 !== 64 && byteIdx < byteLen) bytes[byteIdx++] = b2; if (enc4 !== 64 && byteIdx < byteLen) bytes[byteIdx++] = b3; } return bytes; } /* ------------------------------------------------------------------ */ /* Convenience wrappers for regular JS strings (UTF-8) */ /* ------------------------------------------------------------------ */ /** Encode a JS string to Base-64 using UTF-8. */ static encode(str) { const bytes = _Base64.utf8Encode(str); return _Base64.encodeBytes(bytes); } /** Decode Base-64 to a JS string using UTF-8. */ static decode(base64) { const bytes = _Base64.decodeToBytes(base64); return _Base64.utf8Decode(bytes); } /* ------------------------------------------------------------------ */ /* Helper factories */ /* ------------------------------------------------------------------ */ static createOutputStream() { return new Base64OutputStream(); } static createInputStream(encoded) { return new Base64InputStream(encoded); } }; var Base64OutputStream = class { _buf = []; get length() { return this._buf.length; } writeBytes(arr) { for (let i = 0; i < arr.length; i++) { this.writeByte(arr[i]); } } /* ---------- range-checked primitives ---------- */ writeByte(v) { if (!Number.isInteger(v) || v < 0 || v > 255) throw new RangeError("byte 0-255"); this._buf.push(v); } writeShort(v) { if (!Number.isInteger(v) || v < 0 || v > 65535) throw new RangeError("short 0-65535"); this.writeByte(v >>> 8 & 255); this.writeByte(v & 255); } writeInt(v) { if (!Number.isInteger(v) || v < 0 || v > 4294967295) throw new RangeError("int 0-4294967295"); this.writeByte(v >>> 24 & 255); this.writeByte(v >>> 16 & 255); this.writeByte(v >>> 8 & 255); this.writeByte(v & 255); } /* ---------- IEEE-754 ---------- */ writeFloat(v) { const dv = new DataView(new ArrayBuffer(4)); dv.setFloat32(0, v, false); for (let i = 0; i < 4; i++) this.writeByte(dv.getUint8(i)); } writeDouble(v) { const dv = new DataView(new ArrayBuffer(8)); dv.setFloat64(0, v, false); for (let i = 0; i < 8; i++) this.writeByte(dv.getUint8(i)); } /* ---------- strings ---------- */ writeUTF(str) { const bytes = Base64.utf8Encode(str); this.writeShort(bytes.length); for (const b of bytes) this.writeByte(b); } /** Finalize & return Base-64. */ toString() { return Base64.encodeBytes(Uint8Array.from(this._buf)); } }; var Base64InputStream = class { _bytes; _pos = 0; constructor(encoded) { this._bytes = Base64.decodeToBytes(encoded); } _need(n) { if (this._pos + n > this._bytes.length) throw new RangeError("EOF"); } readBytes(n) { const arr = new Uint8Array(n); for (let i = 0; i < n; i++) { arr[i] = this.readByte(); } return arr; } readByte() { this._need(1); return this._bytes[this._pos++]; } readShort() { return this.readByte() << 8 | this.readByte(); } readInt() { return this.readByte() << 24 | this.readByte() << 16 | this.readByte() << 8 | this.readByte(); } readFloat() { this._need(4); const dv = new DataView(this._bytes.buffer, this._bytes.byteOffset + this._pos, 4); const v = dv.getFloat32(0, false); this._pos += 4; return v; } readDouble() { this._need(8); const dv = new DataView(this._bytes.buffer, this._bytes.byteOffset + this._pos, 8); const v = dv.getFloat64(0, false); this._pos += 8; return v; } readUTF() { const len = this.readShort(); this._need(len); const slice = this._bytes.subarray(this._pos, this._pos + len); this._pos += len; return Base64.utf8Decode(slice); } get remaining() { return this._bytes.length - this._pos; } }; // src/v1/encoding/Base64PacketEncoder.ts var Base64PacketEncoder = class { // encoding name for Base64 transport encoding = "b64"; encode(payload) { const json = JSON.stringify(payload); return Base64.encode(json); } encodeHeader(header) { const { id, frag, last } = header; const out = new Base64OutputStream(); out.writeBytes(uuidToBytes(id)); let flags = 0; if (last !== void 0) flags |= 1; out.writeByte(flags); if (flags & 1) { out.writeShort(frag); out.writeByte(last ? 1 : 0); } return out.toString(); } decode(raw) { try { const json = Base64.decode(raw); return JSON.parse(json); } catch { return null; } } decodeHeader(raw) { try { const inp = new Base64InputStream(raw); const bytes = inp.readBytes(16); const id = bytesToUuid(bytes); const flags = inp.readByte(); const header = { id }; if (flags & 1) { header.frag = inp.readShort(); header.last = inp.readByte() === 1; } return header; } catch { return null; } } }; // src/v1/net.ts var Net = class _Net { static listeners = /* @__PURE__ */ new Map(); static buffers = /* @__PURE__ */ new Map(); static encoders = /* @__PURE__ */ new Map(); static DefaultEncoding = "text"; /** * Register a new encoder implementation by its unique name. */ static registerEncoder(encoder) { this.encoders.set(encoder.encoding, encoder); } /** fire a script event, fragmenting JSON/text payload if needed. Optionally receive a response. */ static send(channel, payload, opts, callback) { const encodingName = opts?.encoding ?? _Net.DefaultEncoding; const encoder = _Net.encoders.get(encodingName); if (!encoder) throw new Error(`Unknown encoder: ${encodingName}`); const raw = encoder.encode(payload); const id = (opts?.id ?? makeUUID()).replace(/-/g, ""); if (callback) { const respChannel = `ipcs:${id}`; const wrapper = (respPayload) => { try { callback(respPayload); } finally { _Net.off(respChannel, wrapper); } }; _Net.on(respChannel, wrapper); } const totalLen = raw.length; const metaPart = `${SPEC_VERSION}|${encodingName}`; let chunks = []; const meta = { version: SPEC_VERSION, encoding: encodingName }; const metaLength = getEnvelopeLength(meta); const noFragHdr = encoder.encodeHeader({ id }); const noFragHdrLen = noFragHdr.length; const noFragOverhead = metaPart.length + RS.length + ETX.length + noFragHdrLen; if (totalLen + noFragOverhead + metaLength <= MAX_CHUNK) { chunks = [[noFragHdr, raw]]; } else { let i = 0; let remaining = totalLen; let written = 0; while (remaining > 0) { const hdr = encoder.encodeHeader({ id, frag: i, last: 0 }); const hdrLen = hdr.length; const chunkSize = Math.min(MAX_CHUNK - hdrLen - metaLength, remaining); chunks.push([hdr, raw.slice(written, written + chunkSize)]); remaining -= chunkSize; written += chunkSize; i++; } } if (chunks.length === 1) { const hdrString = chunks[0][0]; const envelope = buildEnvelope(meta, hdrString, chunks[0][1]); system.sendScriptEvent(channel, envelope); } else { for (let i = 0; i < chunks.length; i++) { const encHdr = { id, frag: i, last: i === chunks.length - 1 }; const hdrString = encHdr.last ? encoder.encodeHeader(encHdr) : chunks[i][0]; const envelope = buildEnvelope(meta, hdrString, chunks[i][1]); system.sendScriptEvent(channel, envelope); } } } /** subscribe to reassembled messages on a channel */ static on(channel, cb) { const arr = _Net.listeners.get(channel) ?? []; arr.push(cb); _Net.listeners.set(channel, arr); } /** unsubscribe a callback from a channel */ static off(channel, cb) { const arr = this.listeners.get(channel); if (!arr) return; const filtered = arr.filter((fn) => fn !== cb); if (filtered.length) this.listeners.set(channel, filtered); else this.listeners.delete(channel); } /** invoke all cbs registered on this channel, providing optional respond function */ static dispatch(channel, payload, id, encodingName) { const arr = _Net.listeners.get(channel); if (!arr) return; for (const cb of arr) { try { if (id && !channel.startsWith("ipcs:")) { const respond = (responsePayload) => { _Net.send(`ipcs:${id}`, responsePayload, { encoding: encodingName, id }); }; cb(payload, respond); } else { cb(payload); } } catch { } } } /** internal dispatcher: parse envelope, reassemble if needed, then fire user-cb */ static handleScriptEvent(channel, raw) { const [metaPart, rest] = raw.split(RS, 2); if (!rest) return; const [encPart, body] = rest.split(ETX, 2); if (body === void 0) return; const [verStr, encodingName] = metaPart.split("|"); if (parseInt(verStr, 10) !== SPEC_VERSION) return; const encoder = _Net.encoders.get(encodingName); if (!encoder) return; const header = encoder.decodeHeader(encPart); if (!header) return; const { id, frag, last } = header; if (frag === void 0 || last === void 0) { const payload = encoder.decode(body); if (payload !== null) _Net.dispatch(channel, payload, id, encodingName); return; } let buf = _Net.buffers.get(id); if (!buf) { buf = { parts: [] }; _Net.buffers.set(id, buf); } if (frag !== void 0 && buf.parts[frag] === void 0) { buf.parts[frag] = { body, frag: header.frag, last: header.last }; if (header.last) { buf.lastIndex = frag; } } if (buf.lastIndex !== void 0 && buf.parts.length === buf.lastIndex + 1 && buf.parts.sort((a, b) => a.frag - b.frag)[buf.lastIndex].frag === buf.lastIndex) { const full = buf.parts.map((p) => p.body).join(""); _Net.buffers.delete(id); const payload = encoder.decode(full); if (payload !== null) _Net.dispatch(channel, payload, id, encodingName); } } }; Net.registerEncoder(new TextPacketEncoder()); Net.registerEncoder(new Base64PacketEncoder()); // src/index.ts var version = 1; var MaxVersion = 1; var MinVersion = 1; function setVersion(v) { if (version < MinVersion || version > MaxVersion) { throw new Error(`IPCS-BE spec version ${version} is not supported`); } version = v; } function send(channel, payload, opts, callback) { if (version === 1) { Net.send(channel, payload, opts, callback); } } function on(channel, cb) { Net.on(channel, cb); } system2.afterEvents.scriptEventReceive.subscribe((evt) => { Net.handleScriptEvent(evt.id, evt.message); }); export { on, send, setVersion }; //# sourceMappingURL=index.mjs.map