using System; using System.IO; using System.Linq; using fNbt; namespace ModpackLauncher.Services; /// /// Pre-populates Minecraft's multiplayer server list (`servers.dat`) so the /// modpack's server is one click away on first launch. /// /// servers.dat is uncompressed NBT (unlike level.dat which is gzipped). Schema: /// compound { /// servers : list[compound] { /// name : string /// ip : string /// acceptTextures : byte (optional, 1 = enabled) /// hidden : byte (optional, 0 = visible) /// icon : string (optional, base64 PNG) /// } /// } /// public sealed class ServerListService { /// /// Add or update an entry. Match-by-IP: if an entry with the same IP exists, /// update its name; otherwise prepend a new entry so the friend's first /// glance at multiplayer shows our server at the top. /// public void EnsureServer(string gameDir, string name, string ip) { if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(ip)) return; Directory.CreateDirectory(gameDir); var path = Path.Combine(gameDir, "servers.dat"); NbtCompound root; NbtList servers; if (File.Exists(path)) { try { var nbt = new NbtFile(); nbt.LoadFromFile(path, NbtCompression.None, _ => true); root = nbt.RootTag; if (root.TryGet("servers", out NbtList? existingList) && existingList is not null) { servers = existingList; } else { servers = new NbtList("servers", NbtTagType.Compound); root.Add(servers); } } catch { // Existing file unreadable -- start fresh rather than crashing the install. root = new NbtCompound(""); servers = new NbtList("servers", NbtTagType.Compound); root.Add(servers); } } else { root = new NbtCompound(""); servers = new NbtList("servers", NbtTagType.Compound); root.Add(servers); } // Match-by-IP. If found, update the display name; otherwise prepend. for (int i = 0; i < servers.Count; i++) { if (servers[i] is not NbtCompound entry) continue; if (entry.TryGet("ip", out NbtString? ipTag) && ipTag?.Value == ip) { entry["name"] = new NbtString("name", name); Save(root, path); return; } } var newEntry = new NbtCompound { new NbtString("name", name), new NbtString("ip", ip), // acceptTextures = 1 lets the server send its resource pack without prompting // (the player still gets the prompt; this just allows the option). Default // value matches what vanilla MC writes when you click "Done" in the UI. new NbtByte("acceptTextures", 1), }; servers.Insert(0, newEntry); Save(root, path); } private static void Save(NbtCompound root, string path) { var file = new NbtFile(root); file.SaveToFile(path, NbtCompression.None); } }