using System.IO; using System.Text; using System.Web.Script.Serialization; namespace AeroMCSetup; /// Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann. /// Kennung wie im Import-Dialog des Launchers: mc, cf, mr, mmc, technic public sealed class FoundLauncher(string kind, string name, string path, int count, string unit) { public string Kind { get; } = kind; public string Name { get; } = name; public string Path { get; } = path; public int Count { get; } = count; public string Unit { get; } = unit; } /// /// Sucht andere Launcher an ihren üblichen Orten – nur lesend, es wird nichts verändert. /// Übernommen wird später im Launcher selbst (gleicher Import wie unter „Importieren“): /// das Setup legt dafür nur pending-import.json ab, AeroMC öffnet beim ersten Start /// den Import-Dialog damit fertig durchsucht. /// public static class LauncherImport { private static string AppData => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); private static string Home => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); private static string PendingFile => System.IO.Path.Combine(Installer.DataDir, "pending-import.json"); public static List Detect() { var found = new List(); void Add(Func probe) { try { if (probe() is { Count: > 0 } f) found.Add(f); } catch { /* unlesbar → einfach nicht anbieten */ } } Add(DetectMinecraftLauncher); Add(DetectCurseForge); Add(DetectModrinthApp); Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher"))); Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC"))); // MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden foreach (var (name, root) in PortableMultiMcFromShortcuts()) { if (found.Any(f => f.Kind == "mmc" && SamePath(f.Path, root))) continue; Add(() => DetectMultiMc(name, root)); } Add(DetectTechnic); return found; } // ---------- offizieller Minecraft Launcher ---------- private static readonly string[] ProfileFiles = ["launcher_profiles.json", "launcher_profiles_microsoft_store.json", "launcher_profiles_microsoft_store_2.json"]; private static FoundLauncher? DetectMinecraftLauncher() { var root = System.IO.Path.Combine(AppData, ".minecraft"); var file = ProfileFiles.Select(f => System.IO.Path.Combine(root, f)).FirstOrDefault(File.Exists); if (file is null) return null; var json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue } .Deserialize>(File.ReadAllText(file, Encoding.UTF8).TrimStart('')); if (json?.TryGetValue("profiles", out var p) != true || p is not Dictionary profiles) return null; // wie isImportableProfile in src/mcimport.js: die automatischen "Neueste Version"-Profile zählen nicht int count = profiles.Values.OfType>().Count(profile => { var type = (Get(profile, "type") ?? "").Trim().ToLowerInvariant(); if (type is "latest-release" or "latest-snapshot") return false; return !string.IsNullOrWhiteSpace(Get(profile, "name")) || !string.IsNullOrWhiteSpace(Get(profile, "lastVersionId")); }); return new FoundLauncher("mc", "Minecraft Launcher", root, count, count == 1 ? "Profil" : "Profile"); } private static string? Get(Dictionary d, string key) => d.TryGetValue(key, out var v) ? v as string : null; // ---------- CurseForge ---------- private static FoundLauncher? DetectCurseForge() { var dir = new[] { System.IO.Path.Combine(Home, "curseforge", "minecraft", "Instances"), System.IO.Path.Combine(Home, "CurseForge", "minecraft", "Instances"), }.FirstOrDefault(Directory.Exists); if (dir is null) return null; string[] markers = ["minecraftinstance.json", "instance.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"]; int count = Directory.GetDirectories(dir).Count(d => markers.Any(m => Exists(d, m))); return new FoundLauncher("cf", "CurseForge", dir, count, count == 1 ? "Instanz" : "Instanzen"); } // ---------- Modrinth App ---------- private static FoundLauncher? DetectModrinthApp() { // neuere Fassungen: ModrinthApp, sehr alte: com.modrinth.theseus (siehe src/mrimport.js) var root = new[] { System.IO.Path.Combine(AppData, "ModrinthApp"), System.IO.Path.Combine(AppData, "com.modrinth.theseus"), }.FirstOrDefault(d => Directory.Exists(System.IO.Path.Combine(d, "profiles"))); if (root is null) return null; string[] markers = ["profile.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"]; int count = Directory.GetDirectories(System.IO.Path.Combine(root, "profiles")).Count(d => markers.Any(m => Exists(d, m))); return new FoundLauncher("mr", "Modrinth App", root, count, count == 1 ? "Instanz" : "Instanzen"); } // ---------- Prism Launcher / PolyMC (MultiMC-Format) ---------- private static FoundLauncher? DetectMultiMc(string name, string root) { var instances = System.IO.Path.Combine(root, "instances"); if (!Directory.Exists(instances)) return null; int count = Directory.GetDirectories(instances).Count(d => File.Exists(System.IO.Path.Combine(d, "instance.cfg"))); return new FoundLauncher("mmc", name, root, count, count == 1 ? "Instanz" : "Instanzen"); } private static readonly Dictionary MultiMcExes = new(StringComparer.OrdinalIgnoreCase) { ["MultiMC.exe"] = "MultiMC", ["prismlauncher.exe"] = "Prism Launcher", ["polymc.exe"] = "PolyMC", }; /// Ordner von MultiMC-artigen Launchern, auf die eine Verknüpfung (Desktop/Startmenü) zeigt. private static IEnumerable<(string Name, string Root)> PortableMultiMcFromShortcuts() { var places = new[] { (Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), SearchOption.TopDirectoryOnly), (Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), SearchOption.TopDirectoryOnly), (Environment.GetFolderPath(Environment.SpecialFolder.Programs), SearchOption.AllDirectories), (Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), SearchOption.AllDirectories), }; var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var (dir, option) in places) { string[] links; try { links = string.IsNullOrEmpty(dir) || !Directory.Exists(dir) ? [] : Directory.GetFiles(dir, "*.lnk", option); } catch { continue; } foreach (var link in links) { var target = Shortcut.Target(link); if (string.IsNullOrEmpty(target) || !MultiMcExes.TryGetValue(System.IO.Path.GetFileName(target), out var name)) continue; var root = System.IO.Path.GetDirectoryName(target); if (root is null || !Directory.Exists(System.IO.Path.Combine(root, "instances")) || !seen.Add(root)) continue; yield return (name, root); } } } private static bool SamePath(string a, string b) => string.Equals(System.IO.Path.GetFullPath(a).TrimEnd('\\'), System.IO.Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); // ---------- Technic ---------- private static FoundLauncher? DetectTechnic() { var modpacks = System.IO.Path.Combine(AppData, ".technic", "modpacks"); if (!Directory.Exists(modpacks)) return null; string[] markers = ["bin", "mods", "config", "saves", "resourcepacks", "options.txt"]; int count = Directory.GetDirectories(modpacks).Count(d => markers.Any(m => Exists(d, m))); return new FoundLauncher("technic", "Technic Launcher", modpacks, count, count == 1 ? "Modpack" : "Modpacks"); } private static bool Exists(string dir, string name) { var p = System.IO.Path.Combine(dir, name); return File.Exists(p) || Directory.Exists(p); } // ---------- Übergabe an den Launcher ---------- /// Merkt die ausgewählten Launcher für den ersten Start von AeroMC vor (oder räumt eine alte Vormerkung weg). public static void WritePending(IReadOnlyCollection selected) { if (selected.Count == 0) { try { if (File.Exists(PendingFile)) File.Delete(PendingFile); } catch { } return; } var data = new Dictionary { ["sources"] = selected.Select(s => new Dictionary { ["kind"] = s.Kind, ["path"] = s.Path }).ToList(), }; Directory.CreateDirectory(Installer.DataDir); File.WriteAllText(PendingFile, new JavaScriptSerializer().Serialize(data), new UTF8Encoding(false)); } }