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()