177 lines
8.4 KiB
C#
177 lines
8.4 KiB
C#
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));
|
||
}
|
||
}
|