@xpgamesllc/ipcs-be
Version:
A set of standardized types and functions for interacting with other Minecraft Bedrock Edition add-ons.
572 lines (562 loc) • 17.6 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
on: () => on,
send: () => send,
setVersion: () => setVersion
});
module.exports = __toCommonJS(src_exports);
var import_server2 = require("@minecraft/server");
// src/v1/net.ts
var import_server = require("@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]);
import_server.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]);
import_server.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);
}
import_server2.system.afterEvents.scriptEventReceive.subscribe((evt) => {
Net.handleScriptEvent(evt.id, evt.message);
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
on,
send,
setVersion
});
//# sourceMappingURL=index.js.map