Skip to content

Commit

Permalink
Add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
halgari committed Oct 3, 2024
1 parent 908f9fd commit 42bd0fb
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace NexusMods.Games.FOMOD.CoreDelegates;

/// <summary>
/// A IGuidedInstaller implementation that uses a preset list of steps to make the same choices
/// a user previously made for specific steps.
/// </summary>
public class PresetGuidedInstaller : IGuidedInstaller
{
private readonly FomodOption[] _steps;
Expand Down
3 changes: 3 additions & 0 deletions src/Games/NexusMods.Games.FOMOD/FomodXmlInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public async ValueTask<InstallerResult> ExecuteAsync(
{
if (options is not null)
{
// NOTE(halgari) The support for passing in presets to the installer is utterly broken. So we're going to
// something different: we will create a new guided installer that will simply emit the user choices based on the
// provided options.
var installer = new PresetGuidedInstaller(options);
installerDelegates.UiDelegates = new UiDelegates(ServiceProvider.GetRequiredService<ILogger<UiDelegates>>(), installer);
}
Expand Down
51 changes: 43 additions & 8 deletions src/NexusMods.Collections/InstallCollectionJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@
using NexusMods.Abstractions.IO.StreamFactories;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.Library;
using NexusMods.Abstractions.Library.Installers;
using NexusMods.Abstractions.Library.Models;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Extensions.BCL;
using NexusMods.Games.FOMOD;
using NexusMods.Hashing.xxHash64;
using NexusMods.MnemonicDB.Abstractions;
Expand Down Expand Up @@ -62,16 +59,22 @@ public class InstallCollectionJob : IJobDefinitionWithStart<InstallCollectionJob
};
return monitor.Begin<InstallCollectionJob, NexusCollectionLoadoutGroup.ReadOnly>(job);
}




/// <summary>
/// Starts the install of a collection.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async ValueTask<NexusCollectionLoadoutGroup.ReadOnly> StartAsync(IJobContext<InstallCollectionJob> context)
{
// Collections are essentially zip files with a collection.json file in them along with several other files.
// So we can treat them as a library archive, and then extract the collection.json file from them.
if (!SourceCollection.AsLibraryFile().TryGetAsLibraryArchive(out var archive))
throw new InvalidOperationException("The source collection is not a library archive.");

// Find the collection.json file and deserialize it.
var file = archive.Children.FirstOrDefault(f => f.Path == "collection.json");

CollectionRoot? root;

{
Expand All @@ -82,10 +85,12 @@ public class InstallCollectionJob : IJobDefinitionWithStart<InstallCollectionJob
if (root is null)
throw new InvalidOperationException("Failed to deserialize the collection.json file.");

// The collection.json file includes the mods by various ids and other info, so first we'll link those to library archives.
ConcurrentBag<ModInstructions> toInstall = new();

await Parallel.ForEachAsync(root.Mods, context.CancellationToken, async (mod, _) => toInstall.Add(await EnsureDownloaded(mod)));

// Now create the collection in the loadout
using var tx = Connection.BeginTransaction();

var group = new NexusCollectionLoadoutGroup.New(tx, out var id)
Expand All @@ -109,6 +114,7 @@ public class InstallCollectionJob : IJobDefinitionWithStart<InstallCollectionJob
var groupResult = await tx.Commit();
var groupRemapped = groupResult.Remap(group);

// Now install the mods
await Parallel.ForEachAsync(toInstall, context.CancellationToken, async (file, _) =>
{
// Bit strange, but Install Mod will want to find the collection group, so we'll have to rebase entity it will get the DB from
Expand All @@ -122,21 +128,31 @@ await Parallel.ForEachAsync(toInstall, context.CancellationToken, async (file, _

private async Task<LoadoutItemGroup.ReadOnly> InstallMod(LoadoutId loadoutId, ModInstructions file, LoadoutItemGroup.ReadOnly group)
{
// If the mod has a hashes entry, then we'll treat it as a replicated mod, and not use the library service to install it.
if (file.Mod.Hashes.Any())
return await InstallReplicatedMod(loadoutId, file, group);

// If there are predefined fomod choices, then we'll use the FomodXmlInstaller to install it.
if (file.Mod.Choices is { Type: ChoicesType.fomod })
return await InstallFomodWithPredefinedChoices(loadoutId, file, group);

// Otherwise, we'll just use the library service to install it.
return await LibraryService.InstallItem(file.LibraryFile.AsLibraryItem(), loadoutId, parent: group.LoadoutItemGroupId);
}

/// <summary>
/// Install a fomod with predefined choices.
/// </summary>
private async Task<LoadoutItemGroup.ReadOnly> InstallFomodWithPredefinedChoices(LoadoutId loadoutId, ModInstructions file, LoadoutItemGroup.ReadOnly collectionGroup)
{
// Get the archive from the library file
if (!file.LibraryFile.TryGetAsLibraryArchive(out var archive))
throw new InvalidOperationException("The library file is not a library archive.");

// Create the installer
var fomodInstaller = FomodXmlInstaller.Create(SerivceProvider, new GamePath(LocationId.Game, ""));

// Create the mod group and install the mod
using var tx = Connection.BeginTransaction();
var group = new LoadoutItemGroup.New(tx, out var id)
{
Expand Down Expand Up @@ -165,14 +181,19 @@ await Parallel.ForEachAsync(toInstall, context.CancellationToken, async (file, _
/// </summary>
private async Task<LoadoutItemGroup.ReadOnly> InstallReplicatedMod(LoadoutId loadoutId, ModInstructions file, LoadoutItemGroup.ReadOnly parentGroup)
{
// So collections hash everything by MD5, so we'll have to collect MD5 information for the files in the archive.
// We don't do this during indexing into the library because this is the only case where we need MD5 hashes.
ConcurrentDictionary<Md5HashValue, HashMapping> hashes = new();

// Get the archive from the library file
if (!file.LibraryFile.TryGetAsLibraryArchive(out var archive))
throw new InvalidOperationException("The library file is not a library archive.");

// Get the collection archive
if (!SourceCollection.AsLibraryFile().TryGetAsLibraryArchive(out var collectionArchive))
throw new InvalidOperationException("The source collection is not a library archive.");

// Hash all the files in the mod
await Parallel.ForEachAsync(archive.Children, async (child, token) =>
{
await using var stream = await FileStore.GetFileStream(child.AsLibraryFile().Hash, token);
Expand All @@ -189,6 +210,7 @@ await Parallel.ForEachAsync(archive.Children, async (child, token) =>
}
);

// If we have any binary patching to do, then we'll do that now.
if (file.Mod.Patches.Any())
await CreatePatches(file.Mod, archive, collectionArchive, hashes);

Expand All @@ -213,11 +235,14 @@ await Parallel.ForEachAsync(archive.Children, async (child, token) =>
LoadoutItemGroup = group,
};

// Now we map the files to their locations based on the hashes
foreach (var pair in file.Mod.Hashes)
{
// Try and find the hash we are looking for
if (!hashes.TryGetValue(pair.MD5, out var libraryItem))
throw new InvalidOperationException("The hash was not found in the archive.");

// Map the file to the specific path
var item = new LoadoutFile.New(tx, out var fileId)
{
Hash = libraryItem.Hash,
Expand Down Expand Up @@ -245,9 +270,12 @@ await Parallel.ForEachAsync(archive.Children, async (child, token) =>
private async Task CreatePatches(Mod modInfo, LibraryArchive.ReadOnly modArchive, LibraryArchive.ReadOnly collectionArchive,
ConcurrentDictionary<Md5HashValue, HashMapping> hashes)
{
// Index all the files in the collection zip file and the mod archive by their paths so we can find them easily.
var modChildren = IndexChildren(modArchive);
var collectionChildren = IndexChildren(collectionArchive);
ConcurrentBag<ArchivedFileEntry> patchedFiles = new();

// These are the generated patch files that we'll need to add to the file store.
ConcurrentBag<ArchivedFileEntry> patchedFiles = [];

await Parallel.ForEachAsync(modInfo.Patches, async (patch, token) =>
{
Expand All @@ -257,22 +285,28 @@ await Parallel.ForEachAsync(modInfo.Patches, async (patch, token) =>
if (!modChildren.TryGetValue(srcPath, out var file))
throw new InvalidOperationException("The file to patch was not found in the archive.");

// Load the source file and check the CRC32 hash
var srcData = (await FileStore.Load(file.Hash, token)).ToArray();

// Calculate the CRC32 hash of the source file
var srcCrc32 = Crc32.HashToUInt32(srcData.AsSpan());
if (srcCrc32 != srcCrc)
throw new InvalidOperationException("The source file's CRC32 hash does not match the expected hash.");

// Load the patch file
var patchName = RelativePath.FromUnsanitizedInput("patches/" + modInfo.Name + "/" + pathString + ".diff");
if (!collectionChildren.TryGetValue(patchName, out var patchFile))
throw new InvalidOperationException("The patch file was not found in the archive.");

var patchedFile = new MemoryStream();
var patchData = (await FileStore.Load(patchFile.Hash, token)).ToArray();

// Generate the patched file
BsDiff.BinaryPatch.Apply(new MemoryStream(srcData), () => new MemoryStream(patchData), patchedFile);

var patchedArray = patchedFile.ToArray();

// Hash the patched file and add it to the patched files list
using var md5 = MD5.Create();
md5.ComputeHash(patchedArray);
var md5Hash = Md5HashValue.From(md5.Hash!);
Expand All @@ -287,6 +321,7 @@ await Parallel.ForEachAsync(modInfo.Patches, async (patch, token) =>
}
);

// Backup the patched files
await FileStore.BackupFiles(patchedFiles, deduplicate: true);
}
private Dictionary<RelativePath, LibraryFile.ReadOnly> IndexChildren(LibraryArchive.ReadOnly archive)
Expand Down

This file was deleted.

0 comments on commit 42bd0fb

Please sign in to comment.