'use strict'; /* * nbt.js – minimaler NBT-Leser und -Schreiber * -------------------------------------------- * Nur so viel, wie servers.dat braucht. Diese Datei ist unkomprimiertes NBT * mit einem Wurzel-Compound, darin eine Liste "servers" aus Compounds mit * name, ip, icon und acceptTextures. * * Bewusst kein Fremdpaket: Der benötigte Ausschnitt ist klein und gut * prüfbar, eine Abhängigkeit für zwei Datentypen wäre unverhältnismäßig. * * Alle Zahlen liegen in NBT als Big-Endian vor, Zeichenketten als * modifiziertes UTF-8 mit vorangestellter Länge (2 Byte). */ const TAG = { END: 0, BYTE: 1, SHORT: 2, INT: 3, LONG: 4, FLOAT: 5, DOUBLE: 6, BYTE_ARRAY: 7, STRING: 8, LIST: 9, COMPOUND: 10, INT_ARRAY: 11, LONG_ARRAY: 12, }; // --------------------------------------------------------------------------- // Lesen // --------------------------------------------------------------------------- class Leser { constructor(buf) { this.buf = buf; this.pos = 0; } pruefe(n) { if (this.pos + n > this.buf.length) throw new Error('NBT endet unerwartet'); } byte() { this.pruefe(1); return this.buf.readInt8(this.pos++); } ubyte() { this.pruefe(1); return this.buf.readUInt8(this.pos++); } short() { this.pruefe(2); const v = this.buf.readInt16BE(this.pos); this.pos += 2; return v; } ushort() { this.pruefe(2); const v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; } int() { this.pruefe(4); const v = this.buf.readInt32BE(this.pos); this.pos += 4; return v; } long() { this.pruefe(8); const v = this.buf.readBigInt64BE(this.pos); this.pos += 8; return v; } float() { this.pruefe(4); const v = this.buf.readFloatBE(this.pos); this.pos += 4; return v; } double() { this.pruefe(8); const v = this.buf.readDoubleBE(this.pos); this.pos += 8; return v; } text() { const laenge = this.ushort(); this.pruefe(laenge); const s = this.buf.toString('utf8', this.pos, this.pos + laenge); this.pos += laenge; return s; } bytes(n) { this.pruefe(n); const b = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return Buffer.from(b); } } function leseWert(l, typ) { switch (typ) { case TAG.BYTE: return l.byte(); case TAG.SHORT: return l.short(); case TAG.INT: return l.int(); case TAG.LONG: return l.long(); case TAG.FLOAT: return l.float(); case TAG.DOUBLE: return l.double(); case TAG.STRING: return l.text(); case TAG.BYTE_ARRAY: return l.bytes(l.int()); case TAG.INT_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.int()); return a; } case TAG.LONG_ARRAY: { const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(l.long()); return a; } case TAG.LIST: { const innerTyp = l.ubyte(); const n = l.int(); const a = []; for (let i = 0; i < n; i++) a.push(leseWert(l, innerTyp)); a.__typ = innerTyp; // Typ merken, damit Schreiben ihn wiederherstellt return a; } case TAG.COMPOUND: { const o = {}; o.__typen = {}; for (;;) { const t = l.ubyte(); if (t === TAG.END) break; const name = l.text(); o.__typen[name] = t; o[name] = leseWert(l, t); } return o; } default: throw new Error('Unbekannter NBT-Typ: ' + typ); } } /* * Liest unkomprimiertes NBT. * -> { name, wert } */ function lese(buf) { const l = new Leser(buf); const typ = l.ubyte(); if (typ !== TAG.COMPOUND) throw new Error('NBT beginnt nicht mit einem Compound'); const name = l.text(); return { name, wert: leseWert(l, TAG.COMPOUND) }; } // --------------------------------------------------------------------------- // Schreiben // --------------------------------------------------------------------------- class Schreiber { constructor() { this.teile = []; } byte(v) { const b = Buffer.alloc(1); b.writeInt8(v, 0); this.teile.push(b); } ubyte(v) { const b = Buffer.alloc(1); b.writeUInt8(v, 0); this.teile.push(b); } short(v) { const b = Buffer.alloc(2); b.writeInt16BE(v, 0); this.teile.push(b); } int(v) { const b = Buffer.alloc(4); b.writeInt32BE(v, 0); this.teile.push(b); } long(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v), 0); this.teile.push(b); } float(v) { const b = Buffer.alloc(4); b.writeFloatBE(v, 0); this.teile.push(b); } double(v) { const b = Buffer.alloc(8); b.writeDoubleBE(v, 0); this.teile.push(b); } text(s) { const d = Buffer.from(String(s), 'utf8'); const l = Buffer.alloc(2); l.writeUInt16BE(d.length, 0); this.teile.push(l, d); } roh(b) { this.teile.push(b); } fertig() { return Buffer.concat(this.teile); } } // Typ raten, wenn er nicht aus dem Einlesen bekannt ist function typVon(wert) { if (typeof wert === 'string') return TAG.STRING; if (typeof wert === 'bigint') return TAG.LONG; if (typeof wert === 'boolean') return TAG.BYTE; if (Buffer.isBuffer(wert)) return TAG.BYTE_ARRAY; if (Array.isArray(wert)) return TAG.LIST; if (wert && typeof wert === 'object') return TAG.COMPOUND; if (Number.isInteger(wert)) return TAG.INT; return TAG.DOUBLE; } function schreibeWert(s, typ, wert) { switch (typ) { case TAG.BYTE: s.byte(wert ? Number(wert) : 0); break; case TAG.SHORT: s.short(Number(wert) || 0); break; case TAG.INT: s.int(Number(wert) || 0); break; case TAG.LONG: s.long(wert || 0); break; case TAG.FLOAT: s.float(Number(wert) || 0); break; case TAG.DOUBLE: s.double(Number(wert) || 0); break; case TAG.STRING: s.text(wert == null ? '' : wert); break; case TAG.BYTE_ARRAY: { const b = Buffer.isBuffer(wert) ? wert : Buffer.alloc(0); s.int(b.length); s.roh(b); break; } case TAG.INT_ARRAY: { s.int(wert.length); for (const v of wert) s.int(v); break; } case TAG.LONG_ARRAY: { s.int(wert.length); for (const v of wert) s.long(v); break; } case TAG.LIST: { const liste = wert || []; // leere Listen bekommen END als Typ – so macht es Minecraft auch const innerTyp = liste.__typ || (liste.length ? typVon(liste[0]) : TAG.END); s.ubyte(innerTyp); s.int(liste.length); for (const v of liste) schreibeWert(s, innerTyp, v); break; } case TAG.COMPOUND: { const typen = (wert && wert.__typen) || {}; for (const schluessel of Object.keys(wert || {})) { if (schluessel === '__typen' || schluessel === '__typ') continue; const t = typen[schluessel] || typVon(wert[schluessel]); s.ubyte(t); s.text(schluessel); schreibeWert(s, t, wert[schluessel]); } s.ubyte(TAG.END); break; } default: throw new Error('Unbekannter NBT-Typ beim Schreiben: ' + typ); } } function schreibe(name, wert) { const s = new Schreiber(); s.ubyte(TAG.COMPOUND); s.text(name || ''); schreibeWert(s, TAG.COMPOUND, wert); return s.fertig(); } module.exports = { lese, schreibe, TAG };