175 lines
6.8 KiB
JavaScript
175 lines
6.8 KiB
JavaScript
'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 };
|