'use strict'; /* * servers.js – Serverliste einer Instanz (servers.dat) * ----------------------------------------------------- * Minecraft legt die Liste der gespeicherten Server als unkomprimiertes NBT * unter /servers.dat ab. Aufbau: * * Compound "" Wurzel * List "servers" Liste von Compounds * String "name" Anzeigename * String "ip" Adresse (optional mit :Port) * String "icon" Base64-PNG des Serversymbols (optional) * Byte "acceptTextures" Ressourcenpakete annehmen (optional) * * Beim Schreiben wird die vorhandene Datei zuerst gesichert – eine kaputte * servers.dat würde alle gespeicherten Server kosten. */ const fs = require('fs'); const path = require('path'); const nbt = require('./nbt'); function datei(gameDir) { return path.join(gameDir, 'servers.dat'); } /* * Liest die Serverliste. * -> { ok, server: [{ name, ip, icon, acceptTextures }] } */ function lies(gameDir) { const p = datei(gameDir); if (!fs.existsSync(p)) return { ok: true, server: [], leer: true }; let roh; try { roh = fs.readFileSync(p); } catch (err) { return { ok: false, message: err.message }; } if (!roh.length) return { ok: true, server: [], leer: true }; try { const { wert } = nbt.lese(roh); const liste = Array.isArray(wert.servers) ? wert.servers : []; return { ok: true, server: liste.map((s) => ({ name: String(s.name || ''), ip: String(s.ip || ''), icon: typeof s.icon === 'string' ? s.icon : '', acceptTextures: Number(s.acceptTextures || 0), })), }; } catch (err) { return { ok: false, message: 'servers.dat konnte nicht gelesen werden: ' + err.message }; } } /* * Schreibt die Serverliste zurück. * Die alte Datei wird vorher als servers.dat.bak gesichert. */ function schreibe(gameDir, server) { const p = datei(gameDir); try { fs.mkdirSync(gameDir, { recursive: true }); const eintraege = (server || []).map((s) => { const c = { __typen: { name: nbt.TAG.STRING, ip: nbt.TAG.STRING }, name: String(s.name || ''), ip: String(s.ip || ''), }; // icon und acceptTextures nur mitschreiben, wenn vorhanden – // so wie Minecraft es selbst tut if (s.icon) { c.__typen.icon = nbt.TAG.STRING; c.icon = String(s.icon); } if (s.acceptTextures) { c.__typen.acceptTextures = nbt.TAG.BYTE; c.acceptTextures = Number(s.acceptTextures); } return c; }); const liste = eintraege; liste.__typ = nbt.TAG.COMPOUND; const wurzel = { __typen: { servers: nbt.TAG.LIST }, servers: liste }; const daten = nbt.schreibe('', wurzel); // erst sichern, dann über eine Zwischendatei ersetzen if (fs.existsSync(p)) { try { fs.copyFileSync(p, p + '.bak'); } catch { /* Sicherung ist Kür */ } } const tmp = p + '.neu'; fs.writeFileSync(tmp, daten); fs.renameSync(tmp, p); return { ok: true, anzahl: eintraege.length }; } catch (err) { return { ok: false, message: 'servers.dat konnte nicht geschrieben werden: ' + err.message }; } } // Adresse normalisieren und grob prüfen function pruefeAdresse(ip) { const s = String(ip || '').trim(); if (!s) return { ok: false, message: 'Bitte eine Serveradresse angeben.' }; if (/\s/.test(s)) return { ok: false, message: 'Die Adresse darf keine Leerzeichen enthalten.' }; const m = s.match(/^([^:]+)(?::(\d+))?$/); if (!m) return { ok: false, message: 'Die Adresse sieht nicht wie ein Server aus.' }; if (m[2] && (Number(m[2]) < 1 || Number(m[2]) > 65535)) { return { ok: false, message: 'Der Port muss zwischen 1 und 65535 liegen.' }; } return { ok: true, ip: s }; } function fuegeHinzu(gameDir, eintrag) { const geprueft = pruefeAdresse(eintrag && eintrag.ip); if (!geprueft.ok) return geprueft; const gelesen = lies(gameDir); if (!gelesen.ok) return gelesen; const liste = gelesen.server; // dieselbe Adresse nicht doppelt aufnehmen if (liste.some((s) => s.ip.toLowerCase() === geprueft.ip.toLowerCase())) { return { ok: false, message: 'Dieser Server steht bereits in der Liste.' }; } liste.push({ name: String((eintrag && eintrag.name) || geprueft.ip).trim() || geprueft.ip, ip: geprueft.ip, icon: '', acceptTextures: 0, }); return schreibe(gameDir, liste); } function entferne(gameDir, ip) { const gelesen = lies(gameDir); if (!gelesen.ok) return gelesen; const vorher = gelesen.server.length; const liste = gelesen.server.filter((s) => s.ip.toLowerCase() !== String(ip || '').toLowerCase()); if (liste.length === vorher) return { ok: false, message: 'Server nicht gefunden.' }; return schreibe(gameDir, liste); } /* * Nur den Anzeigenamen ändern – die Adresse bleibt gleich. * ip identifiziert den Eintrag (wie beim Entfernen). */ function benenneUm(gameDir, ip, neuerName) { const name = String(neuerName || '').trim(); if (!name) return { ok: false, message: 'Bitte einen Namen angeben.' }; const gelesen = lies(gameDir); if (!gelesen.ok) return gelesen; const liste = gelesen.server; const treffer = liste.find((s) => s.ip.toLowerCase() === String(ip || '').toLowerCase()); if (!treffer) return { ok: false, message: 'Server nicht gefunden.' }; if (treffer.name === name) return { ok: true, unveraendert: true }; treffer.name = name; return schreibe(gameDir, liste); } /* * Einen Server in eine andere Instanz übertragen – der eigentliche Zweck: * denselben Server nicht in jeder Instanz neu eintippen zu müssen. */ function uebertrage(zielGameDir, eintrag) { return fuegeHinzu(zielGameDir, eintrag); } module.exports = { lies, schreibe, fuegeHinzu, entferne, benenneUm, uebertrage, pruefeAdresse, datei };