diff --git a/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs b/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs
index f397306962..a7732f372d 100644
--- a/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs
+++ b/src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs
@@ -56,8 +56,9 @@ public RunGameTool(IServiceProvider serviceProvider, T game)
public string Name => $"Run {_game.Name}";
///
- public async Task Execute(Loadout.ReadOnly loadout, CancellationToken cancellationToken)
+ public virtual async Task Execute(Loadout.ReadOnly loadout, CancellationToken cancellationToken, string[]? commandLineArgs)
{
+ commandLineArgs ??= [];
_logger.LogInformation("Starting {Name}", Name);
var program = await GetGamePath(loadout);
@@ -69,10 +70,10 @@ public async Task Execute(Loadout.ReadOnly loadout, CancellationToken cancellati
switch (locator)
{
case SteamLocatorResultMetadata steamLocatorResultMetadata:
- await RunThroughSteam(steamLocatorResultMetadata.AppId, cancellationToken);
+ await RunThroughSteam(steamLocatorResultMetadata.AppId, cancellationToken, commandLineArgs);
return;
case HeroicGOGLocatorResultMetadata heroicGOGLocatorResultMetadata:
- await RunThroughHeroic("gog", heroicGOGLocatorResultMetadata.Id, cancellationToken);
+ await RunThroughHeroic("gog", heroicGOGLocatorResultMetadata.Id, cancellationToken, commandLineArgs);
return;
}
}
@@ -164,7 +165,7 @@ private async Task RunWithShell(CancellationToken cancellationToken, Ab
return process;
}
- private async Task RunThroughSteam(uint appId, CancellationToken cancellationToken)
+ private async Task RunThroughSteam(uint appId, CancellationToken cancellationToken, string[] commandLineArgs)
{
if (!OSInformation.Shared.IsLinux) throw OSInformation.Shared.CreatePlatformNotSupportedException();
@@ -174,8 +175,18 @@ private async Task RunThroughSteam(uint appId, CancellationToken cancellationTok
// the current starts, so we ignore every reaper process that already exists.
var existingReaperProcesses = Process.GetProcessesByName("reaper").Select(x => x.Id).ToHashSet();
+ // Build the Steam URL with optional command line arguments
// https://developer.valvesoftware.com/wiki/Steam_browser_protocol
- await _osInterop.OpenUrl(new Uri($"steam://rungameid/{appId.ToString(CultureInfo.InvariantCulture)}"), fireAndForget: true, cancellationToken: cancellationToken);
+ var steamUrl = $"steam://run/{appId.ToString(CultureInfo.InvariantCulture)}";
+ if (commandLineArgs is { Length: > 0 })
+ {
+ var encodedArgs = commandLineArgs
+ .Select(Uri.EscapeDataString)
+ .Aggregate((a, b) => $"{a} {b}");
+ steamUrl += $"//{encodedArgs}/";
+ }
+
+ await _osInterop.OpenUrl(new Uri(steamUrl), fireAndForget: true, cancellationToken: cancellationToken);
var steam = await WaitForProcessToStart("steam", timeout, existingProcesses: null, cancellationToken);
if (steam is null) return;
@@ -188,11 +199,15 @@ private async Task RunThroughSteam(uint appId, CancellationToken cancellationTok
await reaper.WaitForExitAsync(cancellationToken);
}
- private async Task RunThroughHeroic(string type, long appId, CancellationToken cancellationToken)
+ private async Task RunThroughHeroic(string type, long appId, CancellationToken cancellationToken, string[] commandLineArgs)
{
Debug.Assert(OSInformation.Shared.IsLinux);
// TODO: track process
+ if (commandLineArgs.Length > 0)
+ _logger.LogError("Heroic does not currently support command line arguments: https://github.com/Nexus-Mods/NexusMods.App/issues/2264 . " +
+ $"Args {string.Join(',', commandLineArgs)} were specified but will be ignored.");
+
await _osInterop.OpenUrl(new Uri($"heroic://launch/{type}/{appId.ToString(CultureInfo.InvariantCulture)}"), fireAndForget: true, cancellationToken: cancellationToken);
}
@@ -257,7 +272,7 @@ public IJobTask StartJob(Loadout.ReadOnly loadout, IJobMonitor moni
{
return monitor.Begin(this, async _ =>
{
- await Execute(loadout, cancellationToken);
+ await Execute(loadout, cancellationToken, []);
return Unit.Default;
});
}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Bannerlord.cs
similarity index 74%
rename from src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs
rename to src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Bannerlord.cs
index 4ba07296af..56491a7e4b 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2Bannerlord.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Bannerlord.cs
@@ -1,3 +1,5 @@
+using Bannerlord.ModuleManager;
+using FetchBannerlordVersion;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Diagnostics.Emitters;
using NexusMods.Abstractions.GameLocators;
@@ -18,7 +20,7 @@
using NexusMods.Games.MountAndBlade2Bannerlord.LauncherManager;
using NexusMods.Games.MountAndBlade2Bannerlord.Utils;
using NexusMods.Paths;
-using static NexusMods.Games.MountAndBlade2Bannerlord.MountAndBlade2BannerlordConstants;
+using static NexusMods.Games.MountAndBlade2Bannerlord.BannerlordConstants;
namespace NexusMods.Games.MountAndBlade2Bannerlord;
@@ -26,7 +28,7 @@ namespace NexusMods.Games.MountAndBlade2Bannerlord;
/// Maintained by the BUTR Team
/// https://github.com/BUTR
///
-public sealed class MountAndBlade2Bannerlord : AGame, ISteamGame, IGogGame, IEpicGame, IXboxGame
+public sealed class Bannerlord : AGame, ISteamGame, IGogGame, IEpicGame, IXboxGame
{
public static readonly GameId GameIdStatic = GameId.From(3174);
public static readonly GameDomain DomainStatic = GameDomain.From("mountandblade2bannerlord");
@@ -53,21 +55,22 @@ public sealed class MountAndBlade2Bannerlord : AGame, ISteamGame, IGogGame, IEpi
public IEnumerable XboxIds => ["TaleWorldsEntertainment.MountBladeIIBannerlord"];
public override IStreamFactory Icon =>
- new EmbededResourceStreamFactory("NexusMods.Games.MountAndBlade2Bannerlord.Resources.icon.jpg");
+ new EmbededResourceStreamFactory("NexusMods.Games.MountAndBlade2Bannerlord.Resources.icon.png");
public override IStreamFactory GameImage =>
- new EmbededResourceStreamFactory("NexusMods.Games.MountAndBlade2Bannerlord.Resources.game_image.jpg");
+ new EmbededResourceStreamFactory("NexusMods.Games.MountAndBlade2Bannerlord.Resources.game_image.jpg");
public override ILibraryItemInstaller[] LibraryItemInstallers =>
[
- _serviceProvider.GetRequiredService(),
+ _serviceProvider.GetRequiredService(),
];
public override IDiagnosticEmitter[] DiagnosticEmitters =>
[
- new MountAndBlade2BannerlordDiagnosticEmitter(_serviceProvider),
+ new BannerlordDiagnosticEmitter(_serviceProvider),
+ new MissingProtontricksEmitter(_serviceProvider),
];
- public MountAndBlade2Bannerlord(IServiceProvider serviceProvider, LauncherManagerFactory launcherManagerFactory) : base(serviceProvider)
+ public Bannerlord(IServiceProvider serviceProvider, LauncherManagerFactory launcherManagerFactory) : base(serviceProvider)
{
_serviceProvider = serviceProvider;
_launcherManagerFactory = launcherManagerFactory;
@@ -77,8 +80,11 @@ public MountAndBlade2Bannerlord(IServiceProvider serviceProvider, LauncherManage
protected override Version GetVersion(GameLocatorResult installation)
{
- var launcherManagerHandler = _launcherManagerFactory.Get(installation);
- return Version.TryParse(launcherManagerHandler.GetGameVersion(), out var val) ? val : new Version();
+ // Note(sewer): Bannerlord can use prefixes on versions etc. ,we want to strip them out
+ // so we sanitize/parse with `ApplicationVersion`.
+ var bannerlordVerStr = Fetcher.GetVersion(installation.Path.ToString(), "TaleWorlds.Library.dll");
+ var versionStr = ApplicationVersion.TryParse(bannerlordVerStr, out var av) ? $"{av.Major}.{av.Minor}.{av.Revision}.{av.ChangeSet}" : "0.0.0.0";
+ return Version.TryParse(versionStr, out var val) ? val : new Version();
}
protected override IReadOnlyDictionary GetLocations(IFileSystem fileSystem, GameLocatorResult installation)
@@ -94,7 +100,7 @@ protected override IReadOnlyDictionary GetLocations(IF
protected override ILoadoutSynchronizer MakeSynchronizer(IServiceProvider provider)
{
- return new MountAndBlade2BannerlordLoadoutSynchronizer(provider);
+ return new BannerlordLoadoutSynchronizer(provider);
}
public override List GetInstallDestinations(IReadOnlyDictionary locations)
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordConstants.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordConstants.cs
similarity index 88%
rename from src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordConstants.cs
rename to src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordConstants.cs
index 014281ac8f..c6cc2f9538 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordConstants.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordConstants.cs
@@ -4,7 +4,7 @@
namespace NexusMods.Games.MountAndBlade2Bannerlord;
-public static class MountAndBlade2BannerlordConstants
+public static class BannerlordConstants
{
public static readonly string DocumentsFolderName = "Mount and Blade II Bannerlord";
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordLoadoutSynchronizer.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordLoadoutSynchronizer.cs
similarity index 94%
rename from src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordLoadoutSynchronizer.cs
rename to src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordLoadoutSynchronizer.cs
index 87a2160466..8335cbd151 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordLoadoutSynchronizer.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordLoadoutSynchronizer.cs
@@ -7,11 +7,11 @@
using NexusMods.Games.MountAndBlade2Bannerlord.Models;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.Paths;
-using static NexusMods.Games.MountAndBlade2Bannerlord.MountAndBlade2BannerlordConstants;
+using static NexusMods.Games.MountAndBlade2Bannerlord.BannerlordConstants;
namespace NexusMods.Games.MountAndBlade2Bannerlord;
-public class MountAndBlade2BannerlordLoadoutSynchronizer : ALoadoutSynchronizer
+public class BannerlordLoadoutSynchronizer : ALoadoutSynchronizer
{
// Paths to known locations
private static GamePath GameGenegratedImGuiFile => new(LocationId.Game, "bin/Win64_Shipping_Client/imgui.ini");
@@ -79,13 +79,13 @@ public class MountAndBlade2BannerlordLoadoutSynchronizer : ALoadoutSynchronizer
"SubModule.xml", // Metadata about mods shipped with basegame.
};
- public MountAndBlade2BannerlordLoadoutSynchronizer(IServiceProvider provider) : base(provider)
+ public BannerlordLoadoutSynchronizer(IServiceProvider provider) : base(provider)
{
var settingsManager = provider.GetRequiredService();
- _settings = settingsManager.Get();
+ _settings = settingsManager.Get();
}
- private readonly MountAndBlade2BannerlordSettings _settings;
+ private readonly BannerlordSettings _settings;
public override bool IsIgnoredBackupPath(GamePath path)
{
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordRunGameTool.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordRunGameTool.cs
new file mode 100644
index 0000000000..eb58f3069f
--- /dev/null
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordRunGameTool.cs
@@ -0,0 +1,83 @@
+using Bannerlord.LauncherManager.Utils;
+using Bannerlord.ModuleManager;
+using CliWrap;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NexusMods.Abstractions.GameLocators;
+using NexusMods.Abstractions.Games;
+using NexusMods.Abstractions.Loadouts;
+using NexusMods.Games.Generic;
+using static Bannerlord.LauncherManager.Constants;
+namespace NexusMods.Games.MountAndBlade2Bannerlord;
+
+///
+/// This is to run the game or SMAPI using the shell, which allows them to start their own console,
+/// allowing users to interact with it.
+///
+public class BannerlordRunGameTool : RunGameTool
+{
+ private readonly ILogger _logger;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly GameToolRunner _runner;
+
+ public BannerlordRunGameTool(IServiceProvider serviceProvider, Bannerlord game, GameToolRunner runner)
+ : base(serviceProvider, game)
+ {
+ _serviceProvider = serviceProvider;
+ _runner = runner;
+ _logger = serviceProvider.GetRequiredService>();
+ }
+
+ protected override bool UseShell { get; set; } = false;
+
+ public override async Task Execute(Loadout.ReadOnly loadout, CancellationToken cancellationToken, string[]? commandLineArgs)
+ {
+ commandLineArgs ??= [];
+
+ // We need to 'inject' the current set of enabled modules in addition to any existing parameters.
+ // This way, external arguments specified by outside entities are preserved.
+ var args = await GetBannerlordExeCommandlineArgs(loadout, commandLineArgs, cancellationToken);
+ var install = loadout.InstallationInstance;
+ var exe = install.LocationsRegister[LocationId.Game];
+ if (install.Store != GameStore.XboxGamePass) { exe = exe/BinFolder/Win64Configuration/BannerlordExecutable; }
+ else { exe = exe/BinFolder/XboxConfiguration/BannerlordExecutable; }
+
+ var command = Cli.Wrap(exe.ToString())
+ .WithArguments(args)
+ .WithWorkingDirectory(exe.Parent.ToString());
+
+ // Note(sewer): We use the tool runner to execute an alternative process,
+ // but because the UI expects `RunGameTool`, we override the `RunGameTool's` executor
+ // with `GameToolRunner` implementation.
+ await _runner.ExecuteAsync(loadout, command, true, cancellationToken);
+ }
+
+ // TODO: Refactor Bannerlord.LauncherManager so we don't have to copy (some) of the commandline logic.
+ // (Since LauncherManager has dependencies on code we don't need/want to provide).
+ private async Task GetBannerlordExeCommandlineArgs(Loadout.ReadOnly loadout, string[] commandLineArgs, CancellationToken cancellationToken)
+ {
+ // Set the (automatic) load order.
+ // Copied from Bannerlord.LauncherManager
+ var manifestPipeline = Pipelines.GetManifestPipeline(_serviceProvider);
+ var modules = (await Helpers.GetAllManifestsAsync(_logger, loadout, manifestPipeline, cancellationToken).ToArrayAsync(cancellationToken))
+ .Select(x => x.Item2);
+ var sortedModules = AutoSort(Hack.GetDummyBaseGameModules()
+ .Concat(modules)).Select(x => x.Id).ToArray();
+ var loadOrderCli = sortedModules.Length > 0 ? $"_MODULES_*{string.Join("*", sortedModules)}*_MODULES_" : string.Empty;
+
+ // Add the new arguments
+ return commandLineArgs.Concat(["/singleplayer", loadOrderCli]).ToArray();
+ }
+
+ // Copied from Bannerlord.LauncherManager
+ // needs upstream changes, will do those changes tomorrow (21st Nov 2024)
+ private static IEnumerable AutoSort(IEnumerable source)
+ {
+ var orderedModules = source
+ .OrderByDescending(x => x.IsOfficial)
+ .ThenBy(x => x.Id, new AlphanumComparatorFast())
+ .ToArray();
+
+ return ModuleSorter.TopologySort(orderedModules, module => ModuleUtilities.GetDependencies(orderedModules, module));
+ }
+}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordSettings.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordSettings.cs
similarity index 86%
rename from src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordSettings.cs
rename to src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordSettings.cs
index d096479e6d..4a535fd7e3 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/MountAndBlade2BannerlordSettings.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/BannerlordSettings.cs
@@ -2,7 +2,7 @@
namespace NexusMods.Games.MountAndBlade2Bannerlord;
-public class MountAndBlade2BannerlordSettings : ISettings
+public class BannerlordSettings : ISettings
{
public bool DoFullGameBackup { get; set; } = false;
public bool BetaSorting { get; set; } = false;
@@ -10,10 +10,10 @@ public class MountAndBlade2BannerlordSettings : ISettings
public static ISettingsBuilder Configure(ISettingsBuilder settingsBuilder)
{
- return settingsBuilder.AddToUI(builder => builder
+ return settingsBuilder.AddToUI(builder => builder
.AddPropertyToUI(x => x.DoFullGameBackup, propertyBuilder => propertyBuilder
.AddToSection(Sections.Experimental)
- .WithDisplayName($"Full game backup: {MountAndBlade2Bannerlord.DisplayName}")
+ .WithDisplayName($"Full game backup: {Bannerlord.DisplayName}")
.WithDescription("Backup all game folders, this will greatly increase disk space usage. Should only be changed before managing the game.")
.UseBooleanContainer()
)
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MountAndBlade2BannerlordDiagnosticEmitter.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
similarity index 92%
rename from src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MountAndBlade2BannerlordDiagnosticEmitter.cs
rename to src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
index 4254a60777..d120ee8921 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MountAndBlade2BannerlordDiagnosticEmitter.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/BannerlordDiagnosticEmitter.cs
@@ -6,7 +6,6 @@
using NexusMods.Abstractions.Diagnostics.Emitters;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.Resources;
-using NexusMods.Games.MountAndBlade2Bannerlord.LauncherManager;
using NexusMods.Games.MountAndBlade2Bannerlord.Models;
namespace NexusMods.Games.MountAndBlade2Bannerlord.Diagnostics;
@@ -16,15 +15,14 @@ namespace NexusMods.Games.MountAndBlade2Bannerlord.Diagnostics;
///
/// Only caveat, is we get the current state from loadout, rather than from real disk.
///
-internal partial class MountAndBlade2BannerlordDiagnosticEmitter : ILoadoutDiagnosticEmitter
+internal partial class BannerlordDiagnosticEmitter : ILoadoutDiagnosticEmitter
{
private readonly IResourceLoader _manifestPipeline;
private readonly ILogger _logger;
- public MountAndBlade2BannerlordDiagnosticEmitter(IServiceProvider serviceProvider)
+ public BannerlordDiagnosticEmitter(IServiceProvider serviceProvider)
{
- serviceProvider.GetRequiredService();
- _logger = serviceProvider.GetRequiredService>();
+ _logger = serviceProvider.GetRequiredService>();
_manifestPipeline = Pipelines.GetManifestPipeline(serviceProvider);
}
@@ -42,9 +40,16 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, Can
isEnabledDict[module.Item2] = isEnabled;
}
- foreach (var moduleAndMod in modulesAndMods)
+ // TODO: HACK. Pretend base game modules are installed before we can properly ingest them.
+ foreach (var module in Hack.GetDummyBaseGameModules())
+ isEnabledDict[module] = true;
+ modulesOnly = modulesOnly.Concat(Hack.GetDummyBaseGameModules()).ToArray();
+ // TODO: HACK. Pretend base game modules are installed before we can properly ingest them.
+
+ // Emit diagnostics
+ foreach (var moduleAndMod in isEnabledDict)
{
- var (_, moduleInfo) = moduleAndMod;
+ var moduleInfo = moduleAndMod.Key;
// Note(sewer): All modules are valid by definition
// All modules are selected by definition.
foreach (var diagnostic in ModuleUtilities.ValidateModuleEx(modulesOnly, moduleInfo, module => isEnabledDict.ContainsKey(module), _ => true, false).Select(x => CreateDiagnostic(x)))
@@ -54,6 +59,8 @@ public async IAsyncEnumerable Diagnose(Loadout.ReadOnly loadout, Can
}
}
}
+
+
private Diagnostic? CreateDiagnostic(ModuleIssueV2 issue)
{
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/Diagnostics.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/Diagnostics.cs
index ce094f7f0e..469413a0eb 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/Diagnostics.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/Diagnostics.cs
@@ -1,5 +1,6 @@
using JetBrains.Annotations;
using NexusMods.Abstractions.Diagnostics;
+using NexusMods.Abstractions.Diagnostics.Values;
using NexusMods.Generators.Diagnostics;
namespace NexusMods.Games.MountAndBlade2Bannerlord.Diagnostics;
@@ -11,7 +12,7 @@ internal static partial class Diagnostics
internal static IDiagnosticTemplate MissingDependencyTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, 0))
- .WithTitle("'{ModName}' Is Missing Ddependency with ID '{DependencyId}'")
+ .WithTitle("'{ModName}' Is Missing Dependency with ID '{DependencyId}'")
.WithSeverity(DiagnosticSeverity.Critical)
.WithSummary("'{ModName}' requires mod with ID '{DependencyId}' which is not installed")
.WithDetails("""
@@ -199,9 +200,9 @@ 2. Contact the mod author about updating compatibility
.WithId(new DiagnosticId(Source, 4))
.WithTitle("'{ModName}' Is Missing Dependency with ID '{DependencyId}' and Version '{Version}'")
.WithSeverity(DiagnosticSeverity.Critical)
- .WithSummary("'{ModName}' requires mod with ID '{DependencyId}' and Version '{Version}' which is not installed")
+ .WithSummary("'{ModName}' requires mod with ID '{DependencyId}' and Version '{Version}' or higher which is not installed")
.WithDetails("""
-The mod `{ModName}` requires a mod with the ID `{DependencyId}` to function, but `{DependencyId}` with version `{Version}` is not installed.
+The mod `{ModName}` requires a mod with the ID `{DependencyId}` to function, but `{DependencyId}` with version `{Version}` (or higher) is not installed.
### How to Resolve
1. Download version `{Version}` of mod `{DependencyId}`
@@ -421,11 +422,12 @@ Contact the mod author to fix this configuration error.
internal static IDiagnosticTemplate CircularDependencyTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, 9))
- .WithTitle("'{ModName}' Has Circular Dependency with '{CircularDependencyName}'")
+ .WithTitle("Mods Are Stuck In a Loop: '{ModName}' and '{CircularDependencyName}'")
.WithSeverity(DiagnosticSeverity.Critical)
- .WithSummary("'{ModName}' and '{CircularDependencyName}' are circular dependencies")
+ .WithSummary("Mods Are Stuck In a Loop: '{ModName}' and '{CircularDependencyName}'")
.WithDetails("""
-The mods `{ModName}` and `{CircularDependencyName}` create a circular dependency chain where they depend on each other.
+`{ModName}` and `{CircularDependencyName}` are creating a `circular dependency`.
+This means each mod is waiting for the other to load first, causing a loop that stops both mods from working.
### How to Resolve
1. Contact the mod authors to resolve the circular dependency
@@ -810,4 +812,22 @@ All dependency entries must include an ID to properly identify the required mod.
.AddValue("ModName")
)
.Finish();
+
+ [DiagnosticTemplate]
+ [UsedImplicitly]
+ internal static IDiagnosticTemplate MissingProtontricksForRedMod = DiagnosticTemplateBuilder
+ .Start()
+ .WithId(new DiagnosticId(Source, number: 16))
+ .WithTitle("Missing 'Protontricks' dependency")
+ .WithSeverity(DiagnosticSeverity.Critical)
+ .WithSummary("Protontricks is required to run the game but is not present.")
+ .WithDetails("""
+Protontricks is required to run the game but is not present.
+
+Refer to the {ProtontricksUri} for installation instructions.
+""")
+ .WithMessageData(messageBuilder => messageBuilder
+ .AddValue("ProtontricksUri")
+ )
+ .Finish();
}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MissingProtontricksEmitter.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MissingProtontricksEmitter.cs
new file mode 100644
index 0000000000..c30a779f02
--- /dev/null
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Diagnostics/MissingProtontricksEmitter.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.DependencyInjection;
+using NexusMods.Abstractions.Diagnostics;
+using NexusMods.Abstractions.Diagnostics.Emitters;
+using NexusMods.Abstractions.Diagnostics.Values;
+using NexusMods.Abstractions.GameLocators;
+using NexusMods.Abstractions.Loadouts;
+using NexusMods.CrossPlatform.Process;
+using NexusMods.Paths;
+namespace NexusMods.Games.MountAndBlade2Bannerlord.Diagnostics;
+
+public partial class MissingProtontricksEmitter : ILoadoutDiagnosticEmitter
+{
+ public static readonly NamedLink ProtontricksLink = new("Protontricks Installation Guide", new Uri("https://github.com/Matoking/protontricks?tab=readme-ov-file#installation"));
+
+ ///
+ /// This will be null on non-Linux OSes.
+ ///
+ private ProtontricksDependency? _protontricksDependency;
+
+ ///
+ public MissingProtontricksEmitter(IServiceProvider serviceProvider) => _protontricksDependency = serviceProvider.GetService();
+
+ public async IAsyncEnumerable Diagnose(
+ Loadout.ReadOnly loadout,
+ CancellationToken cancellationToken)
+ {
+ if (!FileSystem.Shared.OS.IsLinux || _protontricksDependency == null)
+ yield break;
+
+ if (loadout.Installation.Store == GameStore.Steam)
+ {
+ var installInfo = await _protontricksDependency.QueryInstallationInformation(cancellationToken);
+ if (!installInfo.HasValue)
+ yield return Diagnostics.CreateMissingProtontricksForRedMod(ProtontricksLink);
+ }
+ }
+}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Hack.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Hack.cs
new file mode 100644
index 0000000000..204d53e316
--- /dev/null
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Hack.cs
@@ -0,0 +1,747 @@
+using System.Collections;
+using System.Xml;
+using Bannerlord.ModuleManager;
+namespace NexusMods.Games.MountAndBlade2Bannerlord;
+
+///
+/// Temporary code, to make game 'usable' while we wait for approval to make the game
+/// properly usable. Scheduled for DELETION.
+///
+public static class Hack
+{
+ public static IEnumerable GetDummyBaseGameModules()
+ {
+ // These are ordered as they are in launcher defaults.
+ // Do not reorder, I didn't fully stub the items above
+ yield return NativeModuleInfo;
+ yield return SandBoxCoreModuleInfo;
+ yield return CustomBattleModuleInfo;
+ yield return SandboxModuleInfo;
+ yield return StoryModeModuleInfo;
+ yield return BirthAndDeathModuleInfo;
+ }
+
+ private static ModuleInfoExtended FromXml(string xml)
+ {
+ var xmlDoc = new XmlDocument();
+ xmlDoc.LoadXml(xml);
+ return ModuleInfoExtended.FromXml(xmlDoc);
+ }
+
+ private static ModuleInfoExtended NativeModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+
+ private static ModuleInfoExtended SandBoxCoreModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+
+ private static ModuleInfoExtended CustomBattleModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+
+ private static ModuleInfoExtended SandboxModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+
+ private static ModuleInfoExtended StoryModeModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+
+ private static ModuleInfoExtended BirthAndDeathModuleInfo = FromXml(
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ );
+}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/BannerlordModInstaller.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/BannerlordModInstaller.cs
new file mode 100644
index 0000000000..d78ec671aa
--- /dev/null
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/BannerlordModInstaller.cs
@@ -0,0 +1,216 @@
+using System.Diagnostics;
+using System.Xml;
+using Bannerlord.LauncherManager.Models;
+using Bannerlord.ModuleManager;
+using DynamicData.Kernel;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NexusMods.Abstractions.GameLocators;
+using NexusMods.Abstractions.IO;
+using NexusMods.Abstractions.Library.Installers;
+using NexusMods.Abstractions.Library.Models;
+using NexusMods.Abstractions.Loadouts;
+using NexusMods.Games.MountAndBlade2Bannerlord.LauncherManager;
+using NexusMods.Games.MountAndBlade2Bannerlord.Models;
+using NexusMods.MnemonicDB.Abstractions;
+using NexusMods.Paths;
+using NexusMods.Paths.Extensions;
+using static Bannerlord.LauncherManager.Constants;
+using static NexusMods.Games.MountAndBlade2Bannerlord.BannerlordConstants;
+using GameStore = Bannerlord.LauncherManager.Models.GameStore;
+
+namespace NexusMods.Games.MountAndBlade2Bannerlord.Installers;
+
+public sealed class BannerlordModInstaller : ALibraryArchiveInstaller
+{
+ private readonly LauncherManagerFactory _launcherManagerFactory;
+ private readonly IFileStore _fileStore;
+
+ public BannerlordModInstaller(IServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService>())
+ {
+ _launcherManagerFactory = serviceProvider.GetRequiredService();
+ _fileStore = serviceProvider.GetRequiredService();
+ }
+
+ public override async ValueTask ExecuteAsync(LibraryArchive.ReadOnly libraryArchive, LoadoutItemGroup.New loadoutGroup, ITransaction transaction, Loadout.ReadOnly loadout, CancellationToken cancellationToken)
+ {
+ var moduleInfoFileTuples = await GetModuleInfoFiles(libraryArchive, cancellationToken);
+ if (moduleInfoFileTuples.Count == 0) return new NotSupported(); // TODO: Will it install in root folder the content?
+
+ var launcherManager = _launcherManagerFactory.Get(loadout.Installation);
+ var store = loadout.Installation.Store;
+ var isXboxStore = store == Abstractions.GameLocators.GameStore.XboxGamePass;
+ var isNonXboxStore = !isXboxStore;
+
+ foreach (var tuple in moduleInfoFileTuples)
+ {
+ var (moduleInfoFile, moduleInfo) = tuple;
+ var parent = moduleInfoFile.Path.Parent;
+
+ var moduleInfoWithPath = new ModuleInfoExtendedWithMetadata(moduleInfo, ModuleProviderType.Vortex, moduleInfoFile.Path);
+
+ var modGroup = new LoadoutItemGroup.New(transaction, out var modGroupEntityId)
+ {
+ IsGroup = true,
+ LoadoutItem = new LoadoutItem.New(transaction, modGroupEntityId)
+ {
+ Name = moduleInfo.Name,
+ LoadoutId = loadout,
+ ParentId = loadoutGroup,
+ },
+ };
+
+ var moduleInfoLoadoutItemId = Optional.None;
+
+ var moduleFiles = libraryArchive.Children.Where(x => x.Path.InFolder(parent)).Select(x => x.Path.ToString()).ToArray();
+ // InstallModuleContent will only install mods if the ModuleInfoExtendedWithPath for a mod was provided
+ var installResult = launcherManager.InstallModuleContent(moduleFiles, [moduleInfoWithPath]);
+ var filesToCopy = installResult.Instructions.OfType();
+ foreach (var instruction in filesToCopy)
+ {
+ var source = instruction.Source.ToRelativePath();
+ var destination = instruction.Destination.ToRelativePath();
+ moduleInfoLoadoutItemId = AddFileFromFilesToCopy(libraryArchive, transaction, loadout,
+ source, destination, modGroup, moduleInfoFile,
+ moduleInfoLoadoutItemId
+ );
+ }
+
+ var storeFilesToCopy = installResult.Instructions.OfType().ToArray();
+ var hasXboxFiles = storeFilesToCopy.Any(x => x.Store == GameStore.Xbox);
+ var hasNonXboxFiles = storeFilesToCopy.Any(x => x.Store != GameStore.Xbox);
+ var usedDestinations = new HashSet();
+
+ foreach (var instruction in storeFilesToCopy)
+ {
+ // Note(sewer) Alias Xbox store with Steam store files, in case mod author
+ // included files for only one version of the game.
+ // For more info, see `0004-MountAndBlade2Bannerlord.md`.
+ var source = instruction.Source.ToRelativePath();
+ var destination = instruction.Destination.ToRelativePath();
+
+ // If this mod has no files for Xbox store, and we're on Xbox store.
+ // InstallModuleContent emits multiple stores for `Win64_Shipping_Client`,
+ // so we want to avoid adding multiple times, hence the hashset.
+
+ // Alias non-Xbox files onto Xbox files, if only non-Xbox files exist.
+ if (!hasXboxFiles && isXboxStore && !usedDestinations.Contains(destination))
+ {
+ destination = destination.Path.Replace("Win64_Shipping_Client", "Gaming.Desktop.x64_Shipping_Client").ToRelativePath();
+ if (!usedDestinations.Contains(destination))
+ {
+ moduleInfoLoadoutItemId = AddFileFromFilesToCopy(libraryArchive, transaction, loadout,
+ source, destination, modGroup, moduleInfoFile,
+ moduleInfoLoadoutItemId
+ );
+ }
+ }
+
+ // Alias Xbox files onto non-Xbox files, if only Xbox files exist.
+ if (!hasNonXboxFiles && isNonXboxStore)
+ {
+ destination = destination.Path.Replace("Gaming.Desktop.x64_Shipping_Client", "Win64_Shipping_Client").ToRelativePath();
+ if (!usedDestinations.Contains(destination))
+ {
+ moduleInfoLoadoutItemId = AddFileFromFilesToCopy(libraryArchive, transaction, loadout,
+ source, destination, modGroup, moduleInfoFile,
+ moduleInfoLoadoutItemId
+ );
+ }
+ }
+
+ if (!usedDestinations.Contains(destination))
+ {
+ moduleInfoLoadoutItemId = AddFileFromFilesToCopy(libraryArchive, transaction, loadout,
+ source, destination, modGroup, moduleInfoFile,
+ moduleInfoLoadoutItemId
+ );
+ }
+
+ usedDestinations.Add(destination);
+ }
+
+ Debug.Assert(moduleInfoLoadoutItemId.HasValue);
+
+ _ = new BannerlordModuleLoadoutItem.New(transaction, modGroupEntityId)
+ {
+ ModuleInfoId = moduleInfoLoadoutItemId.Value,
+ LoadoutItemGroup = modGroup,
+ };
+ }
+
+ return new Success();
+ }
+
+ private static bool StoreEquals(GameStore bannerlordStore, Abstractions.GameLocators.GameStore nexusModsAppStore)
+ {
+ var isXbox = bannerlordStore == GameStore.Xbox && nexusModsAppStore == Abstractions.GameLocators.GameStore.XboxGamePass;
+ var isGog = bannerlordStore == GameStore.GOG && nexusModsAppStore == Abstractions.GameLocators.GameStore.GOG;
+ var isEpic = bannerlordStore == GameStore.Epic && nexusModsAppStore == Abstractions.GameLocators.GameStore.EGS;
+ var isSteam = bannerlordStore == GameStore.Steam && nexusModsAppStore == Abstractions.GameLocators.GameStore.Steam;
+ return isXbox || isGog || isEpic || isSteam;
+ }
+
+ private static Optional AddFileFromFilesToCopy(
+ LibraryArchive.ReadOnly libraryArchive, ITransaction transaction, Loadout.ReadOnly loadout, RelativePath source, RelativePath destination, LoadoutItemGroup.New modGroup, LibraryArchiveFileEntry.ReadOnly moduleInfoFile, Optional moduleInfoLoadoutItemId)
+ {
+ var fileEntry = libraryArchive.Children.First(x => x.Path.Equals(source));
+ var to = new GamePath(LocationId.Game, destination);
+
+ var loadoutFile = new LoadoutFile.New(transaction, out var entityId)
+ {
+ Hash = fileEntry.AsLibraryFile().Hash,
+ Size = fileEntry.AsLibraryFile().Size,
+ LoadoutItemWithTargetPath = new LoadoutItemWithTargetPath.New(transaction, entityId)
+ {
+ TargetPath = to.ToGamePathParentTuple(loadout.Id),
+ LoadoutItem = new LoadoutItem.New(transaction, entityId)
+ {
+ Name = fileEntry.AsLibraryFile().FileName,
+ LoadoutId = loadout,
+ ParentId = modGroup,
+ },
+ },
+ };
+
+ if (fileEntry.Id != moduleInfoFile.Id)
+ return moduleInfoLoadoutItemId;
+
+ moduleInfoLoadoutItemId = entityId;
+
+ _ = new ModuleInfoFileLoadoutFile.New(transaction, entityId)
+ {
+ IsModuleInfoFile = true,
+ LoadoutFile = loadoutFile,
+ };
+ return moduleInfoLoadoutItemId;
+ }
+
+ private async ValueTask>> GetModuleInfoFiles(
+ LibraryArchive.ReadOnly libraryArchive,
+ CancellationToken cancellationToken)
+ {
+ var results = new List<(LibraryArchiveFileEntry.ReadOnly, ModuleInfoExtended)>();
+
+ foreach (var fileEntry in libraryArchive.Children)
+ {
+ if (!fileEntry.Path.FileName.Equals(SubModuleFile)) continue;
+
+ try
+ {
+ await using var stream = await _fileStore.GetFileStream(fileEntry.AsLibraryFile().Hash, token: cancellationToken);
+ var doc = new XmlDocument();
+ doc.Load(stream);
+ var data = ModuleInfoExtended.FromXml(doc);
+
+ results.Add((fileEntry, data));
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, "Exception while deserializing {Path} from {Archive}", fileEntry.Path, fileEntry.Parent.AsLibraryFile().FileName);
+ }
+ }
+
+ return results;
+ }
+}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/MountAndBlade2BannerlordModInstaller.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/MountAndBlade2BannerlordModInstaller.cs
deleted file mode 100644
index 82c85c90f0..0000000000
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Installers/MountAndBlade2BannerlordModInstaller.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using System.Diagnostics;
-using System.Xml;
-using Bannerlord.LauncherManager.Models;
-using Bannerlord.ModuleManager;
-using DynamicData.Kernel;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using NexusMods.Abstractions.GameLocators;
-using NexusMods.Abstractions.IO;
-using NexusMods.Abstractions.Library.Installers;
-using NexusMods.Abstractions.Library.Models;
-using NexusMods.Abstractions.Loadouts;
-using NexusMods.Games.MountAndBlade2Bannerlord.LauncherManager;
-using NexusMods.Games.MountAndBlade2Bannerlord.Models;
-using NexusMods.MnemonicDB.Abstractions;
-using NexusMods.Paths.Extensions;
-using static NexusMods.Games.MountAndBlade2Bannerlord.MountAndBlade2BannerlordConstants;
-
-namespace NexusMods.Games.MountAndBlade2Bannerlord.Installers;
-
-public sealed class MountAndBlade2BannerlordModInstaller : ALibraryArchiveInstaller
-{
- private readonly LauncherManagerFactory _launcherManagerFactory;
- private readonly IFileStore _fileStore;
-
- public MountAndBlade2BannerlordModInstaller(IServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService>())
- {
- _launcherManagerFactory = serviceProvider.GetRequiredService();
- _fileStore = serviceProvider.GetRequiredService();
- }
-
- public override async ValueTask ExecuteAsync(LibraryArchive.ReadOnly libraryArchive, LoadoutItemGroup.New loadoutGroup, ITransaction transaction, Loadout.ReadOnly loadout, CancellationToken cancellationToken)
- {
- var moduleInfoFileTuples = await GetModuleInfoFiles(libraryArchive, cancellationToken);
- if (moduleInfoFileTuples.Count == 0) return new NotSupported(); // TODO: Will it install in root folder the content?
-
- var launcherManager = _launcherManagerFactory.Get(loadout.Installation);
-
- foreach (var tuple in moduleInfoFileTuples)
- {
- var (moduleInfoFile, moduleInfo) = tuple;
- var parent = moduleInfoFile.Path.Parent;
-
- var moduleInfoWithPath = new ModuleInfoExtendedWithMetadata(moduleInfo, ModuleProviderType.Vortex, moduleInfoFile.Path);
-
- var modGroup = new LoadoutItemGroup.New(transaction, out var modGroupEntityId)
- {
- IsGroup = true,
- LoadoutItem = new LoadoutItem.New(transaction, modGroupEntityId)
- {
- Name = moduleInfo.Name,
- LoadoutId = loadout,
- ParentId = loadoutGroup,
- },
- };
-
- var moduleInfoLoadoutItemId = Optional.None;
-
- var moduleFiles = libraryArchive.Children.Where(x => x.Path.InFolder(parent)).Select(x => x.Path.ToString()).ToArray();
- // InstallModuleContent will only install mods if the ModuleInfoExtendedWithPath for a mod was provided
- var installResult = launcherManager.InstallModuleContent(moduleFiles, [moduleInfoWithPath]);
- var filesToCopy = installResult.Instructions.OfType();
- foreach (var instruction in filesToCopy)
- {
- var fileRelativePath = instruction.Source.ToRelativePath();
- var fileEntry = libraryArchive.Children.First(x => x.Path.Equals(fileRelativePath));
-
- var to = new GamePath(LocationId.Game, instruction.Destination.ToRelativePath());
-
- var loadoutFile = new LoadoutFile.New(transaction, out var entityId)
- {
- Hash = fileEntry.AsLibraryFile().Hash,
- Size = fileEntry.AsLibraryFile().Size,
- LoadoutItemWithTargetPath = new LoadoutItemWithTargetPath.New(transaction, entityId)
- {
- TargetPath = to.ToGamePathParentTuple(loadout.Id),
- LoadoutItem = new LoadoutItem.New(transaction, entityId)
- {
- Name = fileEntry.AsLibraryFile().FileName,
- LoadoutId = loadout,
- ParentId = modGroup,
- },
- },
- };
-
- if (fileEntry.Id == moduleInfoFile.Id)
- {
- moduleInfoLoadoutItemId = entityId;
- _ = new ModuleInfoFileLoadoutFile.New(transaction, entityId)
- {
- IsModuleInfoFile = true,
- LoadoutFile = loadoutFile,
- };
- }
- }
-
- Debug.Assert(moduleInfoLoadoutItemId.HasValue);
-
- _ = new BannerlordModuleLoadoutItem.New(transaction, modGroupEntityId)
- {
- ModuleInfoId = moduleInfoLoadoutItemId.Value,
- LoadoutItemGroup = modGroup,
- };
- }
-
- return new Success();
- }
-
- private async ValueTask>> GetModuleInfoFiles(
- LibraryArchive.ReadOnly libraryArchive,
- CancellationToken cancellationToken)
- {
- var results = new List<(LibraryArchiveFileEntry.ReadOnly, ModuleInfoExtended)>();
-
- foreach (var fileEntry in libraryArchive.Children)
- {
- if (!fileEntry.Path.FileName.Equals(SubModuleFile)) continue;
-
- try
- {
- await using var stream = await _fileStore.GetFileStream(fileEntry.AsLibraryFile().Hash, token: cancellationToken);
- var doc = new XmlDocument();
- doc.Load(stream);
- var data = ModuleInfoExtended.FromXml(doc);
-
- results.Add((fileEntry, data));
- }
- catch (Exception e)
- {
- Logger.LogError(e, "Exception while deserializing {Path} from {Archive}", fileEntry.Path, fileEntry.Parent.AsLibraryFile().FileName);
- }
- }
-
- return results;
- }
-}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerFactory.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerFactory.cs
index cd3fe25bc1..86de7031be 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerFactory.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerFactory.cs
@@ -43,13 +43,13 @@ public LauncherManagerNexusModsApp Get(GameLocatorResult gameLocator)
static (installationPath, tuple) => ValueFactory(tuple._serviceProvider, installationPath, tuple.store), (_serviceProvider, store));
}
- public LauncherManagerNexusModsApp Get(string installationPath, Bannerlord.LauncherManager.Models.GameStore store)
+ public LauncherManagerNexusModsApp Get(string installationPath, global::Bannerlord.LauncherManager.Models.GameStore store)
{
return _instances.GetOrAdd(installationPath,
static (installationPath, tuple) => ValueFactory(tuple._serviceProvider, installationPath, tuple.store), (_serviceProvider, store));
}
- private static LauncherManagerNexusModsApp ValueFactory(IServiceProvider serviceProvider, string installationPath, Bannerlord.LauncherManager.Models.GameStore store)
+ private static LauncherManagerNexusModsApp ValueFactory(IServiceProvider serviceProvider, string installationPath, global::Bannerlord.LauncherManager.Models.GameStore store)
{
return new LauncherManagerNexusModsApp(serviceProvider, installationPath, store);
}
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerNexusModsApp.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerNexusModsApp.cs
index b4634c27bf..94085b2e3d 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerNexusModsApp.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/LauncherManager/LauncherManagerNexusModsApp.cs
@@ -20,17 +20,17 @@ public sealed partial class LauncherManagerNexusModsApp : LauncherManagerHandler
ILoadOrderStateProvider
{
private readonly ILogger _logger;
- private readonly MountAndBlade2BannerlordSettings _settings;
+ private readonly BannerlordSettings _settings;
private readonly string _installationPath;
- public string ExecutableParameters { get; private set; } = string.Empty;
+ public string[] ExecutableParameters { get; private set; } = [];
public LauncherManagerNexusModsApp(IServiceProvider serviceProvider, string installationPath, GameStore store)
{
_logger = serviceProvider.GetRequiredService>();
var settingsManager = serviceProvider.GetRequiredService();
- _settings = settingsManager.Get();
+ _settings = settingsManager.Get();
_installationPath = installationPath;
@@ -56,7 +56,7 @@ public LauncherManagerNexusModsApp(IServiceProvider serviceProvider, string inst
///
/// The game executable.
/// List of commandline arguments.
- public void SetGameParameters(string executable, IReadOnlyList gameParameters) => ExecutableParameters = string.Join(" ", gameParameters);
+ public void SetGameParameters(string executable, IReadOnlyList gameParameters) => ExecutableParameters = gameParameters.ToArray();
///
/// Allows us to modify the settings for the vanilla Bannerlord Launcher.
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/NexusMods.Games.MountAndBlade2Bannerlord.csproj b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/NexusMods.Games.MountAndBlade2Bannerlord.csproj
index 57c603f715..ff13634203 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/NexusMods.Games.MountAndBlade2Bannerlord.csproj
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/NexusMods.Games.MountAndBlade2Bannerlord.csproj
@@ -3,8 +3,8 @@
-
-
+
+
@@ -30,6 +30,7 @@
+
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.jpg b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.jpg
deleted file mode 100644
index 74fdc6e61a..0000000000
Binary files a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.jpg and /dev/null differ
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.png b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.png
new file mode 100644
index 0000000000..726de93a6f
Binary files /dev/null and b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Resources/icon.png differ
diff --git a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Services.cs b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Services.cs
index 3083c810c8..fdf94c5e7f 100644
--- a/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Services.cs
+++ b/src/Games/NexusMods.Games.MountAndBlade2Bannerlord/Services.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Games;
+using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.Settings;
using NexusMods.Games.MountAndBlade2Bannerlord.Installers;
using NexusMods.Games.MountAndBlade2Bannerlord.LauncherManager;
@@ -12,10 +13,11 @@ public static class Services
public static IServiceCollection AddMountAndBlade2Bannerlord(this IServiceCollection services)
{
return services
- .AddGame()
+ .AddGame()
+ .AddSingleton()
// Installers
- .AddSingleton()
+ .AddSingleton()
// Diagnostics
@@ -24,7 +26,7 @@ public static IServiceCollection AddMountAndBlade2Bannerlord(this IServiceCollec
.AddModuleInfoFileLoadoutFileModel()
// Misc
- .AddSettings()
+ .AddSettings()
.AddSingleton()
.AddSingleton()
.AddSingleton()