import { readFile, writeFile } from 'fs/promises' import { join } from 'path' import { paths } from './paths' import { config } from '../shared/config' /** * Assure que le serveur configuré est présent dans servers.dat de l'instance. * Si le fichier n'existe pas, il est créé. Si le serveur est déjà listé (même * IP), rien n'est modifié pour ne pas écraser les préférences utilisateur. * * servers.dat : NBT non compressé (contrairement à level.dat). * Structure : TAG_Compound root -> TAG_List "servers" -> TAG_Compound[] serveurs. */ export async function ensureServerInList(): Promise { if (!config.serverAddress?.trim()) return const ip = config.serverAddress.trim() const serversDat = join(paths.instanceDir, 'servers.dat') let servers: ServerEntry[] = [] try { const buf = await readFile(serversDat) servers = decodeServersDat(buf) } catch { // Fichier absent ou illisible : on part d'une liste vide. } if (servers.some((s) => s.ip === ip)) return // Notre serveur en tête de liste. servers = [{ name: config.appName, ip }, ...servers] await writeFile(serversDat, encodeServersDat(servers)) } // --------------------------------------------------------------------------- // Types internes // --------------------------------------------------------------------------- interface ServerEntry { name: string ip: string } // --------------------------------------------------------------------------- // Encodeur NBT minimal // --------------------------------------------------------------------------- function b1(n: number): Buffer { return Buffer.from([n]) } function b2(n: number): Buffer { const b = Buffer.allocUnsafe(2) b.writeUInt16BE(n) return b } function b4(n: number): Buffer { const b = Buffer.allocUnsafe(4) b.writeInt32BE(n) return b } function encStr(s: string): Buffer { const d = Buffer.from(s, 'utf8') return Buffer.concat([b2(d.length), d]) } function encTag(type: number, name: string, payload: Buffer): Buffer { return Buffer.concat([b1(type), encStr(name), payload]) } function encodeServersDat(servers: ServerEntry[]): Buffer { const entries = servers.flatMap((s) => [ encTag(8, 'name', encStr(s.name)), encTag(8, 'ip', encStr(s.ip)), encTag(1, 'acceptTextures', b1(1)), b1(0) // TAG_End clôture le compound dans la liste ]) const listPayload = Buffer.concat([b1(10), b4(servers.length), ...entries]) const rootPayload = Buffer.concat([encTag(9, 'servers', listPayload), b1(0)]) return Buffer.concat([b1(10), encStr(''), rootPayload]) } // --------------------------------------------------------------------------- // Décodeur NBT minimal // --------------------------------------------------------------------------- interface Reader { buf: Buffer pos: number } function ru8(r: Reader): number { return r.buf[r.pos++] } function ru16(r: Reader): number { const v = r.buf.readUInt16BE(r.pos) r.pos += 2 return v } function ri32(r: Reader): number { const v = r.buf.readInt32BE(r.pos) r.pos += 4 return v } function rstr(r: Reader): string { const len = ru16(r) const s = r.buf.subarray(r.pos, r.pos + len).toString('utf8') r.pos += len return s } function skipPayload(r: Reader, type: number): void { switch (type) { case 1: r.pos++; return // TAG_Byte case 2: r.pos += 2; return // TAG_Short case 3: r.pos += 4; return // TAG_Int case 4: r.pos += 8; return // TAG_Long case 5: r.pos += 4; return // TAG_Float case 6: r.pos += 8; return // TAG_Double case 7: r.pos += ri32(r); return // TAG_Byte_Array case 8: r.pos += ru16(r); return // TAG_String case 9: { // TAG_List const et = ru8(r) const n = ri32(r) for (let i = 0; i < n; i++) skipPayload(r, et) return } case 10: { // TAG_Compound while (r.pos < r.buf.length) { const t = ru8(r) if (t === 0) return r.pos += ru16(r) // skip name skipPayload(r, t) } return } case 11: r.pos += ri32(r) * 4; return // TAG_Int_Array case 12: r.pos += ri32(r) * 8; return // TAG_Long_Array } } function decodeServersDat(buf: Buffer): ServerEntry[] { const r: Reader = { buf, pos: 0 } if (ru8(r) !== 10) return [] // root doit être TAG_Compound rstr(r) // nom de la racine (vide) const servers: ServerEntry[] = [] while (r.pos < buf.length) { const type = ru8(r) if (type === 0) break const key = rstr(r) if (type === 9 && key === 'servers') { const elemType = ru8(r) const count = ri32(r) if (elemType !== 10) { // Type inattendu : skip for (let i = 0; i < count; i++) skipPayload(r, elemType) continue } for (let i = 0; i < count; i++) { const entry: Partial = {} while (r.pos < buf.length) { const t = ru8(r) if (t === 0) break const k = rstr(r) if (t === 8) { const val = rstr(r) if (k === 'name') entry.name = val else if (k === 'ip') entry.ip = val } else { skipPayload(r, t) } } if (entry.ip) servers.push({ name: entry.name ?? '', ip: entry.ip }) } } else { skipPayload(r, type) } } return servers }