Skip to content

Commit

Permalink
Fix modpack installs
Browse files Browse the repository at this point in the history
  • Loading branch information
Windows10CE committed Mar 20, 2023
1 parent 0645d88 commit be10037
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 56 deletions.
33 changes: 15 additions & 18 deletions ThunderstoreCLI/Commands/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static async Task<int> 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)
{
Expand Down Expand Up @@ -77,11 +77,11 @@ private static async Task<int> 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<int> InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity)
private static async Task<int> 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!");
Expand All @@ -90,51 +90,48 @@ private static async Task<int> 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)
{
Write.Light($"Total estimated download size: {MiscUtils.GetSizeString(totalSize)}");
}

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
Expand Down
2 changes: 1 addition & 1 deletion ThunderstoreCLI/Commands/UninstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
4 changes: 3 additions & 1 deletion ThunderstoreCLI/Game/ModProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace ThunderstoreCLI.Game;

public record InstalledModVersion(string FullName, string VersionNumber, string[] Dependencies);

public class ModProfile : BaseJson<ModProfile>
{
public string Name { get; set; }
public string ProfileDirectory { get; set; }
public Dictionary<string, PackageManifestV1> InstalledModVersions { get; } = new();
public Dictionary<string, InstalledModVersion> InstalledModVersions { get; } = new();

#pragma warning disable CS8618
private ModProfile() { }
Expand Down
7 changes: 7 additions & 0 deletions ThunderstoreCLI/Models/PackageListingV1.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using ThunderstoreCLI.Commands;

namespace ThunderstoreCLI.Models;

Expand Down Expand Up @@ -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)
Expand Down
67 changes: 31 additions & 36 deletions ThunderstoreCLI/Utils/ModDependencyTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ThunderstoreCLI.Utils;

public static class ModDependencyTree
{
public static IEnumerable<PackageListingV1> Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity)
public static IEnumerable<PackageVersionV1> Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity, bool useExactVersions)
{
List<PackageListingV1>? packages = null;

Expand All @@ -32,67 +32,62 @@ public static IEnumerable<PackageListingV1> Generate(Config config, HttpClient h
packages = PackageListingV1.DeserializeList(packagesJson)!;
}

HashSet<string> visited = new();
foreach (var originalDep in root.Dependencies!)
Queue<string> toVisit = new();
Dictionary<string, (int id, PackageVersionV1 version)> 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<PackageListingV1> GenerateInner(List<PackageListingV1>? packages, Config config, HttpClient http, PackageListingV1 root, Predicate<PackageListingV1> 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;
}
}

0 comments on commit be10037

Please sign in to comment.