diff --git a/Directory.Packages.props b/Directory.Packages.props
index f71122ea63..5b936c9b24 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/benchmarks/NexusMods.Benchmarks/Benchmarks/DataModel.cs b/benchmarks/NexusMods.Benchmarks/Benchmarks/DataModel.cs
index d8ec9d2648..b0a0c740c1 100644
--- a/benchmarks/NexusMods.Benchmarks/Benchmarks/DataModel.cs
+++ b/benchmarks/NexusMods.Benchmarks/Benchmarks/DataModel.cs
@@ -26,7 +26,6 @@ public class DataStoreBenchmark : IBenchmark, IDisposable
private readonly IDataStore _dataStore;
private readonly byte[] _rawData;
private readonly Id64 _rawId;
- private readonly HashRelativePath _fromPutPath;
private readonly IId _immutableRecord;
private readonly FromArchive _record;
@@ -52,10 +51,6 @@ public DataStoreBenchmark()
Random.Shared.NextBytes(_rawData);
_rawId = new Id64(EntityCategory.TestData, (ulong)Random.Shared.NextInt64());
_dataStore.PutRaw(_rawId, _rawData);
-
- var relPutPath = "test.txt".ToRelativePath();
- _fromPutPath = new HashRelativePath(Hash.From((ulong)Random.Shared.NextInt64()), relPutPath);
-
_record = new FromArchive
{
Id = ModFileId.New(),
diff --git a/src/ArchiveManagement/NexusMods.FileExtractor/NexusMods.FileExtractor.csproj b/src/ArchiveManagement/NexusMods.FileExtractor/NexusMods.FileExtractor.csproj
index 0ceed6d208..69b2a06115 100644
--- a/src/ArchiveManagement/NexusMods.FileExtractor/NexusMods.FileExtractor.csproj
+++ b/src/ArchiveManagement/NexusMods.FileExtractor/NexusMods.FileExtractor.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalysisData.cs b/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalysisData.cs
index 5d55b5f78a..623580186d 100644
--- a/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalysisData.cs
+++ b/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalysisData.cs
@@ -5,7 +5,7 @@
namespace NexusMods.Games.BethesdaGameStudios;
[JsonName("BethesdaGameStudios.FileAnalysisData")]
-public class PluginAnalysisData : IFileAnalysisData
+public class PluginAnalysisData
{
public required RelativePath[] Masters { get; init; }
public bool IsLightMaster { get; init; }
diff --git a/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalyzer.cs b/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalyzer.cs
index 5ca3054039..fd62effe4d 100644
--- a/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalyzer.cs
+++ b/src/Games/NexusMods.Games.BethesdaGameStudios/PluginAnalyzer.cs
@@ -1,4 +1,3 @@
-using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Mutagen.Bethesda;
@@ -7,7 +6,6 @@
using Mutagen.Bethesda.Plugins.Meta;
using Mutagen.Bethesda.Plugins.Records;
using Mutagen.Bethesda.Skyrim;
-using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Abstractions.Ids;
using NexusMods.FileExtractor.FileSignatures;
using NexusMods.Paths;
@@ -17,17 +15,13 @@
namespace NexusMods.Games.BethesdaGameStudios;
[UsedImplicitly]
-public class PluginAnalyzer : IFileAnalyzer
+public class PluginAnalyzer
{
- public FileAnalyzerId Id { get; } = FileAnalyzerId.New("9c673a4f-064f-4b1e-83e3-4bf0454575cd", 1);
-
- public IEnumerable FileTypes => new[] { FileType.TES4 };
-
- private static readonly Extension[] ValidExtensions = {
- new(".esp"),
- new(".esm"),
- new(".esl"),
- };
+ private static readonly HashSet ValidExtensions = new Extension[] {
+ new (".esp"),
+ new (".esm"),
+ new (".esl"),
+ }.ToHashSet();
private readonly ILogger _logger;
@@ -36,12 +30,11 @@ public PluginAnalyzer(ILogger logger)
_logger = logger;
}
-#pragma warning disable CS1998
- public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo info, [EnumeratorCancellation] CancellationToken ct = default)
-#pragma warning restore CS1998
+ public async Task AnalyzeAsync(RelativePath path, Stream stream, CancellationToken ct = default)
{
- var extension = info.FileName.ToRelativePath().Extension;
- if (ValidExtensions[0] != extension && ValidExtensions[1] != extension && ValidExtensions[2] != extension) yield break;
+ var extension = path.Extension;
+ if (!ValidExtensions.Contains(extension))
+ return null;
// NOTE(erri120): The GameConstant specifies the header length.
// - Oblivion: 20 bytes
@@ -54,24 +47,24 @@ public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo i
// The current solution just tries different GameConstants, which isn't ideal and
// should be replaced with an identification step that finds the correct GameConstant.
- var startPos = info.Stream.Position;
- var fileAnalysisData = Analyze(GameConstants.SkyrimLE, info);
+ var fileAnalysisData = Analyze(path, GameConstants.SkyrimLE, stream);
if (fileAnalysisData is null)
{
- info.Stream.Position = startPos;
- fileAnalysisData = Analyze(GameConstants.Oblivion, info);
- if (fileAnalysisData is null) yield break;
+ stream.Position = 0;
+ fileAnalysisData = Analyze(path, GameConstants.Oblivion, stream);
+ if (fileAnalysisData is null)
+ return null;
}
- yield return fileAnalysisData;
+ return fileAnalysisData;
}
- private IFileAnalysisData? Analyze(GameConstants targetGame, FileAnalyzerInfo info)
+ private PluginAnalysisData? Analyze(RelativePath path, GameConstants targetGame, Stream stream)
{
try
{
using var readStream = new MutagenInterfaceReadStream(
- new BinaryReadStream(info.Stream, dispose: false),
+ new BinaryReadStream(stream, dispose: false),
new ParsingBundle(targetGame, masterReferences: null!)
{
ModKey = ModKey.Null
@@ -102,7 +95,7 @@ public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo i
}
catch (Exception e)
{
- _logger.LogError(e, "Exception while parsing {} ({})", info.FileName, info.RelativePath);
+ _logger.LogError(e, "Exception while parsing {} ({})", path.FileName, path);
return null;
}
}
diff --git a/src/Games/NexusMods.Games.BethesdaGameStudios/Services.cs b/src/Games/NexusMods.Games.BethesdaGameStudios/Services.cs
index bc8ab18bb6..380f398a76 100644
--- a/src/Games/NexusMods.Games.BethesdaGameStudios/Services.cs
+++ b/src/Games/NexusMods.Games.BethesdaGameStudios/Services.cs
@@ -14,7 +14,7 @@ public static IServiceCollection AddBethesdaGameStudios(this IServiceCollection
services.AddAllSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddAllSingleton();
+ services.AddSingleton();
services.AddAllSingleton();
return services;
}
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/Analyzers/ProjectAnalyzer.cs b/src/Games/NexusMods.Games.DarkestDungeon/Analyzers/ProjectAnalyzer.cs
deleted file mode 100644
index f2f1d075d3..0000000000
--- a/src/Games/NexusMods.Games.DarkestDungeon/Analyzers/ProjectAnalyzer.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Xml;
-using System.Xml.Schema;
-using System.Xml.Serialization;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.Abstractions.Ids;
-using NexusMods.FileExtractor.FileSignatures;
-using NexusMods.Games.DarkestDungeon.Models;
-
-namespace NexusMods.Games.DarkestDungeon.Analyzers;
-
-///
-/// implementation for native Darkest Dungeon mods that have a
-/// project.xml file. The file format is described in the game support document and
-/// deserialized to .
-///
-public class ProjectAnalyzer : IFileAnalyzer
-{
- public FileAnalyzerId Id => FileAnalyzerId.New("3152ea61-a5a1-4d89-a780-25ebbab3da3f", 2);
- public IEnumerable FileTypes => new[] { FileType.XML };
-
- public async IAsyncEnumerable AnalyzeAsync(
- FileAnalyzerInfo info,
- [EnumeratorCancellation] CancellationToken token = default)
- {
- await Task.Yield();
-
- var res = Analyze(info);
- if (res is null) yield break;
- yield return res;
- }
-
- private static ModProject? Analyze(FileAnalyzerInfo info)
- {
- if (!info.FileName.Equals("project.xml", StringComparison.OrdinalIgnoreCase))
- return null;
-
- using var reader = XmlReader.Create(info.Stream, new XmlReaderSettings
- {
- IgnoreComments = true,
- IgnoreWhitespace = true,
- ValidationFlags = XmlSchemaValidationFlags.AllowXmlAttributes
- });
-
- var obj = new XmlSerializer(typeof(ModProject)).Deserialize(reader);
- return obj as ModProject;
- }
-}
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/Installers/LooseFilesModInstaller.cs b/src/Games/NexusMods.Games.DarkestDungeon/Installers/LooseFilesModInstaller.cs
index ac96ed20cf..99664bf0c3 100644
--- a/src/Games/NexusMods.Games.DarkestDungeon/Installers/LooseFilesModInstaller.cs
+++ b/src/Games/NexusMods.Games.DarkestDungeon/Installers/LooseFilesModInstaller.cs
@@ -9,6 +9,7 @@
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.DarkestDungeon.Installers;
@@ -19,26 +20,18 @@ public class LooseFilesModInstaller : IModInstaller
{
private static readonly RelativePath ModsFolder = "mods".ToRelativePath();
- public ValueTask> GetModsAsync(
+ public async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
- {
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
-
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
{
var files = archiveFiles
+ .GetAllDescendentFiles()
.Select(kv =>
{
var (path, file) = kv;
- return file.ToFromArchive(
+ return file!.ToFromArchive(
new GamePath(GameFolderType.Game, ModsFolder.Join(path))
);
});
@@ -47,13 +40,13 @@ private IEnumerable GetMods(
// this needs to be serialized to XML and added to the files enumerable
var modProject = new ModProject
{
- Title = archiveFiles.First().Key.TopParent.ToString()
+ Title = archiveFiles.Path.TopParent.ToString()
};
- yield return new ModInstallerResult
+ return new [] { new ModInstallerResult
{
Id = baseModId,
Files = files,
- };
+ }};
}
}
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/Installers/NativeModInstaller.cs b/src/Games/NexusMods.Games.DarkestDungeon/Installers/NativeModInstaller.cs
index de4bc34bdc..1af8afc5b8 100644
--- a/src/Games/NexusMods.Games.DarkestDungeon/Installers/NativeModInstaller.cs
+++ b/src/Games/NexusMods.Games.DarkestDungeon/Installers/NativeModInstaller.cs
@@ -1,4 +1,7 @@
using System.Diagnostics;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.ArchiveContents;
@@ -10,6 +13,7 @@
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.DarkestDungeon.Installers;
@@ -22,57 +26,60 @@ public class NativeModInstaller : IModInstaller
private static readonly RelativePath ModsFolder = "mods".ToRelativePath();
private static readonly RelativePath ProjectFile = "project.xml".ToRelativePath();
- internal static IEnumerable> GetModProjects(
- EntityDictionary archiveFiles)
+ internal static async Task Node, ModProject Project)>>
+ GetModProjects(
+ FileTreeNode archiveFiles)
{
- return archiveFiles.Where(kv =>
- {
- var (path, file) = kv;
+ return await archiveFiles
+ .GetAllDescendentFiles()
+ .SelectAsync(async kv =>
+ {
+ if (kv.Path.FileName != ProjectFile)
+ return default;
- if (!path.FileName.Equals(ProjectFile)) return false;
- var modProject = file.AnalysisData
- .OfType()
- .FirstOrDefault();
+ await using var stream = await kv.Value!.Open();
+ using var reader = XmlReader.Create(stream, new XmlReaderSettings
+ {
+ IgnoreComments = true,
+ IgnoreWhitespace = true,
+ ValidationFlags = XmlSchemaValidationFlags.AllowXmlAttributes
+ });
- return modProject is not null;
- });
+ var obj = new XmlSerializer(typeof(ModProject)).Deserialize(reader);
+ return (kv, obj as ModProject);
+ })
+ .Where(r => r.Item2 is not null)
+ .Select(r => (r.kv, r.Item2!))
+ .ToArrayAsync();
}
- public ValueTask> GetModsAsync(
+ public async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(srcArchiveHash, archiveFiles));
- }
-
- private static IEnumerable GetMods(
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var modProjectFiles = GetModProjects(archiveFiles).ToArray();
+ var modProjectFiles = (await GetModProjects(archiveFiles)).ToArray();
if (!modProjectFiles.Any())
return Array.Empty();
+ if (modProjectFiles.Length > 1)
+ return Array.Empty();
+
var mods = modProjectFiles
.Select(modProjectFile =>
{
- var parent = modProjectFile.Key.Parent;
- var modProject = modProjectFile.Value.AnalysisData
- .OfType()
- .FirstOrDefault();
+ var parent = modProjectFile.Node.Parent;
+ var modProject = modProjectFile.Project;
if (modProject is null) throw new UnreachableException();
- var modFiles = archiveFiles
- .Where(kv => kv.Key.InFolder(parent))
+ var modFiles = parent.GetAllDescendentFiles()
.Select(kv =>
{
var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, ModsFolder.Join(path.DropFirst(parent.Depth)))
+ return file!.ToFromArchive(
+ new GamePath(GameFolderType.Game, ModsFolder.Join(path.DropFirst(parent.Depth - 1)))
);
});
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/Models/ModProject.cs b/src/Games/NexusMods.Games.DarkestDungeon/Models/ModProject.cs
index aa09c6210f..e5b0f6b255 100644
--- a/src/Games/NexusMods.Games.DarkestDungeon/Models/ModProject.cs
+++ b/src/Games/NexusMods.Games.DarkestDungeon/Models/ModProject.cs
@@ -9,7 +9,7 @@ namespace NexusMods.Games.DarkestDungeon.Models;
///
[JsonName("NexusMods.Games.DarkestDungeon.ModProject")]
[XmlRoot(ElementName = "project")]
-public record ModProject : IFileAnalysisData
+public record ModProject
{
[XmlElement(ElementName = "Title")]
public string Title { get; set; } = string.Empty;
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/NexusMods.Games.DarkestDungeon.csproj b/src/Games/NexusMods.Games.DarkestDungeon/NexusMods.Games.DarkestDungeon.csproj
index a5c1f89809..ccad0b7559 100644
--- a/src/Games/NexusMods.Games.DarkestDungeon/NexusMods.Games.DarkestDungeon.csproj
+++ b/src/Games/NexusMods.Games.DarkestDungeon/NexusMods.Games.DarkestDungeon.csproj
@@ -11,4 +11,5 @@
+
diff --git a/src/Games/NexusMods.Games.DarkestDungeon/Services.cs b/src/Games/NexusMods.Games.DarkestDungeon/Services.cs
index c959823f83..74a08ce294 100644
--- a/src/Games/NexusMods.Games.DarkestDungeon/Services.cs
+++ b/src/Games/NexusMods.Games.DarkestDungeon/Services.cs
@@ -1,9 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
-using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.Games.DarkestDungeon.Analyzers;
using NexusMods.Games.DarkestDungeon.Installers;
namespace NexusMods.Games.DarkestDungeon;
@@ -13,7 +11,6 @@ public static class Services
public static IServiceCollection AddDarkestDungeon(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
diff --git a/src/Games/NexusMods.Games.FOMOD/FomodAnalyzer.cs b/src/Games/NexusMods.Games.FOMOD/FomodAnalyzer.cs
index 0af2a0b9ff..1a495c3cda 100644
--- a/src/Games/NexusMods.Games.FOMOD/FomodAnalyzer.cs
+++ b/src/Games/NexusMods.Games.FOMOD/FomodAnalyzer.cs
@@ -1,48 +1,25 @@
-using System.Runtime.CompilerServices;
-using System.Text;
+using System.Text;
using FomodInstaller.Scripting.XmlScript;
using JetBrains.Annotations;
-using Microsoft.Extensions.Logging;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.Abstractions.Ids;
-using NexusMods.DataModel.JsonConverters;
-using NexusMods.FileExtractor.FileSignatures;
+using NexusMods.Common;
+using NexusMods.DataModel.ModInstallers;
using NexusMods.Paths;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.FOMOD;
-public class FomodAnalyzer : IFileAnalyzer
+public class FomodAnalyzer
{
- public FileAnalyzerId Id { get; } = FileAnalyzerId.New("e5dcce84-ad7c-4882-8873-4f6a2e45a279", 1);
-
- private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
-
- // Note: No type for .fomod because FOMODs are existing archive types listed below.
- public FomodAnalyzer(ILogger logger, IFileSystem fileSystem)
- {
- _logger = logger;
- _fileSystem = fileSystem;
- }
-
- public IEnumerable FileTypes { get; } = new [] { FileType.XML };
-
- public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo info, [EnumeratorCancellation] CancellationToken ct = default)
+ public static async ValueTask AnalyzeAsync(FileTreeNode allFiles, IFileSystem fileSystem,
+ CancellationToken ct = default)
{
- // Not sourced from an archive.
- if (info.RelativePath == null)
- yield break;
- // Check if file path is "fomod/ModuleConfig.xml"
- if (!info.RelativePath.Value.EndsWith(FomodConstants.XmlConfigRelativePath))
- yield break;
-
- // If not from inside an archive, this is probably not a FOMOD.
- if (info.ParentArchive == null)
- yield break;
+ if (!allFiles.GetAllDescendentFiles()
+ .TryGetFirst(x => x.Path.EndsWith(FomodConstants.XmlConfigRelativePath), out var xmlNode))
+ return null;
// If the fomod folder is not at first level, find the prefix.
- var pathPrefix = info.RelativePath.Value.Parent.Parent;
+ var pathPrefix = xmlNode!.Parent.Parent;
// Now get the actual items out.
// Determine if this is a supported FOMOD.
@@ -51,7 +28,8 @@ public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo i
try
{
- using var streamReader = new StreamReader(info.Stream, leaveOpen:true);
+ await using var stream = await xmlNode.Value!.Open();
+ using var streamReader = new StreamReader(stream, leaveOpen:true);
data = await streamReader.ReadToEndAsync(ct);
var xmlScript = new XmlScriptType();
var script = (XmlScript)xmlScript.LoadScript(data, true);
@@ -61,21 +39,19 @@ async Task AddImageIfValid(string? imagePathFragment)
{
if (string.IsNullOrEmpty(imagePathFragment))
return;
- var imagePath = pathPrefix.Join(RelativePath.FromUnsanitizedInput(imagePathFragment));
-
- var path = info.ParentArchive!.Value.Path.Combine(imagePath);
+ var imagePath = pathPrefix.Path.Join(RelativePath.FromUnsanitizedInput(imagePathFragment));
byte[] bytes;
try
{
- bytes = await path.ReadAllBytesAsync(ct);
- }
- catch (FileNotFoundException)
- {
- bytes = await GetPlaceholderImage(ct);
+ var node = allFiles.FindNode(imagePath);
+ await using var imageStream = await node!.Value!.Open();
+ using var ms = new MemoryStream();
+ await imageStream.CopyToAsync(ms, ct);
+ bytes = ms.ToArray();
}
- catch (DirectoryNotFoundException)
+ catch (Exception)
{
- bytes = await GetPlaceholderImage(ct);
+ bytes = await GetPlaceholderImage(fileSystem, ct);
}
images!.Add(new FomodAnalyzerInfo.FomodAnalyzerImage(imagePath, bytes));
@@ -87,47 +63,45 @@ async Task AddImageIfValid(string? imagePathFragment)
foreach (var option in optionGroup.Options)
await AddImageIfValid(option.ImagePath);
}
- catch (Exception e)
+ catch (Exception)
{
- _logger.LogError("Failed to Parse FOMOD: {EMessage}\\n{EStackTrace}", e.Message, e.StackTrace);
- yield break;
+ return null;
}
// TODO: We use Base64 here, which is really, really inefficient. We should zip up the images and store them separately in a non-SOLID archive.
// Add all images to analysis output.
- yield return new FomodAnalyzerInfo()
+ return new FomodAnalyzerInfo
{
XmlScript = data!,
- Images = images
+ Images = images,
+ PathPrefix = pathPrefix.Path
};
}
- internal async Task GetPlaceholderImage(CancellationToken ct = default)
+ public static Task GetPlaceholderImage(IFileSystem fileSystem, CancellationToken ct = default)
{
- return await _fileSystem.GetKnownPath(KnownPath.EntryDirectory).Combine("Assets/InvalidImagePlaceholder.png")
- .ReadAllBytesAsync(ct);
+ return fileSystem.GetKnownPath(KnownPath.EntryDirectory).Combine("Assets/InvalidImagePlaceholder.png").ReadAllBytesAsync(ct);
}
}
-[JsonName("NexusMods.Games.FOMOD.FomodAnalyzerInfo")]
-public record FomodAnalyzerInfo : IFileAnalysisData
+public record FomodAnalyzerInfo
{
public required string XmlScript { get; init; }
public required List Images { get; init; }
+ public required RelativePath PathPrefix { get; init; }
+
public record struct FomodAnalyzerImage(string Path, byte[] Image);
// Keeping in case this is ever needed. We can remove this once all FOMOD stuff is done.
[PublicAPI]
- public async Task DumpToFileSystemAsync(TemporaryPath fomodFolder)
+ public async Task DumpToFileSystemAsync(AbsolutePath fomodFolder)
{
var fs = FileSystem.Shared;
- var path = fomodFolder.Path;
-
// Dump Item
async Task DumpItem(string relativePath, byte[] data)
{
- var finalPath = path.Combine(relativePath);
+ var finalPath = fomodFolder.Combine(relativePath);
fs.CreateDirectory(finalPath.Parent);
await fs.WriteAllBytesAsync(finalPath, data);
}
diff --git a/src/Games/NexusMods.Games.FOMOD/FomodXmlInstaller.cs b/src/Games/NexusMods.Games.FOMOD/FomodXmlInstaller.cs
index ab797dfee5..bea339bc8f 100644
--- a/src/Games/NexusMods.Games.FOMOD/FomodXmlInstaller.cs
+++ b/src/Games/NexusMods.Games.FOMOD/FomodXmlInstaller.cs
@@ -12,18 +12,21 @@
using NexusMods.DataModel.ModInstallers;
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
+using NexusMods.Paths.FileTree;
using NexusMods.Paths.Utilities;
using OneOf.Types;
using Mod = FomodInstaller.Interface.Mod;
namespace NexusMods.Games.FOMOD;
-public class FomodXmlInstaller : IModInstaller
+public class FomodXmlInstaller : AModInstaller
{
private readonly ICoreDelegates _delegates;
private readonly XmlScriptType _scriptType = new();
private readonly ILogger _logger;
private readonly GamePath _fomodInstallationPath;
+ private readonly IFileSystem _fileSystem;
+ private readonly TemporaryFileManager _tempoaryFileManager;
///
/// Creates a new instance of given the provided and .
@@ -34,30 +37,25 @@ public class FomodXmlInstaller : IModInstaller
public static FomodXmlInstaller Create(IServiceProvider provider, GamePath fomodInstallationPath)
{
return new FomodXmlInstaller(provider.GetRequiredService>(),
- provider.GetRequiredService(),
- fomodInstallationPath);
+ provider.GetRequiredService(), provider.GetRequiredService(),
+ provider.GetRequiredService(),
+
+ fomodInstallationPath, provider);
}
public FomodXmlInstaller(ILogger logger, ICoreDelegates coreDelegates,
- GamePath fomodInstallationPath)
+ IFileSystem fileSystem, TemporaryFileManager temporaryFileManager, GamePath fomodInstallationPath,
+ IServiceProvider serviceProvider) : base(serviceProvider)
{
_delegates = coreDelegates;
_fomodInstallationPath = fomodInstallationPath;
_logger = logger;
+ _tempoaryFileManager = temporaryFileManager;
+ _fileSystem = fileSystem;
}
- public Priority GetPriority(GameInstallation installation,
- EntityDictionary archiveFiles)
- {
- var hasScript = archiveFiles.Keys.Any(x => x.EndsWith(FomodConstants.XmlConfigRelativePath));
- return hasScript ? Priority.High : Priority.None;
- }
-
- public async ValueTask> GetModsAsync(
- GameInstallation gameInstallation,
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ public override async ValueTask> GetModsAsync(GameInstallation gameInstallation,
+ ModId baseModId, FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
// the component dealing with FOMODs is built to support all kinds of mods, including those without a script.
@@ -65,20 +63,19 @@ public async ValueTask> GetModsAsync(
// we only intend to support xml scripted FOMODs, this should be good enough
var stopPattern = new List { "fomod" };
- if (!archiveFiles.Keys.TryGetFirst(x => x.EndsWith(FomodConstants.XmlConfigRelativePath), out var xmlFile))
- throw new UnreachableException(
- $"$[{nameof(FomodXmlInstaller)}] XML file not found. This should never be true and is indicative of a bug.");
+ var analyzerInfo = await FomodAnalyzer.AnalyzeAsync(archiveFiles, _fileSystem, cancellationToken);
+ if (analyzerInfo == default)
+ return Array.Empty();
+
+ await using var tmpFolder = _tempoaryFileManager.CreateFolder();
- if (!archiveFiles.TryGetValue(xmlFile, out var analyzedFile))
- throw new UnreachableException(
- $"$[{nameof(FomodXmlInstaller)}] XML data not found. This should never be true and is indicative of a bug.");
+ await analyzerInfo.DumpToFileSystemAsync(tmpFolder.Path.Combine(analyzerInfo.PathPrefix));
- var analyzerInfo = analyzedFile.AnalysisData.OfType().FirstOrDefault();
- if (analyzerInfo == default) return Array.Empty();
+ var xmlPath = analyzerInfo.PathPrefix.Join(FomodConstants.XmlConfigRelativePath);
// Setup mod, exclude script path so it doesn't get picked up and thus double read from disk
- var modFiles = archiveFiles.Keys.Select(x => x.ToNativeSeparators(OSInformation.Shared)).ToList();
- var mod = new Mod(modFiles, stopPattern, xmlFile, string.Empty, _scriptType);
+ var modFiles = archiveFiles.GetAllDescendentFiles().Select(x => x.Path.ToNativeSeparators(OSInformation.Shared)).ToList();
+ var mod = new Mod(modFiles, stopPattern, xmlPath.ToString(), string.Empty, _scriptType);
await mod.InitializeWithoutLoadingScript();
var executor = _scriptType.CreateExecutor(mod, _delegates);
@@ -114,7 +111,7 @@ private static string FixXmlScript(string input)
private IEnumerable InstructionsToModFiles(
IList instructions,
- EntityDictionary files,
+ FileTreeNode files,
GamePath gameTargetPath)
{
var res = instructions.Select(instruction =>
@@ -142,22 +139,23 @@ private IEnumerable InstructionsToModFiles(
private static AModFile ConvertInstructionCopy(
Instruction instruction,
- EntityDictionary files,
+ FileTreeNode files,
GamePath gameTargetPath)
{
var src = RelativePath.FromUnsanitizedInput(instruction.source);
var dest = RelativePath.FromUnsanitizedInput(instruction.destination);
- var file = files.First(x => x.Key.Equals(src)).Value;
+ var file = files.FindNode(src)!.Value;
return new FromArchive
{
Id = ModFileId.New(),
To = new GamePath(gameTargetPath.Type, gameTargetPath.Path.Join(dest)),
- Hash = file.Hash,
+ Hash = file!.Hash,
Size = file.Size
};
}
+
private static AModFile ConvertInstructionMkdir(
Instruction instruction,
GamePath gameTargetPath)
diff --git a/src/Games/NexusMods.Games.FOMOD/Services.cs b/src/Games/NexusMods.Games.FOMOD/Services.cs
index f5d7ad706b..8a44f9ef21 100644
--- a/src/Games/NexusMods.Games.FOMOD/Services.cs
+++ b/src/Games/NexusMods.Games.FOMOD/Services.cs
@@ -17,7 +17,6 @@ public static class Services
/// Collection of services.
public static IServiceCollection AddFomod(this IServiceCollection services)
{
- services.AddAllSingleton();
services.AddAllSingleton();
services.AddAllSingleton();
return services;
diff --git a/src/Games/NexusMods.Games.Generic/Entities/IniAnalysisData.cs b/src/Games/NexusMods.Games.Generic/Entities/IniAnalysisData.cs
deleted file mode 100644
index ac91642ef3..0000000000
--- a/src/Games/NexusMods.Games.Generic/Entities/IniAnalysisData.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.JsonConverters;
-
-namespace NexusMods.Games.Generic.Entities;
-
-[JsonName("Generic.FileAnalysisData")]
-public class IniAnalysisData : IFileAnalysisData
-{
- public required HashSet Sections { get; init; }
- public required HashSet Keys { get; init; }
-}
diff --git a/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalysisData.cs b/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalysisData.cs
new file mode 100644
index 0000000000..c98b689492
--- /dev/null
+++ b/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalysisData.cs
@@ -0,0 +1,7 @@
+namespace NexusMods.Games.Generic.FileAnalyzers;
+
+public class IniAnalysisData
+{
+ public required HashSet Sections { get; init; }
+ public required HashSet Keys { get; init; }
+}
diff --git a/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalzyer.cs b/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalzyer.cs
index 322663bcb3..75d9b57478 100644
--- a/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalzyer.cs
+++ b/src/Games/NexusMods.Games.Generic/FileAnalyzers/IniAnalzyer.cs
@@ -2,15 +2,14 @@
using IniParser;
using IniParser.Model.Configuration;
using IniParser.Parser;
-using NexusMods.DataModel.Abstractions;
+using NexusMods.Common;
using NexusMods.DataModel.Abstractions.Ids;
using NexusMods.FileExtractor.FileSignatures;
-using NexusMods.Games.Generic.Entities;
namespace NexusMods.Games.Generic.FileAnalyzers;
-public class IniAnalzyer : IFileAnalyzer
+public class IniAnalzyer
{
public FileAnalyzerId Id { get; } = FileAnalyzerId.New("904bca7b-fbd6-4350-b4e2-6fdbd034ec76", 1);
public IEnumerable FileTypes => new[] { FileType.INI };
@@ -25,16 +24,15 @@ public class IniAnalzyer : IFileAnalyzer
SkipInvalidLines = true,
};
-#pragma warning disable CS1998
- public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo info, [EnumeratorCancellation] CancellationToken token = default)
-#pragma warning restore CS1998
+ public static async Task AnalyzeAsync(IStreamFactory info)
{
- var data = new StreamIniDataParser(new IniDataParser(Config)).ReadData(new StreamReader(info.Stream));
+ await using var os = await info.GetStreamAsync();
+ var data = new StreamIniDataParser(new IniDataParser(Config)).ReadData(new StreamReader(os));
var sections = data.Sections.Select(s => s.SectionName).ToHashSet();
var keys = data.Global.Select(k => k.KeyName)
.Concat(data.Sections.SelectMany(d => d.Keys).Select(kv => kv.KeyName))
.ToHashSet();
- yield return new IniAnalysisData
+ return new IniAnalysisData
{
Sections = sections,
Keys = keys
diff --git a/src/Games/NexusMods.Games.Generic/Installers/GenericFolderMatchInstaller.cs b/src/Games/NexusMods.Games.Generic/Installers/GenericFolderMatchInstaller.cs
index b916ed2d79..f2b919acbe 100644
--- a/src/Games/NexusMods.Games.Generic/Installers/GenericFolderMatchInstaller.cs
+++ b/src/Games/NexusMods.Games.Generic/Installers/GenericFolderMatchInstaller.cs
@@ -1,15 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
-using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Games.GameCapabilities.FolderMatchInstallerCapability;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Loadouts.ModFiles;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.Generic.Installers;
@@ -21,7 +18,7 @@ namespace NexusMods.Games.Generic.Installers;
///
/// Example: myMod/Textures/myTexture.dds -> Skyrim/Data/Textures/myTexture.dds
///
-public class GenericFolderMatchInstaller : IModInstaller
+public class GenericFolderMatchInstaller : AModInstaller
{
private readonly ILogger _logger;
private readonly IEnumerable _installFolderTargets;
@@ -34,10 +31,11 @@ public class GenericFolderMatchInstaller : IModInstaller
///
public static GenericFolderMatchInstaller Create(IServiceProvider provider, IEnumerable installFolderTargets)
{
- return new GenericFolderMatchInstaller(provider.GetRequiredService>(), installFolderTargets);
+ return new GenericFolderMatchInstaller(provider.GetRequiredService>(), installFolderTargets, provider);
}
- private GenericFolderMatchInstaller(ILogger logger, IEnumerable installFolderTargets)
+ private GenericFolderMatchInstaller(ILogger logger, IEnumerable installFolderTargets, IServiceProvider serviceProvider)
+ : base(serviceProvider)
{
_logger = logger;
_installFolderTargets = installFolderTargets;
@@ -45,12 +43,11 @@ private GenericFolderMatchInstaller(ILogger logger,
#region IModInstaller
- public ValueTask> GetModsAsync(GameInstallation gameInstallation, ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles, CancellationToken cancellationToken = default)
+ public override ValueTask> GetModsAsync(GameInstallation gameInstallation,
+ ModId baseModId, FileTreeNode archiveFiles,
+ CancellationToken cancellationToken = default)
{
-
List missedFiles = new();
List modFiles = new();
@@ -94,7 +91,7 @@ public ValueTask> GetModsAsync(GameInstallation
///
///
///
- private IEnumerable GetModFilesForTarget(EntityDictionary archiveFiles,
+ private IEnumerable GetModFilesForTarget(FileTreeNode archiveFiles,
InstallFolderTarget target, List missedFiles)
{
List modFiles = new();
@@ -102,9 +99,10 @@ private IEnumerable GetModFilesForTarget(EntityDictionary f.Path),
+ out var prefixToDrop))
{
- foreach (var (filePath, fileData) in archiveFiles)
+ foreach (var (filePath, fileData) in archiveFiles.GetAllDescendentFiles())
{
var trimmedPath = filePath;
@@ -128,7 +126,7 @@ private IEnumerable GetModFilesForTarget(EntityDictionary
+
+
+
+
diff --git a/src/Games/NexusMods.Games.Generic/Services.cs b/src/Games/NexusMods.Games.Generic/Services.cs
index 31f1ef0f99..864ffc0d90 100644
--- a/src/Games/NexusMods.Games.Generic/Services.cs
+++ b/src/Games/NexusMods.Games.Generic/Services.cs
@@ -1,10 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
-using NexusMods.DataModel.ModInstallers;
-using NexusMods.Games.Generic.Entities;
using NexusMods.Games.Generic.FileAnalyzers;
-using NexusMods.Games.Generic.Installers;
namespace NexusMods.Games.Generic;
@@ -12,9 +7,7 @@ public static class Services
{
public static IServiceCollection AddGenericGameSupport(this IServiceCollection services)
{
- services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
return services;
}
}
diff --git a/src/Games/NexusMods.Games.Generic/TypeFinder.cs b/src/Games/NexusMods.Games.Generic/TypeFinder.cs
deleted file mode 100644
index b7206c041b..0000000000
--- a/src/Games/NexusMods.Games.Generic/TypeFinder.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
-using NexusMods.Games.Generic.Entities;
-
-namespace NexusMods.Games.Generic;
-
-public class TypeFinder : ITypeFinder
-{
- public IEnumerable DescendentsOf(Type type)
- {
- return AllTypes.Where(t => t.IsAssignableTo(type));
- }
-
- private IEnumerable AllTypes => new[]
- {
- typeof(IniAnalysisData),
- };
-}
diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077.cs
index 4a5bc5cc3f..cf6e7cb580 100644
--- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077.cs
+++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077.cs
@@ -11,10 +11,12 @@ public class Cyberpunk2077 : AGame, ISteamGame, IGogGame, IEpicGame
{
public static readonly GameDomain StaticDomain = GameDomain.From("cyberpunk2077");
private readonly IFileSystem _fileSystem;
+ private readonly IServiceProvider _serviceProvider;
- public Cyberpunk2077(IEnumerable gameLocators, IFileSystem fileSystem) : base(gameLocators)
+ public Cyberpunk2077(IEnumerable gameLocators, IFileSystem fileSystem, IServiceProvider provider) : base(gameLocators)
{
_fileSystem = fileSystem;
+ _serviceProvider = provider;
}
public override string Name => "Cyberpunk 2077";
@@ -53,7 +55,7 @@ protected override IEnumerable> GetLo
{
new RedModInstaller(),
new SimpleOverlayModInstaller(),
- new AppearancePreset(),
+ new AppearancePreset(_serviceProvider),
new FolderlessModInstaller()
};
}
diff --git a/src/Games/NexusMods.Games.RedEngine/FileAnalyzers/RedModInfoAnalyzer.cs b/src/Games/NexusMods.Games.RedEngine/FileAnalyzers/RedModInfoAnalyzer.cs
deleted file mode 100644
index ef65bb9f67..0000000000
--- a/src/Games/NexusMods.Games.RedEngine/FileAnalyzers/RedModInfoAnalyzer.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.Abstractions.Ids;
-using NexusMods.DataModel.JsonConverters;
-using NexusMods.FileExtractor.FileSignatures;
-
-namespace NexusMods.Games.RedEngine.FileAnalyzers;
-
-public class RedModInfoAnalyzer : IFileAnalyzer
-{
- public FileAnalyzerId Id { get; } = FileAnalyzerId.New("8bf03d10-c48e-4929-906f-852b6afecd5e", 1);
-
- public IEnumerable FileTypes => new[] { FileType.JSON };
- public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo info, [EnumeratorCancellation] CancellationToken ct = default)
- {
- // TODO: We can probably check by fileName here before blindly deserializing - Sewer.
- InfoJson? jsonInfo;
- try
- {
- jsonInfo = await JsonSerializer.DeserializeAsync(info.Stream, cancellationToken: ct);
- }
- catch (JsonException)
- {
- yield break;
- }
- if (jsonInfo != null)
- yield return new RedModInfo { Name = jsonInfo.Name };
- }
-}
-
-internal class InfoJson
-{
- [JsonPropertyName("name")]
- public string Name { get; set; } = string.Empty;
-}
-
-[JsonName("NexusMods.Games.RedEngine.FileAnalyzers.RedModInfo")]
-public record RedModInfo : IFileAnalysisData
-{
- // ReSharper disable once UnusedAutoPropertyAccessor.Global
- public required string Name { get; init; }
-}
diff --git a/src/Games/NexusMods.Games.RedEngine/ModInstallers/AppearancePreset.cs b/src/Games/NexusMods.Games.RedEngine/ModInstallers/AppearancePreset.cs
index e1f5923f63..23cf221eb2 100644
--- a/src/Games/NexusMods.Games.RedEngine/ModInstallers/AppearancePreset.cs
+++ b/src/Games/NexusMods.Games.RedEngine/ModInstallers/AppearancePreset.cs
@@ -1,57 +1,54 @@
-using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
-using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.FileExtractor.FileSignatures;
-using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
using NexusMods.Paths.Utilities;
namespace NexusMods.Games.RedEngine.ModInstallers;
-public class AppearancePreset : IModInstaller
+///
+/// This mod installer is used to install appearance presets for Cyberpunk 2077, they are installed into a specific
+/// folder under the cyber engine tweaks mod's subfolder for the appearance change unlocker.
+///
+public class AppearancePreset : AModInstaller
{
private static readonly RelativePath[] Paths = {
"bin/x64/plugins/cyber_engine_tweaks/mods/AppearanceChangeUnlocker/character-preset/female".ToRelativePath(),
"bin/x64/plugins/cyber_engine_tweaks/mods/AppearanceChangeUnlocker/character-preset/male".ToRelativePath()
};
- public ValueTask> GetModsAsync(
+ ///
+ /// DI Constructor
+ ///
+ ///
+ public AppearancePreset(IServiceProvider serviceProvider) : base(serviceProvider) { }
+
+
+ public override async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
-
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var modFiles = archiveFiles
- .Where(kv => kv.Key.Extension == KnownExtensions.Preset)
+ var modFiles = archiveFiles.GetAllDescendentFiles()
+ .Where(kv => kv.Path.Extension == KnownExtensions.Preset)
.SelectMany(kv =>
{
var (path, file) = kv;
- return Paths.Select(relPath => file.ToFromArchive(
+ return Paths.Select(relPath => file!.ToFromArchive(
new GamePath(GameFolderType.Game, relPath.Join(path))
));
}).ToArray();
if (!modFiles.Any())
- yield break;
+ return NoResults;
- yield return new ModInstallerResult
+ return new ModInstallerResult[] { new()
{
Id = baseModId,
Files = modFiles
- };
+ }};
}
}
diff --git a/src/Games/NexusMods.Games.RedEngine/ModInstallers/FolderlessModInstaller.cs b/src/Games/NexusMods.Games.RedEngine/ModInstallers/FolderlessModInstaller.cs
index f56754cead..94b4fcdb7b 100644
--- a/src/Games/NexusMods.Games.RedEngine/ModInstallers/FolderlessModInstaller.cs
+++ b/src/Games/NexusMods.Games.RedEngine/ModInstallers/FolderlessModInstaller.cs
@@ -1,14 +1,9 @@
-using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
-using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
-using NexusMods.Paths.Utilities;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.RedEngine.ModInstallers;
@@ -19,39 +14,27 @@ public class FolderlessModInstaller : IModInstaller
{
private static readonly RelativePath Destination = "archive/pc/mod".ToRelativePath();
- public ValueTask> GetModsAsync(
- GameInstallation gameInstallation,
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
- CancellationToken cancellationToken = default)
+ public async ValueTask> GetModsAsync(GameInstallation gameInstallation, ModId baseModId,
+ FileTreeNode archiveFiles, CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var modFiles = archiveFiles
- .Where(kv => !Helpers.IgnoreExtensions.Contains(kv.Key.Extension))
- .Select(kv =>
- {
- var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, Destination.Join(path.FileName))
- );
- })
+ var modFiles = archiveFiles.GetAllDescendentFiles()
+ .Where(f => !Helpers.IgnoreExtensions.Contains(f.Path.Extension))
+ .Select(f => f.Value!.ToFromArchive(
+ new GamePath(GameFolderType.Game, Destination.Join(f.Path.FileName))
+ ))
.ToArray();
if (!modFiles.Any())
- yield break;
+ return Enumerable.Empty();
- yield return new ModInstallerResult
+ return new[]
{
- Id = baseModId,
- Files = modFiles
+ new ModInstallerResult
+ {
+ Id = baseModId,
+ Files = modFiles
+ }
};
}
}
diff --git a/src/Games/NexusMods.Games.RedEngine/ModInstallers/RedModInstaller.cs b/src/Games/NexusMods.Games.RedEngine/ModInstallers/RedModInstaller.cs
index 4784882186..e59156fd00 100644
--- a/src/Games/NexusMods.Games.RedEngine/ModInstallers/RedModInstaller.cs
+++ b/src/Games/NexusMods.Games.RedEngine/ModInstallers/RedModInstaller.cs
@@ -1,14 +1,13 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.Games.RedEngine.FileAnalyzers;
-using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.RedEngine.ModInstallers;
@@ -17,52 +16,55 @@ public class RedModInstaller : IModInstaller
private static readonly RelativePath InfoJson = "info.json".ToRelativePath();
private static readonly RelativePath Mods = "mods".ToRelativePath();
- private static bool IsInfoJson(KeyValuePair file)
+ public async ValueTask> GetModsAsync(GameInstallation gameInstallation, ModId baseModId,
+ FileTreeNode archiveFiles, CancellationToken cancellationToken = default)
{
- return file.Key.FileName == InfoJson && file.Value.AnalysisData.OfType().Any();
- }
+ var infos = (await archiveFiles.GetAllDescendentFiles()
+ .Where(f => f.Path.FileName == InfoJson)
+ .SelectAsync(async f => (File: f, InfoJson: await ReadInfoJson(f.Value!)))
+ .ToArrayAsync())
+ .Where(node => node.InfoJson != null)
+ .ToArray();
- public ValueTask> GetModsAsync(
- GameInstallation gameInstallation,
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
- CancellationToken cancellationToken = default)
- {
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var modFiles = archiveFiles
- .Where(IsInfoJson)
- .SelectMany(infoJson =>
+ List results = new();
+
+ var baseIdUsed = false;
+ foreach (var node in infos)
+ {
+ var modFolder = node.File.Parent;
+ var parentName = modFolder.Name;
+ var files = new List();
+ foreach (var childNode in modFolder.GetAllDescendentFiles())
{
- var parent = infoJson.Key.Parent;
- var parentName = parent.FileName;
+ var path = childNode.Path;
+ var entry = childNode.Value;
+ files.Add(entry!.ToFromArchive(new GamePath(GameFolderType.Game, Mods.Join(parentName).Join(path.RelativeTo(modFolder.Path)))));
- return archiveFiles
- .Where(kv => kv.Key.InFolder(parent))
- .Select(kv =>
- {
- var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, Mods.Join(parentName).Join(path.RelativeTo(parent)))
- );
- });
- })
- .ToArray();
+ }
- if (!modFiles.Any())
- yield break;
+ results.Add(new ModInstallerResult
+ {
+ Id = baseIdUsed ? ModId.New() : baseModId,
+ Files = files,
+ Name = node.InfoJson?.Name ?? ""
+ });
+ baseIdUsed = true;
+ }
- yield return new ModInstallerResult
- {
- Id = baseModId,
- Files = modFiles
- };
+ return results;
+ }
+
+ private static async Task ReadInfoJson(ModSourceFileEntry entry)
+ {
+ await using var stream = await entry.Open();
+ return await JsonSerializer.DeserializeAsync(stream);
}
+
+}
+
+internal class RedModInfo
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
}
diff --git a/src/Games/NexusMods.Games.RedEngine/ModInstallers/SimpleOverlayModInstaller.cs b/src/Games/NexusMods.Games.RedEngine/ModInstallers/SimpleOverlayModInstaller.cs
index 063539bdb8..85b0b1a30e 100644
--- a/src/Games/NexusMods.Games.RedEngine/ModInstallers/SimpleOverlayModInstaller.cs
+++ b/src/Games/NexusMods.Games.RedEngine/ModInstallers/SimpleOverlayModInstaller.cs
@@ -1,13 +1,16 @@
+using DynamicData;
using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.ArchiveContents;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
+using NexusMods.DataModel.Loadouts.ModFiles;
using NexusMods.DataModel.ModInstallers;
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.RedEngine.ModInstallers;
@@ -24,88 +27,49 @@ public class SimpleOverlayModInstaller : IModInstaller
.Select(x => x.ToRelativePath())
.ToArray();
-
- private static HashSet RootFolder(EntityDictionary files)
- {
- var filtered = files.Where(f => !Helpers.IgnoreExtensions.Contains(f.Key.Extension));
-
- var sets = filtered.Select(f => RootPaths.SelectMany(root => GetOffsets(f.Key, root)).ToHashSet())
- .Aggregate((set, x) =>
- {
- set.IntersectWith(x);
- return set;
- });
- return sets;
- }
-
- ///
- /// Returns the offsets of which subSection is a subfolder of basePath.
- ///
- ///
- ///
- ///
- private static IEnumerable GetOffsets(RelativePath basePath, RelativePath subSection)
- {
- var depth = 0;
- while (true)
- {
- if (basePath.Depth == 0) // root
- yield break;
-
- if (basePath.Depth < subSection.Depth)
- yield break;
-
- if (basePath == subSection)
- yield return depth;
-
- if (basePath.StartsWith(subSection))
- yield return depth;
-
- basePath = basePath.DropFirst();
- depth++;
- }
- }
-
- public ValueTask> GetModsAsync(
+ public async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var roots = RootFolder(archiveFiles);
+ var roots = RootPaths
+ .SelectMany(archiveFiles.FindSubPath)
+ .OrderBy(node => node.Depth)
+ .ToArray();
+
+ if (roots.Length == 0)
+ return Array.Empty();
- if (roots.Count == 0)
- yield break;
+ var highestRoot = roots.First();
+ var siblings= roots.Where(root => root.Depth == highestRoot.Depth)
+ .ToArray();
- var root = roots.First();
+ var newFiles = new List();
- var modFiles = archiveFiles
- .Where(kv => !Helpers.IgnoreExtensions.Contains(kv.Key.Extension))
- .Select(kv =>
+ foreach (var node in siblings)
+ {
+ foreach (var (filePath, fileInfo) in node.GetAllDescendentFiles())
{
- var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, path.DropFirst(root))
- );
- })
- .ToArray();
+ var relativePath = filePath.DropFirst(node.Path.Depth);
+ newFiles.Add(new FromArchive()
+ {
+ Id = ModFileId.New(),
+ Hash = fileInfo!.Hash,
+ Size = fileInfo.Size,
+ To = new GamePath(GameFolderType.Game, relativePath)
+ });
+ }
+ }
- if (!modFiles.Any())
- yield break;
+ if (!newFiles.Any())
+ return Array.Empty();
- yield return new ModInstallerResult
+ return new ModInstallerResult[]{ new()
{
Id = baseModId,
- Files = modFiles
- };
+ Files = newFiles
+ }};
}
}
diff --git a/src/Games/NexusMods.Games.RedEngine/Services.cs b/src/Games/NexusMods.Games.RedEngine/Services.cs
index 15cee5d8b5..d31c1c4698 100644
--- a/src/Games/NexusMods.Games.RedEngine/Services.cs
+++ b/src/Games/NexusMods.Games.RedEngine/Services.cs
@@ -1,10 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Games;
-using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.Games.RedEngine.FileAnalyzers;
using NexusMods.Games.RedEngine.ModInstallers;
namespace NexusMods.Games.RedEngine;
@@ -18,10 +15,8 @@ public static IServiceCollection AddRedEngineGames(this IServiceCollection servi
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton>();
services.AddSingleton();
- services.AddSingleton();
return services;
}
}
diff --git a/src/Games/NexusMods.Games.RedEngine/TypeFinder.cs b/src/Games/NexusMods.Games.RedEngine/TypeFinder.cs
deleted file mode 100644
index 0ddf6ec9dd..0000000000
--- a/src/Games/NexusMods.Games.RedEngine/TypeFinder.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
-using NexusMods.Games.RedEngine.FileAnalyzers;
-
-namespace NexusMods.Games.RedEngine;
-
-public class TypeFinder : ITypeFinder
-{
- public IEnumerable DescendentsOf(Type type)
- {
- return AllTypes.Where(t => t.IsAssignableTo(type));
- }
-
- private IEnumerable AllTypes => new[]
- {
- typeof(RedModInfo)
- };
-}
diff --git a/src/Games/NexusMods.Games.Reshade/ReshadePresetInstaller.cs b/src/Games/NexusMods.Games.Reshade/ReshadePresetInstaller.cs
index 7106ae146f..c7d4327507 100644
--- a/src/Games/NexusMods.Games.Reshade/ReshadePresetInstaller.cs
+++ b/src/Games/NexusMods.Games.Reshade/ReshadePresetInstaller.cs
@@ -1,20 +1,19 @@
using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
-using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.ModInstallers;
-using NexusMods.FileExtractor.FileSignatures;
-using NexusMods.Games.Generic.Entities;
-using NexusMods.Hashing.xxHash64;
+using NexusMods.Games.Generic.FileAnalyzers;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
+using NexusMods.Paths.Utilities;
namespace NexusMods.Games.Reshade;
-public class ReshadePresetInstaller : IModInstaller
+public class ReshadePresetInstaller : AModInstaller
{
+
+
private static readonly HashSet IgnoreFiles = new[]
{
"readme.txt",
@@ -24,69 +23,57 @@ public class ReshadePresetInstaller : IModInstaller
.Select(t => t.ToRelativePath())
.ToHashSet();
- public Priority GetPriority(GameInstallation installation, EntityDictionary archiveFiles)
+ private static Extension Ini = new(".ini");
+
+ public ReshadePresetInstaller(IServiceProvider serviceProvider) : base(serviceProvider) {}
+
+ public override async ValueTask> GetModsAsync(
+ GameInstallation gameInstallation,
+ ModId baseModId,
+ FileTreeNode archiveFiles,
+ CancellationToken cancellationToken = default)
{
- var filtered = archiveFiles
- .Where(f => !IgnoreFiles.Contains(f.Key.FileName))
- .ToList();
- // We have to be able to find the game's executable
- if (installation.Game is not AGame)
- return Priority.None;
+ var filtered = archiveFiles.GetAllDescendentFiles()
+ .Where(f => !IgnoreFiles.Contains(f.Path.FileName))
+ .ToList();
// We only support ini files for now
- if (!filtered.All(f => f.Value.FileTypes.Contains(FileType.INI)))
- return Priority.None;
+ if (filtered.Any(f => f.Path.Extension != Ini))
+ return NoResults;
// Get all the ini data
- var iniData = filtered
- .Select(f => f.Value.AnalysisData
- .OfType()
- .FirstOrDefault())
- .Where(d => d is not null)
- .Select(d => d!)
- .ToList();
+ var iniData = await filtered
+ .SelectAsync(async f => await IniAnalzyer.AnalyzeAsync(f.Value!.StreamFactory))
+ .ToListAsync(cancellationToken: cancellationToken);
// All the files must have ini data
if (iniData.Count != filtered.Count)
- return Priority.None;
+ return NoResults;
// All the files must have a section that ends with .fx marking them as likely a reshade preset
if (!iniData.All(f => f.Sections.All(x => x.EndsWith(".fx", StringComparison.CurrentCultureIgnoreCase))))
- return Priority.None;
-
- return Priority.Low;
- }
+ return NoResults;
- public ValueTask> GetModsAsync(
- GameInstallation gameInstallation,
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
- CancellationToken cancellationToken = default)
- {
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
-
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var modFiles = archiveFiles
- .Where(kv => !IgnoreFiles.Contains(kv.Key.FileName))
+ var modFiles = archiveFiles.GetAllDescendentFiles()
+ .Where(kv => !IgnoreFiles.Contains(kv.Name.FileName))
.Select(kv =>
{
var (path, file) = kv;
- return file.ToFromArchive(
+ return file!.ToFromArchive(
new GamePath(GameFolderType.Game, path.FileName)
);
- });
+ }).ToArray();
+
+ if (!modFiles.Any())
+ return NoResults;
- yield return new ModInstallerResult
+ return new [] { new ModInstallerResult
{
Id = baseModId,
Files = modFiles
- };
+ }};
}
+
+
}
diff --git a/src/Games/NexusMods.Games.Sifu/Sifu.cs b/src/Games/NexusMods.Games.Sifu/Sifu.cs
index bce10f406d..feb4b6e921 100644
--- a/src/Games/NexusMods.Games.Sifu/Sifu.cs
+++ b/src/Games/NexusMods.Games.Sifu/Sifu.cs
@@ -1,10 +1,12 @@
using NexusMods.DataModel.Games;
+using NexusMods.DataModel.ModInstallers;
using NexusMods.Paths;
namespace NexusMods.Games.Sifu;
public class Sifu : AGame, ISteamGame, IEpicGame
{
+ private readonly IServiceProvider _serviceProvider;
public IEnumerable SteamIds => new[] { 2138710u };
public IEnumerable EpicCatalogItemId => new[] { "c80a76de890145edbe0d41679dbccc66" };
@@ -15,9 +17,10 @@ public override GamePath GetPrimaryFile(GameStore store)
{
return new(GameFolderType.Game, "Sifu.exe");
}
-
- public Sifu(IEnumerable gameLocators) : base(gameLocators)
+
+ public Sifu(IEnumerable gameLocators, IServiceProvider serviceProvider) : base(gameLocators)
{
+ _serviceProvider = serviceProvider;
}
protected override IEnumerable> GetLocations(
@@ -27,4 +30,7 @@ protected override IEnumerable> GetLo
{
yield return new KeyValuePair(GameFolderType.Game, installation.Path);
}
+
+ ///
+ public override IEnumerable Installers => new[] { new SifuModInstaller(_serviceProvider) };
}
diff --git a/src/Games/NexusMods.Games.Sifu/SifuModInstaller.cs b/src/Games/NexusMods.Games.Sifu/SifuModInstaller.cs
index 455219a1b9..3a6b8e57ff 100644
--- a/src/Games/NexusMods.Games.Sifu/SifuModInstaller.cs
+++ b/src/Games/NexusMods.Games.Sifu/SifuModInstaller.cs
@@ -9,59 +9,48 @@
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
namespace NexusMods.Games.Sifu;
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
-public class SifuModInstaller : IModInstaller
+public class SifuModInstaller : AModInstaller
{
private static readonly Extension PakExt = new(".pak");
private static readonly RelativePath ModsPath = "Content/Paks/~mods".ToRelativePath();
- public Priority GetPriority(GameInstallation installation, EntityDictionary archiveFiles)
- {
- return installation.Game is Sifu && ContainsUEModFile(archiveFiles)
- ? Priority.Normal
- : Priority.None;
- }
-
- private static bool ContainsUEModFile(EntityDictionary files)
- {
- return files.Any(kv => kv.Key.Extension == PakExt);
- }
+ public SifuModInstaller(IServiceProvider serviceProvider) : base(serviceProvider) { }
- public ValueTask> GetModsAsync(
+ public override async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(baseModId, srcArchiveHash, archiveFiles));
- }
+ var pakFile = archiveFiles.GetAllDescendentFiles()
+ .FirstOrDefault(node => node.Path.Extension == PakExt);
- private IEnumerable GetMods(
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var pakPath = archiveFiles.Keys.First(filePath => filePath.FileName.Extension == PakExt).Parent;
+ if (pakFile == null)
+ return NoResults;
- var modFiles = archiveFiles
- .Where(kv => kv.Key.InFolder(pakPath))
+ var pakPath = pakFile.Parent;
+
+ var modFiles = pakPath.GetAllDescendentFiles()
.Select(kv =>
{
var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, ModsPath.Join(path.RelativeTo(pakPath)))
+ return file!.ToFromArchive(
+ new GamePath(GameFolderType.Game, ModsPath.Join(path.RelativeTo(pakPath.Path)))
);
});
- yield return new ModInstallerResult
+ return new [] { new ModInstallerResult
{
Id = baseModId,
- Files = modFiles
- };
+ Files = modFiles,
+ Name = pakPath.Name.FileName
+ }};
}
+
}
diff --git a/src/Games/NexusMods.Games.StardewValley/Analyzers/SMAPIManifestAnalyzer.cs b/src/Games/NexusMods.Games.StardewValley/Analyzers/SMAPIManifestAnalyzer.cs
deleted file mode 100644
index 995a3e47e8..0000000000
--- a/src/Games/NexusMods.Games.StardewValley/Analyzers/SMAPIManifestAnalyzer.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Text.Json;
-using Microsoft.Extensions.Logging;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.Abstractions.Ids;
-using NexusMods.FileExtractor.FileSignatures;
-using NexusMods.Games.StardewValley.Models;
-
-// ReSharper disable InconsistentNaming
-
-namespace NexusMods.Games.StardewValley.Analyzers;
-
-///
-/// for mods that use the Stardew Modding API (SMAPI).
-/// This looks for manifest.json files and returns .
-///
-public class SMAPIManifestAnalyzer : IFileAnalyzer
-{
- public FileAnalyzerId Id => FileAnalyzerId.New("f917e906-d28a-472d-b6e5-e7d2c61c60e4", 1);
-
- public IEnumerable FileTypes => new[] { FileType.JSON };
-
- private static readonly JsonSerializerOptions JsonSerializerOptions = new()
- {
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip,
- };
-
- private readonly ILogger _logger;
-
- public SMAPIManifestAnalyzer(ILogger logger)
- {
- _logger = logger;
- }
-
- public async IAsyncEnumerable AnalyzeAsync(FileAnalyzerInfo info, [EnumeratorCancellation] CancellationToken token = default)
- {
- if (!info.FileName.Equals("manifest.json", StringComparison.OrdinalIgnoreCase))
- yield break;
-
- SMAPIManifest? result = null;
- try
- {
- result = await JsonSerializer.DeserializeAsync(info.Stream, JsonSerializerOptions, cancellationToken: token);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Exception while deserializing SMAPIManifest at {RelativePath}", info.RelativePath);
- }
-
- if (result is null) yield break;
- yield return result;
- }
-}
diff --git a/src/Games/NexusMods.Games.StardewValley/Constants.cs b/src/Games/NexusMods.Games.StardewValley/Constants.cs
new file mode 100644
index 0000000000..d7fa77bf41
--- /dev/null
+++ b/src/Games/NexusMods.Games.StardewValley/Constants.cs
@@ -0,0 +1,10 @@
+using NexusMods.Paths;
+using NexusMods.Paths.Extensions;
+
+namespace NexusMods.Games.StardewValley;
+
+public class Constants
+{
+ public static readonly RelativePath ModsFolder = "Mods".ToRelativePath();
+ public static readonly RelativePath ManifestFile = "manifest.json".ToRelativePath();
+}
diff --git a/src/Games/NexusMods.Games.StardewValley/Emitters/001_MissingDependencies.cs b/src/Games/NexusMods.Games.StardewValley/Emitters/001_MissingDependencies.cs
index b61cf8d72d..5d8e13817b 100644
--- a/src/Games/NexusMods.Games.StardewValley/Emitters/001_MissingDependencies.cs
+++ b/src/Games/NexusMods.Games.StardewValley/Emitters/001_MissingDependencies.cs
@@ -1,6 +1,11 @@
+using System.Text.Json;
+using NexusMods.Common;
+using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Diagnostics;
using NexusMods.DataModel.Diagnostics.Emitters;
+using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
+using NexusMods.DataModel.Loadouts.ModFiles;
using NexusMods.DataModel.Loadouts.Mods;
using NexusMods.Games.StardewValley.Models;
@@ -8,14 +13,21 @@ namespace NexusMods.Games.StardewValley.Emitters;
public class MissingDependenciesEmitter : ILoadoutDiagnosticEmitter
{
- public IEnumerable Diagnose(Loadout loadout)
+ private readonly IArchiveManager _archiveManager;
+
+ public MissingDependenciesEmitter(IArchiveManager archiveManager)
+ {
+ _archiveManager = archiveManager;
+ }
+
+ public async IAsyncEnumerable Diagnose(Loadout loadout)
{
// TODO: check the versions
- var modIdToManifest = loadout.Mods
- .Select(kv => (Id: kv.Key, Manifest: GetManifest(kv.Value)))
+ var modIdToManifest = await loadout.Mods
+ .SelectAsync(async kv => (Id: kv.Key, Manifest: await GetManifest(kv.Value)))
.Where(tuple => tuple.Manifest is not null)
- .ToDictionary(x => x.Id, x => x.Manifest!);
+ .ToDictionaryAsync(x => x.Id, x => x.Manifest!);
var knownUniqueIds = modIdToManifest
.Select(x => x.Value.UniqueID)
@@ -54,16 +66,26 @@ private static IEnumerable GetRequiredDependencies(SMAPIManifest? manife
return requiredDependencies;
}
- private static SMAPIManifest? GetManifest(Mod mod)
+ private async ValueTask GetManifest(Mod mod)
{
- var manifest = mod.Files.Select(kv =>
- {
- var (_, file) = kv;
- var manifest = file.Metadata
- .OfType()
- .FirstOrDefault();
- return manifest;
- }).FirstOrDefault(x => x is not null);
+
+ var manifest = await mod.Files
+ .Values
+ .OfType()
+ .Where(f => f.To.FileName == Constants.ManifestFile)
+ .OfType()
+ .SelectAsync(async fa =>
+ {
+ try
+ {
+ await using var stream = await _archiveManager.GetFileStream(fa.Hash);
+ return await JsonSerializer.DeserializeAsync(stream);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }).FirstOrDefaultAsync(m => m != null);
return manifest;
}
}
diff --git a/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIInstaller.cs b/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIInstaller.cs
index 6a83d14301..69078b6637 100644
--- a/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIInstaller.cs
+++ b/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIInstaller.cs
@@ -4,6 +4,7 @@
using NexusMods.DataModel;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.ArchiveContents;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Loadouts.ModFiles;
@@ -12,6 +13,7 @@
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
@@ -22,7 +24,7 @@ namespace NexusMods.Games.StardewValley.Installers;
/// for SMAPI itself. This is different from ,
/// which is an implementation of for mods that use SMAPI.
///
-public class SMAPIInstaller : IModInstaller
+public class SMAPIInstaller : AModInstaller
{
private static readonly RelativePath InstallDatFile = "install.dat".ToRelativePath();
private static readonly RelativePath LinuxFolder = "linux".ToRelativePath();
@@ -31,80 +33,83 @@ public class SMAPIInstaller : IModInstaller
private readonly IOSInformation _osInformation;
private readonly FileHashCache _fileHashCache;
+ private readonly IDownloadRegistry _downloadRegistry;
- public SMAPIInstaller(IOSInformation osInformation, FileHashCache fileHashCache)
+ private SMAPIInstaller(IOSInformation osInformation, FileHashCache fileHashCache, IDownloadRegistry downloadRegistry, IServiceProvider serviceProvider)
+ : base(serviceProvider)
{
_osInformation = osInformation;
_fileHashCache = fileHashCache;
+ _downloadRegistry = downloadRegistry;
}
- private static KeyValuePair[] GetInstallDataFiles(EntityDictionary files)
+ private static FileTreeNode[] GetInstallDataFiles(FileTreeNode files)
{
- var installDataFiles = files.Where(kv =>
+ var installDataFiles = files.GetAllDescendentFiles()
+ .Where(kv =>
{
var (path, file) = kv;
var fileName = path.FileName;
var parent = path.Parent.FileName;
- return file.FileTypes.Contains(FileType.ZIP) &&
- fileName.Equals(InstallDatFile) &&
+ return fileName.Equals(InstallDatFile) &&
(parent.Equals(LinuxFolder) ||
parent.Equals(MacOSFolder) ||
parent.Equals(WindowsFolder));
- }).ToArray();
+ })
+ .ToArray();
return installDataFiles;
}
- public ValueTask> GetModsAsync(
+ public override async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
- {
- return ValueTask.FromResult(GetMods(gameInstallation, baseModId, srcArchiveHash, archiveFiles));
- }
-
- private IEnumerable GetMods(
- GameInstallation gameInstallation,
- ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
{
var modFiles = new List();
var installDataFiles = GetInstallDataFiles(archiveFiles);
- if (installDataFiles.Length != 3) throw new UnreachableException($"{nameof(SMAPIInstaller)} should guarantee that {nameof(GetInstallDataFiles)} returns 3 files when called from {nameof(GetModsAsync)} but it has {installDataFiles.Length} files instead!");
+ if (installDataFiles.Length != 3)
+ return NoResults;
var installDataFile = _osInformation.MatchPlatform(
state: ref installDataFiles,
- onWindows: (ref KeyValuePair[] dataFiles) => dataFiles.First(kv => kv.Key.Parent.FileName.Equals("windows")),
- onLinux: (ref KeyValuePair[] dataFiles) => dataFiles.First(kv => kv.Key.Parent.FileName.Equals("linux")),
- onOSX: (ref KeyValuePair[] dataFiles) => dataFiles.First(kv => kv.Key.Parent.FileName.Equals("macOS"))
+ onWindows: (ref FileTreeNode[] dataFiles) => dataFiles.First(kv => kv.Path.Parent.FileName.Equals("windows")),
+ onLinux: (ref FileTreeNode[] dataFiles) => dataFiles.First(kv => kv.Path.Parent.FileName.Equals("linux")),
+ onOSX: (ref FileTreeNode[] dataFiles) => dataFiles.First(kv => kv.Path.Parent.FileName.Equals("macOS"))
);
var (path, file) = installDataFile;
- if (file is not AnalyzedArchive archive)
- throw new UnreachableException($"{nameof(AnalyzedFile)} that has the file type {nameof(FileType.ZIP)} is not a {nameof(AnalyzedArchive)}");
+
+ var found = _downloadRegistry.GetByHash(file!.Hash).ToArray();
+ DownloadId downloadId;
+ if (!found.Any())
+ downloadId = await RegisterDataFile(path, file, cancellationToken);
+ else
+ {
+ downloadId = found.First();
+ }
+
var gameFolderPath = gameInstallation.Locations
.First(x => x.Key == GameFolderType.Game).Value;
- var archiveContents = archive.Contents;
+ var archiveContents = (await _downloadRegistry.Get(downloadId)).GetFileTree();
// TODO: install.dat is an archive inside an archive see https://github.com/Nexus-Mods/NexusMods.App/issues/244
// the basicFiles have to be extracted from the nested archive and put inside the game folder
// https://github.com/Pathoschild/SMAPI/blob/9763bc7484e29cbc9e7f37c61121d794e6720e75/src/SMAPI.Installer/InteractiveInstaller.cs#L380-L384
- var basicFiles = archiveContents
- .Where(kv => !kv.Key.Equals("unix-launcher.sh")).ToArray();
+ var basicFiles = archiveContents.GetAllDescendentFiles()
+ .Where(kv => !kv.Path.Equals("unix-launcher.sh")).ToArray();
if (_osInformation.IsLinux || _osInformation.IsOSX)
{
// TODO: Replace game launcher (StardewValley) with unix-launcher.sh by overwriting the game file
// https://github.com/Pathoschild/SMAPI/blob/9763bc7484e29cbc9e7f37c61121d794e6720e75/src/SMAPI.Installer/InteractiveInstaller.cs#L386-L417
- var modLauncherScriptFile = archiveContents
- .First(kv => kv.Key.FileName.Equals("unix-launcher.sh"));
+ var modLauncherScriptFile = archiveContents.GetAllDescendentFiles()
+ .First(kv => kv.Path.FileName.Equals("unix-launcher.sh"));
var gameLauncherScriptFilePath = gameFolderPath.Combine("StardewValley");
}
@@ -127,18 +132,25 @@ private IEnumerable GetMods(
});
// TODO: consider adding Name and Version
- yield return new ModInstallerResult
+ return new [] { new ModInstallerResult
{
Id = baseModId,
Files = modFiles
- };
+ }};
+ }
+
+ private async ValueTask RegisterDataFile(RelativePath filename, ModSourceFileEntry file, CancellationToken token)
+ {
+ return await _downloadRegistry.RegisterDownload(file.StreamFactory, new FilePathMetadata { OriginalName = filename.FileName, Quality = Quality.Low}, token);
}
public static SMAPIInstaller Create(IServiceProvider serviceProvider)
{
return new SMAPIInstaller(
serviceProvider.GetRequiredService(),
- serviceProvider.GetRequiredService()
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider
);
}
}
diff --git a/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIModInstaller.cs b/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIModInstaller.cs
index e56502ebe9..3256440f43 100644
--- a/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIModInstaller.cs
+++ b/src/Games/NexusMods.Games.StardewValley/Installers/SMAPIModInstaller.cs
@@ -1,15 +1,13 @@
-using System.Diagnostics;
+using System.Text.Json;
using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
-using NexusMods.DataModel.ArchiveContents;
-using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.ModInstallers;
using NexusMods.Games.StardewValley.Models;
-using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
+using NexusMods.Paths.FileTree;
+
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
@@ -18,62 +16,66 @@ namespace NexusMods.Games.StardewValley.Installers;
///
/// for mods that use the Stardew Modding API (SMAPI).
///
-public class SMAPIModInstaller : IModInstaller
+public class SMAPIModInstaller : AModInstaller
{
- private static readonly RelativePath ModsFolder = "Mods".ToRelativePath();
- private static readonly RelativePath ManifestFile = "manifest.json".ToRelativePath();
- private static IEnumerable> GetManifestFiles(
- EntityDictionary files)
+
+ ///
+ /// DI Constructor
+ ///
+ ///
+ private SMAPIModInstaller(IServiceProvider serviceProvider) : base(serviceProvider) { }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ ///
+ ///
+ public static SMAPIModInstaller Create(IServiceProvider serviceProvider) => new(serviceProvider);
+
+ private static IAsyncEnumerable<(FileTreeNode, SMAPIManifest)> GetManifestFiles(
+ FileTreeNode files)
{
- return files.Where(kv =>
+ return files.GetAllDescendentFiles()
+ .SelectAsync(async kv =>
{
var (path, file) = kv;
- if (!path.FileName.Equals(ManifestFile)) return false;
- var manifest = file.AnalysisData
- .OfType()
- .FirstOrDefault();
+ if (!path.FileName.Equals(Constants.ManifestFile))
+ return default;
+
+ await using var stream = await file!.Open();
- return manifest is not null;
- });
+ return (kv, await JsonSerializer.DeserializeAsync(stream));
+ })
+ .Where(manifest => manifest.Item2 != null)
+ .Select(m => (m.kv, m.Item2!));
}
- public ValueTask> GetModsAsync(
+ public override async ValueTask> GetModsAsync(
GameInstallation gameInstallation,
ModId baseModId,
- Hash srcArchiveHash,
- EntityDictionary archiveFiles,
+ FileTreeNode archiveFiles,
CancellationToken cancellationToken = default)
{
- return ValueTask.FromResult(GetMods(srcArchiveHash, archiveFiles));
- }
+ var manifestFiles = await GetManifestFiles(archiveFiles)
+ .ToArrayAsync(cancellationToken: cancellationToken);
- private IEnumerable GetMods(
- Hash srcArchiveHash,
- EntityDictionary archiveFiles)
- {
- var manifestFiles = GetManifestFiles(archiveFiles).ToArray();
if (!manifestFiles.Any())
- throw new UnreachableException($"{nameof(SMAPIModInstaller)} should guarantee that {nameof(GetModsAsync)} is never called for archives that don't have a SMAPI manifest file.");
+ return NoResults;
var mods = manifestFiles
- .Select(manifestFile =>
+ .Select(found =>
{
- var parent = manifestFile.Key.Parent;
- var manifest = manifestFile.Value.AnalysisData
- .OfType()
- .FirstOrDefault();
+ var (manifestFile, manifest) = found;
+ var parent = manifestFile.Parent;
- if (manifest is null) throw new UnreachableException();
-
- var modFiles = archiveFiles
- .Where(kv => kv.Key.InFolder(parent))
+ var modFiles = parent.GetAllDescendentFiles()
.Select(kv =>
{
var (path, file) = kv;
- return file.ToFromArchive(
- new GamePath(GameFolderType.Game, ModsFolder.Join(path.DropFirst(parent.Depth)))
+ return file!.ToFromArchive(
+ new GamePath(GameFolderType.Game, Constants.ModsFolder.Join(path.DropFirst(parent.Depth - 1)))
);
});
@@ -88,4 +90,5 @@ private IEnumerable GetMods(
return mods;
}
+
}
diff --git a/src/Games/NexusMods.Games.StardewValley/Models/SMAPIManifest.cs b/src/Games/NexusMods.Games.StardewValley/Models/SMAPIManifest.cs
index 4c9ee58a47..92bf6c1976 100644
--- a/src/Games/NexusMods.Games.StardewValley/Models/SMAPIManifest.cs
+++ b/src/Games/NexusMods.Games.StardewValley/Models/SMAPIManifest.cs
@@ -11,7 +11,7 @@ namespace NexusMods.Games.StardewValley.Models;
///
[PublicAPI]
[JsonName("NexusMods.Games.StardewValley.SMAPIManifest")]
-public record SMAPIManifest : IFileAnalysisData
+public record SMAPIManifest
{
///
/// The mod name.
diff --git a/src/Games/NexusMods.Games.StardewValley/NexusMods.Games.StardewValley.csproj b/src/Games/NexusMods.Games.StardewValley/NexusMods.Games.StardewValley.csproj
index f2a8211a49..9c34e8e91f 100644
--- a/src/Games/NexusMods.Games.StardewValley/NexusMods.Games.StardewValley.csproj
+++ b/src/Games/NexusMods.Games.StardewValley/NexusMods.Games.StardewValley.csproj
@@ -15,4 +15,5 @@
+
diff --git a/src/Games/NexusMods.Games.StardewValley/Services.cs b/src/Games/NexusMods.Games.StardewValley/Services.cs
index 480029b5e2..5aeaec175f 100644
--- a/src/Games/NexusMods.Games.StardewValley/Services.cs
+++ b/src/Games/NexusMods.Games.StardewValley/Services.cs
@@ -1,11 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Common;
-using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Diagnostics.Emitters;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.JsonConverters.ExpressionGenerator;
-using NexusMods.DataModel.ModInstallers;
-using NexusMods.Games.StardewValley.Analyzers;
using NexusMods.Games.StardewValley.Emitters;
using NexusMods.Games.StardewValley.Installers;
@@ -16,9 +13,6 @@ public static class Services
public static IServiceCollection AddStardewValley(this IServiceCollection services)
{
services.AddAllSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
.AddSingleton()
.AddSingleton();
return services;
diff --git a/src/Games/NexusMods.Games.StardewValley/StardewValley.cs b/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
index 0c665bbc71..dd070b8f56 100644
--- a/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
+++ b/src/Games/NexusMods.Games.StardewValley/StardewValley.cs
@@ -86,7 +86,7 @@ protected override IEnumerable> GetLo
public override IEnumerable Installers => new IModInstaller[]
{
SMAPIInstaller.Create(_serviceProvider),
- new SMAPIModInstaller()
+ SMAPIModInstaller.Create(_serviceProvider)
};
}
diff --git a/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs b/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs
index b784482f1a..1b583400ae 100644
--- a/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs
+++ b/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs
@@ -2,8 +2,10 @@
using NexusMods.Abstractions.CLI;
using NexusMods.Abstractions.CLI.DataOutputs;
using NexusMods.CLI;
+using NexusMods.Common;
using NexusMods.Common.GuidedInstaller;
using NexusMods.DataModel.Abstractions;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.Hashing.xxHash64;
@@ -36,14 +38,14 @@ public StressTest(ILogger logger,
LoadoutManager loadoutManager, Client client,
TemporaryFileManager temporaryFileManager,
IHttpDownloader downloader,
- IArchiveAnalyzer archiveAnalyzer,
IArchiveInstaller archiveInstaller,
+ IDownloadRegistry downloadRegistry,
IEnumerable gameLocators,
IGuidedInstaller optionSelector)
{
((CliGuidedInstaller)optionSelector).SkipAll = true;
- _archiveAnalyzer = archiveAnalyzer;
_archiveInstaller = archiveInstaller;
+ _downloadRegistry = downloadRegistry;
_downloader = downloader;
_loadoutManager = loadoutManager;
_logger = logger;
@@ -63,9 +65,9 @@ public StressTest(ILogger logger,
private readonly LoadoutManager _loadoutManager;
private readonly ILogger _logger;
- private readonly IArchiveAnalyzer _archiveAnalyzer;
private readonly IArchiveInstaller _archiveInstaller;
private readonly ManuallyAddedLocator _manualLocator;
+ private readonly IDownloadRegistry _downloadRegistry;
public async Task Run(IGame game, AbsolutePath output, CancellationToken token)
{
@@ -118,8 +120,9 @@ public async Task Run(IGame game, AbsolutePath output, CancellationToken to
var list = await _loadoutManager.ManageGameAsync(install,
indexGameFiles: false, token: cts.Token);
- var analysisData = await _archiveAnalyzer.AnalyzeFileAsync(tmpPath, token);
- await _archiveInstaller.AddMods(list.Value.LoadoutId, analysisData.Hash, token: token);
+ var downloadId = await _downloadRegistry.RegisterDownload(tmpPath, new FilePathMetadata
+ { OriginalName = tmpPath.Path.Name, Quality = Quality.Low }, token);
+ await _archiveInstaller.AddMods(list.Value.LoadoutId, downloadId, token: token);
results.Add((file.FileName, mod.ModId, file.FileId, hash, true, null));
_logger.LogInformation("Installed {ModId} {FileId} {FileName} - {Size}", mod.ModId, file.FileId,
diff --git a/src/Networking/NexusMods.Networking.Downloaders/DownloadService.cs b/src/Networking/NexusMods.Networking.Downloaders/DownloadService.cs
index 592abcde74..23825877e8 100644
--- a/src/Networking/NexusMods.Networking.Downloaders/DownloadService.cs
+++ b/src/Networking/NexusMods.Networking.Downloaders/DownloadService.cs
@@ -3,8 +3,11 @@
using DynamicData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using NexusMods.Common;
+using NexusMods.DataModel;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Abstractions.Ids;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.DataModel.RateLimiting;
using NexusMods.Hashing.xxHash64;
using NexusMods.Networking.Downloaders.Interfaces;
@@ -27,23 +30,23 @@ public class DownloadService : IDownloadService
private readonly ILogger _logger;
private readonly IServiceProvider _provider;
private readonly IDataStore _store;
- private readonly IArchiveAnalyzer _archiveAnalyzer;
private readonly Subject _started = new();
private readonly Subject _completed = new();
private readonly Subject _cancelled = new();
private readonly Subject _paused = new();
private readonly Subject _resumed = new();
- private readonly Subject<(IDownloadTask task, Hash analyzedHash, string modName)> _analyzed = new();
+ private readonly Subject<(IDownloadTask task, DownloadId analyzedHash, string modName)> _analyzed = new();
private readonly IObservable> _tasksChangeSet;
private readonly ReadOnlyObservableCollection _currentDownloads;
private bool _isDisposed = false;
+ private readonly IDownloadRegistry _downloadRegistry;
- public DownloadService(ILogger logger, IServiceProvider provider, IDataStore store, IArchiveAnalyzer archiveAnalyzer)
+ public DownloadService(ILogger logger, IServiceProvider provider, IDataStore store, IDownloadRegistry downloadRegistry)
{
_logger = logger;
_provider = provider;
_store = store;
- _archiveAnalyzer = archiveAnalyzer;
+ _downloadRegistry = downloadRegistry;
_tasks = new SourceList();
_tasksChangeSet = _tasks.Connect();
@@ -104,7 +107,7 @@ internal IEnumerable GetItemsToResume()
public IObservable ResumedTasks => _resumed;
///
- public IObservable<(IDownloadTask task, Hash analyzedHash, string modName)> AnalyzedArchives => _analyzed;
+ public IObservable<(IDownloadTask task, DownloadId downloadId, string modName)> AnalyzedArchives => _analyzed;
///
public Task AddNxmTask(NXMUrl url)
@@ -155,7 +158,7 @@ public void OnCancelled(IDownloadTask task)
}
///
- public void OnPaused(IDownloadTask task)
+ public void OnPaused(IDownloadTask task)
{
_paused.OnNext(task);
UpdatePersistedState(task);
@@ -165,9 +168,9 @@ public void OnPaused(IDownloadTask task)
public void OnResumed(IDownloadTask task) => _resumed.OnNext(task);
///
- public Size GetThroughput()
+ public Size GetThroughput()
{
- var provider = DateTimeProvider.Instance;
+ var provider = DateTimeProvider.Instance;
var totalThroughput = 0L;
foreach (var download in _currentDownloads)
totalThroughput += download.CalculateThroughput(provider);
@@ -185,8 +188,14 @@ public async Task FinalizeDownloadAsync(IDownloadTask task, TemporaryPath path,
try
{
- var analyzed = await _archiveAnalyzer.AnalyzeFileAsync(path);
- _analyzed.OnNext((task, analyzed.Hash, modName));
+ // TODO: Fix this
+ var downloadId = await _downloadRegistry.RegisterDownload(path.Path, new FilePathMetadata
+ {
+ Name = modName,
+ OriginalName = path.Path.FileName,
+ Quality = Quality.Low
+ });
+ _analyzed.OnNext((task, downloadId, modName));
}
catch (Exception e)
{
@@ -227,11 +236,11 @@ public void Dispose()
{
if (_isDisposed)
return;
-
+
// Pause all tasks, which persists them to datastore.
foreach (var task in _currentDownloads)
task.Suspend();
-
+
_isDisposed = true;
_tasks.Dispose();
_started.Dispose();
diff --git a/src/Networking/NexusMods.Networking.Downloaders/Interfaces/IDownloadService.cs b/src/Networking/NexusMods.Networking.Downloaders/Interfaces/IDownloadService.cs
index ac197759a6..6b691acce8 100644
--- a/src/Networking/NexusMods.Networking.Downloaders/Interfaces/IDownloadService.cs
+++ b/src/Networking/NexusMods.Networking.Downloaders/Interfaces/IDownloadService.cs
@@ -1,4 +1,5 @@
using DynamicData;
+using NexusMods.DataModel;
using NexusMods.DataModel.RateLimiting;
using NexusMods.Hashing.xxHash64;
using NexusMods.Networking.NexusWebApi.Types;
@@ -52,7 +53,7 @@ public interface IDownloadService : IDisposable
/// This gets fired whenever a download is complete and an archive has been analyzed.
/// You can use this callback to gather additional metadata about the archive, or install the mods within.
///
- IObservable<(IDownloadTask task, Hash analyzedHash, string modName)> AnalyzedArchives { get; }
+ IObservable<(IDownloadTask task, DownloadId downloadId, string modName)> AnalyzedArchives { get; }
///
/// Adds a task that will download from a NXM link.
diff --git a/src/Networking/NexusMods.Networking.HttpDownloader/AdvancedHttpDownloader.cs b/src/Networking/NexusMods.Networking.HttpDownloader/AdvancedHttpDownloader.cs
index 79b50c13b0..eb5e414d44 100644
--- a/src/Networking/NexusMods.Networking.HttpDownloader/AdvancedHttpDownloader.cs
+++ b/src/Networking/NexusMods.Networking.HttpDownloader/AdvancedHttpDownloader.cs
@@ -151,7 +151,7 @@ private static async Task FinalizeDownload(DownloadState state, Cancellati
state.StateFilePath.Delete();
File.Move(tempPath.ToString(), state.Destination.ToString(), true);
- return await state.Destination.XxHash64Async(cancel);
+ return await state.Destination.XxHash64Async(token: cancel);
}
#region Output Writing
diff --git a/src/NexusMods.App.UI/RightContent/LoadoutGrid/LoadoutGridViewModel.cs b/src/NexusMods.App.UI/RightContent/LoadoutGrid/LoadoutGridViewModel.cs
index 89fe738e60..b7895f9a55 100644
--- a/src/NexusMods.App.UI/RightContent/LoadoutGrid/LoadoutGridViewModel.cs
+++ b/src/NexusMods.App.UI/RightContent/LoadoutGrid/LoadoutGridViewModel.cs
@@ -11,7 +11,9 @@
using NexusMods.App.UI.RightContent.LoadoutGrid.Columns.ModInstalled;
using NexusMods.App.UI.RightContent.LoadoutGrid.Columns.ModName;
using NexusMods.App.UI.RightContent.LoadoutGrid.Columns.ModVersion;
+using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Loadouts.Cursors;
@@ -35,8 +37,8 @@ public class LoadoutGridViewModel : AViewModel, ILoadoutG
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly LoadoutRegistry _loadoutRegistry;
- private readonly IArchiveAnalyzer _archiveAnalyzer;
private readonly IArchiveInstaller _archiveInstaller;
+ private readonly IDownloadRegistry _downloadRegistry;
[Reactive]
public string LoadoutName { get; set; } = "";
@@ -48,14 +50,14 @@ public LoadoutGridViewModel(
IServiceProvider provider,
LoadoutRegistry loadoutRegistry,
IFileSystem fileSystem,
- IArchiveAnalyzer archiveAnalyzer,
- IArchiveInstaller archiveInstaller)
+ IArchiveInstaller archiveInstaller,
+ IDownloadRegistry downloadRegistry)
{
_logger = logger;
_fileSystem = fileSystem;
_loadoutRegistry = loadoutRegistry;
- _archiveAnalyzer = archiveAnalyzer;
_archiveInstaller = archiveInstaller;
+ _downloadRegistry = downloadRegistry;
_columns =
new SourceCache, LoadoutColumn>(
@@ -124,8 +126,14 @@ public Task AddMod(string path)
var _ = Task.Run(async () =>
{
- var analyzedFile = await _archiveAnalyzer.AnalyzeFileAsync(file, CancellationToken.None);
- await _archiveInstaller.AddMods(LoadoutId, analyzedFile.Hash, token: CancellationToken.None);
+ var downloadId = await _downloadRegistry.RegisterDownload(file,
+ new FilePathMetadata
+ {
+ OriginalName = file.FileName,
+ Quality = Quality.Low,
+ Name = file.FileName
+ });
+ await _archiveInstaller.AddMods(LoadoutId, downloadId, token: CancellationToken.None);
});
return Task.CompletedTask;
diff --git a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs
index 70a6ec14cf..2e69d55767 100644
--- a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs
+++ b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs
@@ -7,6 +7,7 @@
using NexusMods.App.UI.LeftMenu;
using NexusMods.App.UI.Overlays;
using NexusMods.App.UI.Overlays.MetricsOptIn;
+using NexusMods.DataModel;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Loadouts;
using NexusMods.Hashing.xxHash64;
@@ -60,7 +61,7 @@ public MainWindowViewModel(
downloadService.AnalyzedArchives.Subscribe(tuple =>
{
// Because HandleDownloadedAnalyzedArchive is an async task, it begins automatically.
- HandleDownloadedAnalyzedArchive(tuple.task, tuple.analyzedHash, tuple.modName).ContinueWith(t =>
+ HandleDownloadedAnalyzedArchive(tuple.task, tuple.downloadId, tuple.modName).ContinueWith(t =>
{
if (t.Exception != null)
logger.LogError(t.Exception, "Error while installing downloaded analyzed archive");
@@ -109,7 +110,7 @@ public MainWindowViewModel(
}
- private async Task HandleDownloadedAnalyzedArchive(IDownloadTask task, Hash analyzedHash, string modName)
+ private async Task HandleDownloadedAnalyzedArchive(IDownloadTask task, DownloadId downloadId, string modName)
{
var loadouts = Array.Empty();
if (task is IHaveGameDomain gameDomain)
@@ -123,9 +124,9 @@ private async Task HandleDownloadedAnalyzedArchive(IDownloadTask task, Hash anal
await Task.Run(async () =>
{
if (loadouts.Length > 0)
- await _archiveInstaller.AddMods(loadouts[0], analyzedHash, modName);
+ await _archiveInstaller.AddMods(loadouts[0], downloadId, modName);
else
- await _archiveInstaller.AddMods(_registry.AllLoadouts().First().LoadoutId, analyzedHash, modName);
+ await _archiveInstaller.AddMods(_registry.AllLoadouts().First().LoadoutId, downloadId, modName);
});
}
diff --git a/src/NexusMods.CLI/Services.cs b/src/NexusMods.CLI/Services.cs
index 2b4ed9894a..43fc0541ac 100644
--- a/src/NexusMods.CLI/Services.cs
+++ b/src/NexusMods.CLI/Services.cs
@@ -82,7 +82,6 @@ public static IServiceCollection AddCLI(this IServiceCollection services)
.AddVerb();
services.AddAllSingleton>(_ => new Resource("File Extraction"));
- services.AddAllSingleton>(_ => new Resource("File Analysis"));
return services;
}
diff --git a/src/NexusMods.CLI/Types/DownloadHandlers/NxmDownloadProtocolHandler.cs b/src/NexusMods.CLI/Types/DownloadHandlers/NxmDownloadProtocolHandler.cs
index eebf4caa54..0fc72ff3ac 100644
--- a/src/NexusMods.CLI/Types/DownloadHandlers/NxmDownloadProtocolHandler.cs
+++ b/src/NexusMods.CLI/Types/DownloadHandlers/NxmDownloadProtocolHandler.cs
@@ -1,4 +1,6 @@
-using NexusMods.DataModel.Abstractions;
+using NexusMods.Common;
+using NexusMods.DataModel.Abstractions;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.DataModel.Loadouts.Markers;
using NexusMods.Networking.HttpDownloader;
using NexusMods.Networking.NexusWebApi;
@@ -16,18 +18,20 @@ public class NxmDownloadProtocolHandler : IDownloadProtocolHandler
private readonly Client _client;
private readonly IHttpDownloader _downloader;
private readonly TemporaryFileManager _temp;
- private readonly IArchiveAnalyzer _archiveAnalyzer;
private readonly IArchiveInstaller _archiveInstaller;
+ private readonly IDownloadRegistry _downloaderRegistry;
///
- public NxmDownloadProtocolHandler(Client client,
- IHttpDownloader downloader, TemporaryFileManager temp,
- IArchiveInstaller archiveInstaller, IArchiveAnalyzer archiveAnalyzer)
+ public NxmDownloadProtocolHandler(Client client,
+ IHttpDownloader downloader,
+ TemporaryFileManager temp,
+ IArchiveInstaller archiveInstaller,
+ IDownloadRegistry downloadRegistry)
{
- _archiveAnalyzer = archiveAnalyzer;
_archiveInstaller = archiveInstaller;
_client = client;
_downloader = downloader;
+ _downloaderRegistry = downloadRegistry;
_temp = temp;
}
@@ -39,7 +43,7 @@ public async Task Handle(string url, LoadoutMarker loadout, string modName, Canc
{
await using var tempPath = _temp.CreateFile();
var parsed = NXMUrl.Parse(url);
-
+
// Note: Normally we should probably source domains from the loadout, but in this case, this is okay.
Response links;
if (parsed.Key == null)
@@ -48,9 +52,14 @@ public async Task Handle(string url, LoadoutMarker loadout, string modName, Canc
links = await _client.DownloadLinksAsync(parsed.Mod.Game, parsed.Mod.ModId, parsed.Mod.FileId, parsed.Key.Value, parsed.ExpireTime!.Value, token);
var downloadUris = links.Data.Select(u => new HttpRequestMessage(HttpMethod.Get, u.Uri)).ToArray();
-
+
await _downloader.DownloadAsync(downloadUris, tempPath, null, null, token);
- var hash = await _archiveAnalyzer.AnalyzeFileAsync(tempPath, token);
- await _archiveInstaller.AddMods(loadout.Value.LoadoutId, hash.Hash, modName, token:token);
+ var downloadId = await _downloaderRegistry.RegisterDownload(tempPath.Path, new FilePathMetadata
+ {
+ OriginalName = tempPath.Path.Name,
+ Quality = Quality.Low,
+ Name = tempPath.Path.Name
+ }, token);
+ await _archiveInstaller.AddMods(loadout.Value.LoadoutId, downloadId, modName, token:token);
}
}
diff --git a/src/NexusMods.CLI/Verbs/AnalyzeArchive.cs b/src/NexusMods.CLI/Verbs/AnalyzeArchive.cs
index b01d3a70ff..b4538e62c1 100644
--- a/src/NexusMods.CLI/Verbs/AnalyzeArchive.cs
+++ b/src/NexusMods.CLI/Verbs/AnalyzeArchive.cs
@@ -1,8 +1,10 @@
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.CLI;
using NexusMods.Abstractions.CLI.DataOutputs;
+using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.ArchiveContents;
+using NexusMods.DataModel.ArchiveMetaData;
using NexusMods.Paths;
namespace NexusMods.CLI.Verbs;
@@ -13,7 +15,8 @@ namespace NexusMods.CLI.Verbs;
///
public class AnalyzeArchive : AVerb, IRenderingVerb
{
- private readonly IArchiveAnalyzer _archiveContentsCache;
+ private readonly ILogger _logger;
+ private readonly IDownloadRegistry _downloadRegistry;
///
public IRenderer Renderer { get; set; } = null!;
@@ -21,12 +24,11 @@ public class AnalyzeArchive : AVerb, IRenderingVerb
///
/// DI constructor
///
- ///
///
- public AnalyzeArchive(IArchiveAnalyzer archiveContentsCache, ILogger logger)
+ public AnalyzeArchive(ILogger logger, IDownloadRegistry downloadRegistry)
{
_logger = logger;
- _archiveContentsCache = archiveContentsCache;
+ _downloadRegistry = downloadRegistry;
}
///
@@ -37,7 +39,6 @@ public AnalyzeArchive(IArchiveAnalyzer archiveContentsCache, ILogger("i", "inputFile", "File to Analyze")
});
- private readonly ILogger _logger;
///
public async Task Run(AbsolutePath inputFile, CancellationToken token)
@@ -46,19 +47,23 @@ public async Task Run(AbsolutePath inputFile, CancellationToken token)
{
var results = await Renderer.WithProgress(token, async () =>
{
- var file = await _archiveContentsCache.AnalyzeFileAsync(inputFile, token) as AnalyzedArchive;
- if (file == null) return Array.Empty