Skip to content

Commit

Permalink
Adapted for multimod support
Browse files Browse the repository at this point in the history
  • Loading branch information
Aragas committed Apr 25, 2023
1 parent 24ea880 commit 81ba458
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace NexusMods.Games.MountAndBlade2Bannerlord.Extensions;

public static class LoadoutExtensions
internal static class LoadoutExtensions
{
public static bool HasModuleInstalled(this Loadout loadout, string moduleId)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Diagnostics;
using Bannerlord.LauncherManager.Models;
using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
Expand All @@ -18,73 +19,101 @@ namespace NexusMods.Games.MountAndBlade2Bannerlord.Installers;

public sealed class MountAndBlade2BannerlordModInstaller : IModInstaller
{
private readonly LauncherManagerFactory _launcherManagerFactory;

public MountAndBlade2BannerlordModInstaller(LauncherManagerFactory launcherManagerFactory)
{
_launcherManagerFactory = launcherManagerFactory;
}

private readonly LauncherManagerFactory _launcherManagerFactory;

// TODO: We had in mind creating optional mod types (Framework, Gameplay, Assets, etc) that we potentially could map to priorities
public Priority Priority(GameInstallation installation, EntityDictionary<RelativePath, AnalyzedFile> files)
public Priority GetPriority(GameInstallation installation, EntityDictionary<RelativePath, AnalyzedFile> archiveFiles)
{
if (!installation.Is<MountAndBlade2Bannerlord>()) return Common.Priority.None;
if (!installation.Is<MountAndBlade2Bannerlord>()) return Priority.None;

var launcherManager = _launcherManagerFactory.Get(installation);
var result = launcherManager.TestModuleContent(files.Select(x => x.Key.ToString()).ToArray());
var result = launcherManager.TestModuleContent(archiveFiles.Select(x => x.Key.ToString()).ToArray());

return result.Supported
? Common.Priority.Normal
: Common.Priority.None;
return result.Supported ? Priority.Normal : Priority.None;
}

public ValueTask<IEnumerable<AModFile>> GetFilesToExtractAsync(GameInstallation installation, Hash srcArchive, EntityDictionary<RelativePath, AnalyzedFile> files, CancellationToken ct = default)
public ValueTask<IEnumerable<ModInstallerResult>> GetModsAsync(GameInstallation installation, ModId baseModId, Hash srcArchiveHash, EntityDictionary<RelativePath, AnalyzedFile> archiveFiles, CancellationToken ct = default)
{
var filesToExtract = new List<AModFile>();
static IEnumerable<KeyValuePair<RelativePath, AnalyzedFile>> GetModuleInfoFiles(EntityDictionary<RelativePath, AnalyzedFile> files)
{
return files.Where(kv =>
{
var (path, file) = kv;
var moduleInfos = files
.Select(x => x.Value.AnalysisData.OfType<MountAndBlade2BannerlordModuleInfo>().FirstOrDefault() is { } data ? new ModuleInfoExtendedWithPath(data.ModuleInfo, x.Key.Path) : null)
.OfType<ModuleInfoExtendedWithPath>()
.ToArray();
if (!path.FileName.Equals(MountAndBlade2BannerlordConstants.SubModuleFile)) return false;
return file.AnalysisData.OfType<MountAndBlade2BannerlordModuleInfo>().FirstOrDefault() is { } moduleInfo;
});
}

var launcherManager = _launcherManagerFactory.Get(installation);
// InstallModuleContent will only install mods if the ModuleInfoExtendedWithPath for a mod was provided
var result = launcherManager.InstallModuleContent(files.Select(x => x.Key.ToString()).ToArray(), moduleInfos);
filesToExtract.AddRange(result.Instructions.OfType<CopyInstallInstruction>().Select(instruction =>

var moduleInfoFiles = GetModuleInfoFiles(archiveFiles).ToArray();
var mods = moduleInfoFiles.Select(moduleInfoFile =>
{
var relativePath = instruction.Source.ToRelativePath();
var file = files[relativePath];
return new FromArchive
var parent = moduleInfoFile.Key.Parent;
var moduleInfo = moduleInfoFile.Value.AnalysisData.OfType<MountAndBlade2BannerlordModuleInfo>().FirstOrDefault()?.ModuleInfo;
if (moduleInfo is null) throw new UnreachableException();
var moduleInfoWithPath = new ModuleInfoExtendedWithPath(moduleInfo, moduleInfoFile.Key.Path);
// InstallModuleContent will only install mods if the ModuleInfoExtendedWithPath for a mod was provided
var result = launcherManager.InstallModuleContent(archiveFiles.Where(kv => kv.Key.InFolder(parent)).Select(x => x.Key.ToString()).ToArray(), new[] { moduleInfoWithPath });
var modFiles = result.Instructions.OfType<CopyInstallInstruction>().Select(instruction =>
{
Id = ModFileId.New(),
To = new GamePath(GameFolderType.Game, MountAndBlade2BannerlordConstants.ModFolder.Join(relativePath)),
From = new HashRelativePath(srcArchive, relativePath),
Hash = file.Hash,
Size = file.Size,
Metadata = ImmutableHashSet.CreateRange<IModFileMetadata>(new List<IModFileMetadata>
var relativePath = instruction.Source.ToRelativePath();
var file = archiveFiles[relativePath];
return new FromArchive
{
new ModuleIdMetadata { ModuleId = instruction.ModuleId },
new OriginalPathMetadata { OriginalRelativePath = relativePath.Path }
})
Id = ModFileId.New(),
To = new GamePath(GameFolderType.Game, MountAndBlade2BannerlordConstants.ModFolder.Join(relativePath)),
From = new HashRelativePath(srcArchiveHash, relativePath),
Hash = file.Hash,
Size = file.Size,
Metadata = ImmutableHashSet.CreateRange<IModFileMetadata>(new List<IModFileMetadata>
{
new ModuleIdMetadata { ModuleId = instruction.ModuleId },
new OriginalPathMetadata { OriginalRelativePath = relativePath.Path }
})
};
});
return new ModInstallerResult
{
Id = baseModId,
Files = modFiles,
Name = moduleInfo.Name,
Version = moduleInfo.Version.ToString()
};
}));

if (filesToExtract.Count == 0) // Not a valid Bannerlord Module - install in root folder the content
});
if (moduleInfoFiles.Length == 0) // Not a valid Bannerlord Module - install in root folder the content
{
filesToExtract.AddRange(files.Select(kv =>
var modFiles = archiveFiles.Select(kv =>
{
var (path, file) = kv;
return new FromArchive
{
Id = ModFileId.New(),
To = new GamePath(GameFolderType.Game, path),
From = new HashRelativePath(srcArchive, path),
From = new HashRelativePath(srcArchiveHash, path),
Hash = file.Hash,
Size = file.Size,
};
}));
});
mods = new List<ModInstallerResult>
{
new ModInstallerResult
{
Id = baseModId,
Files = modFiles
}
};
}

return ValueTask.FromResult<IEnumerable<AModFile>>(filesToExtract);
return ValueTask.FromResult(mods);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ public LauncherManagerFactory(ILoggerFactory loggerFactory)
_loggerFactory = loggerFactory;
}

internal LauncherManagerNexusMods Get(GameInstallation installation)
public LauncherManagerNexusMods Get(GameInstallation installation)
{
var store = Converter.ToGameStoreTW(installation.Store);
return _instances.GetOrAdd(installation.Locations[GameFolderType.Game].ToString(),
static (installationPath, tuple) => ValueFactory(tuple._loggerFactory, installationPath, tuple.store), (_loggerFactory, store));
}
internal LauncherManagerNexusMods Get(GameLocatorResult gameLocator)
public LauncherManagerNexusMods Get(GameLocatorResult gameLocator)
{
var store = Converter.ToGameStoreTW(gameLocator.Store);
return _instances.GetOrAdd(gameLocator.Path.ToString(),
static (installationPath, tuple) => ValueFactory(tuple._loggerFactory, installationPath, tuple.store), (_loggerFactory, store));
}

internal LauncherManagerNexusMods Get(string installationPath, GameStoreTW store)
public LauncherManagerNexusMods Get(string installationPath, GameStoreTW store)
{
return _instances.GetOrAdd(installationPath,
static (installationPath, tuple) => ValueFactory(tuple._loggerFactory, installationPath, tuple.store), (_loggerFactory, store));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace NexusMods.Games.MountAndBlade2Bannerlord.Services;

internal sealed partial class LauncherManagerNexusMods
partial class LauncherManagerNexusMods
{
public override string GetGameVersion()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace NexusMods.Games.MountAndBlade2Bannerlord.Services;

internal sealed partial class LauncherManagerNexusMods : LauncherManagerHandler
public sealed partial class LauncherManagerNexusMods : LauncherManagerHandler
{
private readonly ILogger _logger;
private readonly string _installationPath;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Immutable;
using Bannerlord.ModuleManager;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Sorting.Rules;

namespace NexusMods.Games.MountAndBlade2Bannerlord.Utils;

public static class SortRulesUtils
{
private static ModId GetModIdFromModuleId(string moduleId)
{
// No idea
return ModId.New();
}

public static ImmutableList<ISortRule<Mod, ModId>> GetSortRules(ModuleInfoExtended moduleInfo)
{
var sortRulesBuilder = ImmutableList.CreateBuilder<ISortRule<Mod, ModId>>();
sortRulesBuilder.AddRange(moduleInfo.DependenciesLoadBeforeThisDistinct().Select(x => new Before<Mod, ModId>(GetModIdFromModuleId(x.Id))));
sortRulesBuilder.AddRange(moduleInfo.DependenciesLoadBeforeThisDistinct().Select(x => new After<Mod, ModId>(GetModIdFromModuleId(x.Id))));
return sortRulesBuilder.ToImmutable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class MountAndBlade2BannerlordModInstallerTests : AModInstallerTest<Mount
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/BUTR/Bannerlord.XmlSchemas/master/SubModule.xsd" >
<Id value="Bannerlord.Harmony" />
<Name value="Harmony" />
<Version value="v2.3.0.0" />
<Version value="v2.2.0.0" />
<DefaultModule value="false" />
<ModuleCategory value="Singleplayer" />
<ModuleType value ="Community" />
Expand Down Expand Up @@ -62,13 +62,13 @@ public async Task Test_WithBLSE()

await using (file)
{
var filesToExtract = await GetFilesToExtractFromInstaller(file.Path);
filesToExtract.Should().HaveCount(11);
filesToExtract.Should().AllSatisfy(x => x.To.Path.StartsWith("bin"));
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Shared.dll");
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Standalone.exe");
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Launcher.exe");
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.LauncherEx.exe");
var (_, modFiles) = await GetModWithFilesFromInstaller(file.Path);
modFiles.Should().HaveCount(11);
modFiles.Should().AllSatisfy(x => x.To.Path.StartsWith("bin"));
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Shared.dll");
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Standalone.exe");
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.Launcher.exe");
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.BLSE.LauncherEx.exe");
}
}

Expand All @@ -82,14 +82,45 @@ public async Task Test_WithStandardMod()

await using (file)
{
var filesToExtract = await GetFilesToExtractFromInstaller(file.Path);
filesToExtract.Should().HaveCount(49);
filesToExtract.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
filesToExtract.Should().Contain(x => x.To.FileName == "SubModule.xml");
var (mod, modFiles) = await GetModWithFilesFromInstaller(file.Path);
mod.Name.Should().BeEquivalentTo("Harmony");
mod.Version.Should().BeEquivalentTo("v2.3.0.0");
modFiles.Should().HaveCount(49);
modFiles.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
modFiles.Should().Contain(x => x.To.FileName == "SubModule.xml");
}
}

[Fact]
[Trait("RequiresNetworking", "True")]
public async Task Test_WithStandardModMultiple()
{
// Calradia at War (Custom Spawns): CalradiaAtWar For Bannerlord v1.1.0 - v1.1.1 - v1.1.2 Main File (Version 1.9.5)
var (file, hash) = await DownloadMod(Game.Domain, ModId.From(411), FileId.From(34610));
hash.Should().Be(Hash.From(0x356140A133B4217E));

await using (file)
{
var modsWithFiles = await GetModsWithFilesFromInstaller(file.Path);
var (mod1, mod1Files) = modsWithFiles.FirstOrDefault(x => x.Key.Name == "Custom Spawns API");
var (mod2, mod2Files) = modsWithFiles.FirstOrDefault(x => x.Key.Name == "Calradia At War");

mod1.Name.Should().BeEquivalentTo("Custom Spawns API");
mod1.Version.Should().BeEquivalentTo("v1.9.5.0");
mod1Files.Should().HaveCount(8);
mod1Files.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/CustomSpawns"));
mod1Files.Should().Contain(x => x.To.FileName == "CustomSpawns.dll");
mod1Files.Should().Contain(x => x.To.FileName == "SubModule.xml");

mod2.Name.Should().BeEquivalentTo("Calradia At War");
mod2.Version.Should().BeEquivalentTo("v1.9.1.0");
mod2Files.Should().HaveCount(20);
mod2Files.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/CalradiaAtWar"));
mod2Files.Should().Contain(x => x.To.FileName == "SubModule.xml");
}
}

[Fact]
public async Task Test_WithFakeMod()
{
Expand All @@ -101,13 +132,15 @@ public async Task Test_WithFakeMod()
var file = await CreateTestArchive(testFiles);
await using (file)
{
var filesToExtract = await GetFilesToExtractFromInstaller(file.Path);
filesToExtract.Should().HaveCount(3);
filesToExtract.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
filesToExtract.Should().Contain(x => x.To.FileName == "SubModule.xml");
var (mod, modFiles) = await GetModWithFilesFromInstaller(file.Path);
mod.Name.Should().BeEquivalentTo("Harmony");
mod.Version.Should().BeEquivalentTo("v2.2.0.0");
modFiles.Should().HaveCount(3);
modFiles.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
modFiles.Should().Contain(x => x.To.FileName == "SubModule.xml");
}
}
}
[Fact]
public async Task Test_WithFakeMod_WithModulesRoot()
{
Expand All @@ -119,11 +152,13 @@ public async Task Test_WithFakeMod_WithModulesRoot()
var file = await CreateTestArchive(testFiles);
await using (file)
{
var filesToExtract = await GetFilesToExtractFromInstaller(file.Path);
filesToExtract.Should().HaveCount(3);
filesToExtract.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
filesToExtract.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
filesToExtract.Should().Contain(x => x.To.FileName == "SubModule.xml");
var (mod, modFiles) = await GetModWithFilesFromInstaller(file.Path);
mod.Name.Should().BeEquivalentTo("Harmony");
mod.Version.Should().BeEquivalentTo("v2.2.0.0");
modFiles.Should().HaveCount(3);
modFiles.Should().AllSatisfy(x => x.To.Path.StartsWith("Modules/Test"));
modFiles.Should().Contain(x => x.To.FileName == "Bannerlord.Harmony.dll");
modFiles.Should().Contain(x => x.To.FileName == "SubModule.xml");
}
}
}

0 comments on commit 81ba458

Please sign in to comment.