Upload via GUI (123 Dateien)

This commit is contained in:
2026-10-03 07:09:06 +00:00
parent 41c63ef0ed
commit bc68395437
29 changed files with 1255 additions and 197 deletions
+194 -176
View File
@@ -1,176 +1,194 @@
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, 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(() => 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");
}
// ---------- 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));
}
}
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));
}
}