Files
OFLauncher/src/main/server-list.ts
T
2026-06-19 11:04:12 +02:00

189 lines
5.3 KiB
TypeScript

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<void> {
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<ServerEntry> = {}
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
}