From be10037b59f74134aa647bf90de824a8222a3871 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 28 Feb 2023 21:00:28 -0600 Subject: [PATCH] Fix modpack installs --- ThunderstoreCLI/Commands/InstallCommand.cs | 33 +++++----- ThunderstoreCLI/Commands/UninstallCommand.cs | 2 +- ThunderstoreCLI/Game/ModProfile.cs | 4 +- ThunderstoreCLI/Models/PackageListingV1.cs | 7 ++ ThunderstoreCLI/Utils/ModDependencyTree.cs | 67 +++++++++----------- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 7d688d2..1d7bb28 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -37,7 +37,7 @@ public static async Task Run(Config config) Match packageMatch = FullPackageNameRegex.Match(package); if (File.Exists(package)) { - returnCode = await InstallZip(config, http, def, profile, package, null, null); + returnCode = await InstallZip(config, http, def, profile, package, null, null, false); } else if (packageMatch.Success) { @@ -77,11 +77,11 @@ private static async Task InstallFromRepository(Config config, HttpClient h versionData ??= packageData!.LatestVersion!; var zipPath = await config.Cache.GetFileOrDownload($"{versionData.FullName}.zip", versionData.DownloadUrl!); - var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!, packageData!.CommunityListings!.First().Community); + var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!, packageData!.CommunityListings!.First().Community, packageData.CommunityListings!.First().Categories!.Contains("Modpacks")); return returnCode; } - private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity) + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity, bool isModpack) { using var zip = ZipFile.OpenRead(zipPath); var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); @@ -90,15 +90,15 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe manifest.Namespace ??= backupNamespace; - var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest, sourceCommunity) - .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.Fullname!)) + var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest, sourceCommunity, isModpack) + .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.FullNameParts["fullname"].Value)) .ToArray(); if (dependenciesToInstall.Length > 0) { var totalSize = dependenciesToInstall - .Where(d => !config.Cache.ContainsFile($"{d.Fullname}-{d.Versions![0].VersionNumber}.zip")) - .Select(d => d.Versions![0].FileSize) + .Where(d => !config.Cache.ContainsFile($"{d.FullName}-{d.VersionNumber}.zip")) + .Select(d => d.FileSize) .Sum(); if (totalSize != 0) { @@ -106,35 +106,32 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe } var downloadTasks = dependenciesToInstall.Select(mod => - { - var version = mod.Versions![0]; - return config.Cache.GetFileOrDownload($"{mod.Fullname}-{version.VersionNumber}.zip", version.DownloadUrl!); - }).ToArray(); + config.Cache.GetFileOrDownload($"{mod.FullName}-{mod.VersionNumber}.zip", mod.DownloadUrl!) + ).ToArray(); var spinner = new ProgressSpinner("dependencies downloaded", downloadTasks); await spinner.Spin(); - foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) + foreach (var (tempZipPath, pVersion) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) { - var packageVersion = package.Versions![0]; - int returnCode = RunInstaller(game, profile, tempZipPath, package.Owner); + int returnCode = RunInstaller(game, profile, tempZipPath, pVersion.FullNameParts["namespace"].Value); if (returnCode == 0) { - Write.Success($"Installed mod: {package.Fullname}-{packageVersion.VersionNumber}"); + Write.Success($"Installed mod: {pVersion.FullName}"); } else { - Write.Error($"Failed to install mod: {package.Fullname}-{packageVersion.VersionNumber}"); + Write.Error($"Failed to install mod: {pVersion.FullName}"); return returnCode; } - profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package, packageVersion); + profile.InstalledModVersions[pVersion.FullNameParts["fullname"].Value] = new InstalledModVersion(pVersion.FullNameParts["fullname"].Value, pVersion.VersionNumber!, pVersion.Dependencies!); } } var exitCode = RunInstaller(game, profile, zipPath, backupNamespace); if (exitCode == 0) { - profile.InstalledModVersions[manifest.FullName] = manifest; + profile.InstalledModVersions[manifest.FullName] = new InstalledModVersion(manifest.FullName, manifest.VersionNumber!, manifest.Dependencies!); Write.Success($"Installed mod: {manifest.FullName}-{manifest.VersionNumber}"); } else diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index ed57c79..c510fc5 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -35,7 +35,7 @@ public static int Run(Config config) var searchWithDash = search + '-'; foreach (var mod in profile.InstalledModVersions.Values) { - if (mod.Dependencies!.Any(s => s.StartsWith(searchWithDash))) + if (mod.Dependencies.Any(s => s.StartsWith(searchWithDash))) { if (modsToRemove.Add(mod.FullName)) { diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index 89f5869..e842c10 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -4,11 +4,13 @@ namespace ThunderstoreCLI.Game; +public record InstalledModVersion(string FullName, string VersionNumber, string[] Dependencies); + public class ModProfile : BaseJson { public string Name { get; set; } public string ProfileDirectory { get; set; } - public Dictionary InstalledModVersions { get; } = new(); + public Dictionary InstalledModVersions { get; } = new(); #pragma warning disable CS8618 private ModProfile() { } diff --git a/ThunderstoreCLI/Models/PackageListingV1.cs b/ThunderstoreCLI/Models/PackageListingV1.cs index 4a6743d..0f07ac4 100644 --- a/ThunderstoreCLI/Models/PackageListingV1.cs +++ b/ThunderstoreCLI/Models/PackageListingV1.cs @@ -1,4 +1,6 @@ +using System.Text.RegularExpressions; using Newtonsoft.Json; +using ThunderstoreCLI.Commands; namespace ThunderstoreCLI.Models; @@ -104,6 +106,11 @@ public class PackageVersionV1 [JsonProperty("file_size")] public int FileSize { get; set; } + [JsonIgnore] + private GroupCollection? _fullNameParts; + [JsonIgnore] + public GroupCollection FullNameParts => _fullNameParts ??= InstallCommand.FullPackageNameRegex.Match(FullName!).Groups; + public PackageVersionV1() { } public PackageVersionV1(PackageVersionData version) diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index 8d421f8..3ecde32 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -8,7 +8,7 @@ namespace ThunderstoreCLI.Utils; public static class ModDependencyTree { - public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity) + public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity, bool useExactVersions) { List? packages = null; @@ -32,67 +32,62 @@ public static IEnumerable Generate(Config config, HttpClient h packages = PackageListingV1.DeserializeList(packagesJson)!; } - HashSet visited = new(); - foreach (var originalDep in root.Dependencies!) + Queue toVisit = new(); + Dictionary dict = new(); + int currentId = 0; + foreach (var dep in root.Dependencies!) { - var match = InstallCommand.FullPackageNameRegex.Match(originalDep); + toVisit.Enqueue(dep); + } + while (toVisit.TryDequeue(out var packageString)) + { + var match = InstallCommand.FullPackageNameRegex.Match(packageString); var fullname = match.Groups["fullname"].Value; - var depPackage = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); - if (depPackage == null) + if (dict.TryGetValue(fullname, out var current)) { + dict[fullname] = (currentId++, current.version); continue; } - foreach (var dependency in GenerateInner(packages, config, http, depPackage, p => visited.Contains(p.Fullname!))) + var package = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match); + if (package is null) + continue; + PackageVersionV1? version; + if (useExactVersions) { - // can happen on cycles, oh well - if (visited.Contains(dependency.Fullname!)) + string requiredVersion = match.Groups["version"].Value; + version = package.Versions!.FirstOrDefault(v => v.VersionNumber == requiredVersion); + if (version is null) { - continue; + Write.Warn($"Version {requiredVersion} could not be found for mod {fullname}, using latest instead"); + version = package.Versions!.First(); } - visited.Add(dependency.Fullname!); - yield return dependency; } - } - } - - private static IEnumerable GenerateInner(List? packages, Config config, HttpClient http, PackageListingV1 root, Predicate visited) - { - if (visited(root)) - { - yield break; - } - - foreach (var dependency in root.Versions!.First().Dependencies!) - { - var match = InstallCommand.FullPackageNameRegex.Match(dependency); - var fullname = match.Groups["fullname"].Value; - var package = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); - if (package == null) + else { - continue; + version = package.Versions!.First(); } - foreach (var innerPackage in GenerateInner(packages, config, http, package, visited)) + dict[fullname] = (currentId++, version); + foreach (var dep in version.Dependencies!) { - yield return innerPackage; + toVisit.Enqueue(dep); } } - - yield return root; + return dict.Values.OrderByDescending(x => x.id).Select(x => x.version); } - private static PackageListingV1? AttemptResolveExperimental(Config config, HttpClient http, Match nameMatch, string neededBy) + private static PackageListingV1? AttemptResolveExperimental(Config config, HttpClient http, Match nameMatch) { var response = http.Send(config.Api.GetPackageMetadata(nameMatch.Groups["namespace"].Value, nameMatch.Groups["name"].Value)); if (response.StatusCode == HttpStatusCode.NotFound) { - Write.Warn($"Failed to resolve dependency {nameMatch.Groups["fullname"].Value} for {neededBy}, continuing without it."); + Write.Warn($"Failed to resolve dependency {nameMatch.Groups["fullname"].Value}, continuing without it."); return null; } response.EnsureSuccessStatusCode(); using var reader = new StreamReader(response.Content.ReadAsStream()); var data = PackageData.Deserialize(reader.ReadToEnd()); - Write.Warn($"Package {data!.Fullname} (needed by {neededBy}) exists in different community, ignoring"); + Write.Warn($"Package {data!.Fullname} exists in different community, ignoring"); return null; } }