Files
2026-10-03 07:09:06 +00:00

195 lines
9.4 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.Text;
using System.Web.Script.Serialization;
namespace AeroMCSetup;
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mr, mmc, technic</param>
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;
}
/// <summary>
/// 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.
/// </summary>
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<FoundLauncher> Detect()
{
var found = new List<FoundLauncher>();
void Add(Func<FoundLauncher?> 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<Dictionary<string, object>>(File.ReadAllText(file, Encoding.UTF8).TrimStart(''));
if (json?.TryGetValue("profiles", out var p) != true || p is not Dictionary<string, object> profiles) return null;
// wie isImportableProfile in src/mcimport.js: die automatischen "Neueste Version"-Profile zählen nicht
int count = profiles.Values.OfType<Dictionary<string, object>>().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<string, object> 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<string, string> MultiMcExes = new(StringComparer.OrdinalIgnoreCase)
{
["MultiMC.exe"] = "MultiMC",
["prismlauncher.exe"] = "Prism Launcher",
["polymc.exe"] = "PolyMC",
};
/// <summary>Ordner von MultiMC-artigen Launchern, auf die eine Verknüpfung (Desktop/Startmenü) zeigt.</summary>
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<string>(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 ----------
/// <summary>Merkt die ausgewählten Launcher für den ersten Start von AeroMC vor (oder räumt eine alte Vormerkung weg).</summary>
public static void WritePending(IReadOnlyCollection<FoundLauncher> selected)
{
if (selected.Count == 0)
{
try { if (File.Exists(PendingFile)) File.Delete(PendingFile); } catch { }
return;
}
var data = new Dictionary<string, object>
{
["sources"] = selected.Select(s => new Dictionary<string, string> { ["kind"] = s.Kind, ["path"] = s.Path }).ToList(),
};
Directory.CreateDirectory(Installer.DataDir);
File.WriteAllText(PendingFile, new JavaScriptSerializer().Serialize(data), new UTF8Encoding(false));
}
}