diff --git a/src/main/paths.ts b/src/main/paths.ts index ad53fb5..b062f04 100644 --- a/src/main/paths.ts +++ b/src/main/paths.ts @@ -46,9 +46,11 @@ class LauncherPaths { return this.ensure(join(this.root, 'java')) } - /** Cache des tokens d'auth (prismarine-auth). */ + /** Cache des tokens d'auth (prismarine-auth). Toujours dans userData, + * indépendamment de dataDir — les tokens survivent ainsi aux changements + * de dossier de données et aux mises à jour du launcher. */ get authCache(): string { - return this.ensure(join(this.root, 'auth-cache')) + return this.ensure(join(app.getPath('userData'), 'auth-cache')) } /** Fichier de réglages utilisateur. */ diff --git a/src/main/play.ts b/src/main/play.ts index b2fa0ef..11fbec7 100644 --- a/src/main/play.ts +++ b/src/main/play.ts @@ -5,6 +5,7 @@ import { fetchPackMeta } from './modpack' import { ensureJava } from './java' import { installMinecraft, installNeoForge } from './install' import { syncModpack } from './modpack' +import { ensureServerInList } from './server-list' import { launchGame } from './launch' import { getSettings } from './settings' import { emit } from './events' @@ -42,6 +43,7 @@ export async function play(opts?: PlayOptions): Promise { await installMinecraft(meta.minecraft, repair) const versionId = await installNeoForge(meta.neoforge, meta.minecraft, javaPath, repair) await syncModpack(javaPath) + await ensureServerInList() const settings = await getSettings() const proc = await launchGame(versionId, auth, javaPath, settings) diff --git a/src/main/server-list.ts b/src/main/server-list.ts new file mode 100644 index 0000000..f918a28 --- /dev/null +++ b/src/main/server-list.ts @@ -0,0 +1,188 @@ +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 +}