From 47369851f21a121eee4b9b00294cd6861efb007c Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Wed, 11 Feb 2026 11:56:28 -0800 Subject: [PATCH] Configuration scoped modlist (#1423) (Closes #1225) * Mod selection for each launch configuration (Closes #1225) * Store enabled mods with the launched application --- .../Options/DaybreakApiOptions.cs | 13 ++ .../Configuration/Options/FocusViewOptions.cs | 4 - .../Configuration/ProjectConfiguration.cs | 1 + Daybreak.Core/Models/ModListEntry.cs | 1 + .../Services/Api/DaybreakApiService.cs | 9 +- .../ApplicationLauncher.cs | 40 ++-- .../Services/DirectSong/DirectSongService.cs | 1 + .../Guildwars/GuildWarsVersionChecker.cs | 1 + .../LaunchConfigurationService.cs | 10 +- .../Services/ReShade/ReShadeService.cs | 1 + .../Services/Toolbox/ToolboxService.cs | 1 + Daybreak.Core/Services/UMod/UModService.cs | 1 + .../Views/LaunchConfigurationsView.razor | 215 +++++++++--------- .../Views/LaunchConfigurationsView.razor.cs | 17 +- .../Views/LaunchConfigurationsView.razor.css | 24 +- Daybreak.Core/Views/LaunchView.razor.cs | 26 +-- Daybreak.Core/Views/ModsView.razor | 133 ++++++----- Daybreak.Core/Views/ModsView.razor.cs | 58 ++++- Daybreak.Core/Views/ModsView.razor.css | 3 +- Daybreak.Core/wwwroot/css/site.css | 7 + .../Services/Wine/WinePrefixManager.cs | 3 + .../GuildWarsApplicationLaunchContext.cs | 7 + .../LaunchConfiguration.cs | 6 + .../LaunchConfigurationWithCredentials.cs | 7 +- Daybreak.Shared/Services/Mods/IModService.cs | 5 + .../Services/Screens/GuildwarsScreenPlacer.cs | 1 + .../Services/SimpleNotificationMod.cs | 1 + 27 files changed, 365 insertions(+), 231 deletions(-) create mode 100644 Daybreak.Core/Configuration/Options/DaybreakApiOptions.cs diff --git a/Daybreak.Core/Configuration/Options/DaybreakApiOptions.cs b/Daybreak.Core/Configuration/Options/DaybreakApiOptions.cs new file mode 100644 index 00000000..c86e740c --- /dev/null +++ b/Daybreak.Core/Configuration/Options/DaybreakApiOptions.cs @@ -0,0 +1,13 @@ +using Daybreak.Shared.Attributes; +using System.Text.Json.Serialization; + +namespace Daybreak.Configuration.Options; + +[OptionsName(Name = "Daybreak API")] +[OptionsIgnore] +public sealed class DaybreakApiOptions +{ + [JsonPropertyName(nameof(Enabled))] + [OptionName(Name = "Enabled", Description = "If true, the Daybreak API is injected into Guild Wars, enabling extended functionality such as loading builds, character switching, and the focus view")] + public bool Enabled { get; set; } = false; +} diff --git a/Daybreak.Core/Configuration/Options/FocusViewOptions.cs b/Daybreak.Core/Configuration/Options/FocusViewOptions.cs index d69b493f..7a86ce2a 100644 --- a/Daybreak.Core/Configuration/Options/FocusViewOptions.cs +++ b/Daybreak.Core/Configuration/Options/FocusViewOptions.cs @@ -8,10 +8,6 @@ namespace Daybreak.Configuration.Options; [OptionsIgnore] public sealed class FocusViewOptions { - [JsonPropertyName(nameof(Enabled))] - [OptionName(Name = "Enabled", Description = "If true, the focus view is enabled, showing live information from the game")] - public bool Enabled { get; set; } = false; - [JsonPropertyName(nameof(ExperienceDisplay))] [OptionName(Name = "Experience Display Mode", Description = "Sets how should the experience display show the information")] public ExperienceDisplay ExperienceDisplay { get; set; } diff --git a/Daybreak.Core/Configuration/ProjectConfiguration.cs b/Daybreak.Core/Configuration/ProjectConfiguration.cs index a14f1325..83c76274 100644 --- a/Daybreak.Core/Configuration/ProjectConfiguration.cs +++ b/Daybreak.Core/Configuration/ProjectConfiguration.cs @@ -334,6 +334,7 @@ public class ProjectConfiguration : PluginConfigurationBase optionsProducer.RegisterOptions(); optionsProducer.RegisterOptions(); + optionsProducer.RegisterOptions(); optionsProducer.RegisterOptions(); optionsProducer.RegisterOptions(); diff --git a/Daybreak.Core/Models/ModListEntry.cs b/Daybreak.Core/Models/ModListEntry.cs index fc8def6a..6dc91b67 100644 --- a/Daybreak.Core/Models/ModListEntry.cs +++ b/Daybreak.Core/Models/ModListEntry.cs @@ -10,6 +10,7 @@ public sealed class ModListEntry public required bool IsVisible { get; init; } public required bool CanManage { get; init; } public required bool CanUninstall { get; init; } + public required bool CanDisable { get; init; } public bool IsEnabled { get; set; } public bool IsInstalled { get; set; } diff --git a/Daybreak.Core/Services/Api/DaybreakApiService.cs b/Daybreak.Core/Services/Api/DaybreakApiService.cs index 1601565d..7bb90ed2 100644 --- a/Daybreak.Core/Services/Api/DaybreakApiService.cs +++ b/Daybreak.Core/Services/Api/DaybreakApiService.cs @@ -24,7 +24,7 @@ public sealed class DaybreakApiService( IStubInjector stubInjector, INotificationService notificationService, IHttpClient scopedApiClient, - IOptionsMonitor liveUpdateableOptions, + IOptionsMonitor daybreakApiOptions, ILogger logger, ILogger scopedApiLogger) : IDaybreakApiService @@ -42,7 +42,7 @@ public sealed class DaybreakApiService( private readonly IStubInjector stubInjector = stubInjector.ThrowIfNull(); private readonly INotificationService notificationService = notificationService.ThrowIfNull(); private readonly IHttpClient scopedApiClient = scopedApiClient.ThrowIfNull(); - private readonly IOptionsMonitor liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull(); + private readonly IOptionsMonitor daybreakApiOptions = daybreakApiOptions.ThrowIfNull(); private readonly ILogger logger = logger.ThrowIfNull(); private readonly ILogger scopedApiLogger = scopedApiLogger.ThrowIfNull(); @@ -52,10 +52,10 @@ public sealed class DaybreakApiService( public bool IsEnabled { - get => this.liveUpdateableOptions.CurrentValue.Enabled; + get => this.daybreakApiOptions.CurrentValue.Enabled; set { - var options = this.liveUpdateableOptions.CurrentValue; + var options = this.daybreakApiOptions.CurrentValue; options.Enabled = value; this.optionsProvider.SaveOption(options); } @@ -65,6 +65,7 @@ public sealed class DaybreakApiService( public bool IsVisible => true; public bool CanCustomManage => false; public bool CanUninstall => false; + public bool CanDisable => true; public IProgressAsyncOperation PerformUninstallation(CancellationToken cancellationToken) { diff --git a/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs index 578e063d..243ff161 100644 --- a/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -17,12 +17,10 @@ using Daybreak.Shared.Services.Privilege; using Daybreak.Views; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Photino.NET; namespace Daybreak.Services.ApplicationLauncher; internal sealed class ApplicationLauncher( - PhotinoWindow photinoWindow, IDaybreakInjector daybreakInjector, IGuildWarsExecutableManager guildWarsExecutableManager, INotificationService notificationService, @@ -40,7 +38,6 @@ internal sealed class ApplicationLauncher( private static readonly TimeSpan LaunchTimeout = TimeSpan.FromMinutes(1); - private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull(); private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull(); private readonly IGuildWarsExecutableManager guildWarsExecutableManager = guildWarsExecutableManager.ThrowIfNull(); @@ -71,7 +68,7 @@ internal sealed class ApplicationLauncher( cancellationToken, timeout.Token ); - var gwProcess = await this.LaunchGuildwarsProcess( + var (gwProcess, enabledMods) = await this.LaunchGuildwarsProcess( launchConfigurationWithCredentials, cancellation.Token ); @@ -85,6 +82,7 @@ internal sealed class ApplicationLauncher( LaunchConfiguration = launchConfigurationWithCredentials, GuildWarsProcess = gwProcess, ProcessId = (uint)gwProcess.Id, + EnabledMods = enabledMods, }; } @@ -103,7 +101,7 @@ internal sealed class ApplicationLauncher( this.daybreakRestartingService.RestartDaybreakAsNormalUser(); } - private async Task LaunchGuildwarsProcess( + private async Task<(Process? Process, IReadOnlyList EnabledMods)> LaunchGuildwarsProcess( LaunchConfigurationWithCredentials launchConfigurationWithCredentials, CancellationToken cancellationToken ) @@ -137,7 +135,7 @@ internal sealed class ApplicationLauncher( title: "Can not launch Guild Wars", description: "No available executables found. All executables are currently in use" ); - return default; + return (default, []); } executable = firstAvailableExecutable; @@ -178,11 +176,15 @@ internal sealed class ApplicationLauncher( var enabledMods = this .modsManager.GetMods() - .Where(m => m.IsEnabled && m.IsInstalled) + .Where(m => launchConfigurationWithCredentials.CustomModLoadoutEnabled + ? m.IsInstalled && (!m.CanDisable || launchConfigurationWithCredentials.EnabledMods?.Contains(m.Name) is true) + : m.IsInstalled && m.IsEnabled) .ToList(); var disabledMods = this .modsManager.GetMods() - .Where(m => !m.IsEnabled && m.IsInstalled) + .Where(m => launchConfigurationWithCredentials.CustomModLoadoutEnabled + ? m.IsInstalled && (!m.CanDisable || launchConfigurationWithCredentials.EnabledMods?.Contains(m.Name) is not true) + : m.IsInstalled && !m.IsEnabled) .ToList(); if (this.launcherOptions.CurrentValue.CancelLaunchOutOfDateMods) @@ -269,7 +271,7 @@ internal sealed class ApplicationLauncher( title: $"{mod.Name} exception", description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildWarsStartingDisabled)}" ); - return default; + return (default, []); } } @@ -293,7 +295,7 @@ internal sealed class ApplicationLauncher( title: $"{mod.Name} canceled startup", description: $"Mod canceled the startup during {nameof(mod.OnGuildWarsStarting)}" ); - return default; + return (default, []); } } catch (TaskCanceledException) @@ -311,7 +313,7 @@ internal sealed class ApplicationLauncher( title: $"{mod.Name} exception", description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildWarsStarting)}" ); - return default; + return (default, []); } } @@ -331,7 +333,7 @@ internal sealed class ApplicationLauncher( title: "Failed to launch Guild Wars", description: $"Injector failed to launch Guild Wars with launch result {launchResult}. Check logs for details" ); - return default; + return (default, []); } var threadHandle = launchResult.ThreadHandle; @@ -347,7 +349,7 @@ internal sealed class ApplicationLauncher( title: "Failed to launch Guild Wars", description: "Injector returned invalid process or thread handle. Check logs for details" ); - return default; + return (default, []); } // Reset launch context with the launched process @@ -386,7 +388,7 @@ internal sealed class ApplicationLauncher( ProcessId = (uint)pId, } ); - return default; + return (default, []); } } catch (TaskCanceledException) @@ -412,7 +414,7 @@ internal sealed class ApplicationLauncher( title: $"{mod.Name} exception", description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildWarsCreated)}" ); - return default; + return (default, []); } } @@ -427,7 +429,7 @@ internal sealed class ApplicationLauncher( title: "Failed to launch Guild Wars", description: $"Injector failed to resume Guild Wars with resume result {resumeResult}. Check logs for details" ); - return default; + return (default, []); } var sw = Stopwatch.StartNew(); @@ -443,7 +445,7 @@ internal sealed class ApplicationLauncher( title: "Guild Wars process not ready", description: "Guild Wars process exited or timed out before becoming ready. Please check logs for details" ); - return default; + return (default, []); } /* @@ -485,11 +487,11 @@ internal sealed class ApplicationLauncher( title: $"{mod.Name} exception", description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildWarsStarted)}" ); - return default; + return (default, []); } } - return process; + return (process, enabledMods); } public GuildWarsApplicationLaunchContext? GetGuildwarsProcess( diff --git a/Daybreak.Core/Services/DirectSong/DirectSongService.cs b/Daybreak.Core/Services/DirectSong/DirectSongService.cs index 3e6802fa..785cde94 100644 --- a/Daybreak.Core/Services/DirectSong/DirectSongService.cs +++ b/Daybreak.Core/Services/DirectSong/DirectSongService.cs @@ -57,6 +57,7 @@ internal sealed class DirectSongService( public bool IsVisible => true; public bool CanCustomManage => false; public bool CanUninstall => true; + public bool CanDisable => true; public bool IsEnabled { get => this.options.CurrentValue.Enabled; diff --git a/Daybreak.Core/Services/Guildwars/GuildWarsVersionChecker.cs b/Daybreak.Core/Services/Guildwars/GuildWarsVersionChecker.cs index e3b1361d..5e0bb8b8 100644 --- a/Daybreak.Core/Services/Guildwars/GuildWarsVersionChecker.cs +++ b/Daybreak.Core/Services/Guildwars/GuildWarsVersionChecker.cs @@ -47,6 +47,7 @@ internal sealed class GuildWarsVersionChecker( private readonly ILogger logger = logger.ThrowIfNull(); public bool CanUninstall => false; + public bool CanDisable => true; public IProgressAsyncOperation PerformUninstallation(CancellationToken cancellationToken) { diff --git a/Daybreak.Core/Services/LaunchConfigurations/LaunchConfigurationService.cs b/Daybreak.Core/Services/LaunchConfigurations/LaunchConfigurationService.cs index 0456f755..68cf4e15 100644 --- a/Daybreak.Core/Services/LaunchConfigurations/LaunchConfigurationService.cs +++ b/Daybreak.Core/Services/LaunchConfigurations/LaunchConfigurationService.cs @@ -111,6 +111,8 @@ internal sealed class LaunchConfigurationService( config.Executable = launchConfigurationWithCredentials.ExecutablePath; config.Arguments = launchConfigurationWithCredentials.Arguments; config.SteamSupport = launchConfigurationWithCredentials.SteamSupport; + config.EnabledMods = launchConfigurationWithCredentials.EnabledMods; + config.CustomModLoadoutEnabled = launchConfigurationWithCredentials.CustomModLoadoutEnabled; var options = this.liveUpdateableOptions.CurrentValue; options.LaunchConfigurations = configs; this.optionsProvider.SaveOption(options); @@ -124,7 +126,9 @@ internal sealed class LaunchConfigurationService( Executable = launchConfigurationWithCredentials.ExecutablePath, Identifier = launchConfigurationWithCredentials.Identifier, Arguments = launchConfigurationWithCredentials.Arguments, - SteamSupport = launchConfigurationWithCredentials.SteamSupport + SteamSupport = launchConfigurationWithCredentials.SteamSupport, + EnabledMods = launchConfigurationWithCredentials.EnabledMods, + CustomModLoadoutEnabled = launchConfigurationWithCredentials.CustomModLoadoutEnabled }); var optionsNew = this.liveUpdateableOptions.CurrentValue; optionsNew.LaunchConfigurations = configs; @@ -152,7 +156,9 @@ internal sealed class LaunchConfigurationService( ExecutablePath = launchConfiguration.Executable, Arguments = launchConfiguration.Arguments, Name = launchConfiguration.Name, - SteamSupport = launchConfiguration.SteamSupport ?? true + SteamSupport = launchConfiguration.SteamSupport ?? true, + EnabledMods = launchConfiguration.EnabledMods, + CustomModLoadoutEnabled = launchConfiguration.CustomModLoadoutEnabled ?? false }; } } diff --git a/Daybreak.Core/Services/ReShade/ReShadeService.cs b/Daybreak.Core/Services/ReShade/ReShadeService.cs index e0935185..e5dcf146 100644 --- a/Daybreak.Core/Services/ReShade/ReShadeService.cs +++ b/Daybreak.Core/Services/ReShade/ReShadeService.cs @@ -99,6 +99,7 @@ internal sealed class ReShadeService( File.Exists(ReShadeLogPath) && File.Exists(ConfigIniPath); public bool CanUninstall => true; + public bool CanDisable => true; public IProgressAsyncOperation PerformUninstallation(CancellationToken cancellationToken) { diff --git a/Daybreak.Core/Services/Toolbox/ToolboxService.cs b/Daybreak.Core/Services/Toolbox/ToolboxService.cs index cd50fbf2..ac9e7d4f 100644 --- a/Daybreak.Core/Services/Toolbox/ToolboxService.cs +++ b/Daybreak.Core/Services/Toolbox/ToolboxService.cs @@ -57,6 +57,7 @@ internal sealed class ToolboxService( public bool IsVisible => true; public bool CanCustomManage => false; public bool CanUninstall => true; + public bool CanDisable => true; public bool IsEnabled { get => this.toolboxOptions.CurrentValue.Enabled; diff --git a/Daybreak.Core/Services/UMod/UModService.cs b/Daybreak.Core/Services/UMod/UModService.cs index dc30160e..079ef23c 100644 --- a/Daybreak.Core/Services/UMod/UModService.cs +++ b/Daybreak.Core/Services/UMod/UModService.cs @@ -64,6 +64,7 @@ internal sealed class UModService( public bool IsVisible => true; public bool CanCustomManage => true; public bool CanUninstall => true; + public bool CanDisable => true; public bool IsEnabled { get => this.uModOptions.CurrentValue.Enabled; diff --git a/Daybreak.Core/Views/LaunchConfigurationsView.razor b/Daybreak.Core/Views/LaunchConfigurationsView.razor index 8bfdede5..b2495d5f 100644 --- a/Daybreak.Core/Views/LaunchConfigurationsView.razor +++ b/Daybreak.Core/Views/LaunchConfigurationsView.razor @@ -1,117 +1,114 @@ 
-
- +
+ +
+
+
+

Launch Configurations

-
-
-

Launch Configurations

-
-
- @foreach (var config in this.ViewModel.LaunchConfigurations) - { -
-
-
- Identifier -
-
- - - -
-
-
-
- Name -
-
- -
-
-
-
- Executable -
-
- - -
- @(string.IsNullOrWhiteSpace(context) ? "Any Executable" : context) -
-
-
-
-
-
-
- Credentials -
-
- - - @(this.ViewModel.Credentials.FirstOrDefault(c => c.Identifier == context)?.Username ?? "Unknown") - - -
-
-
-
- Custom Arguments -
-
- -
-
-
-
- Steam support -
-
- -
-
-
- } +
+ @foreach (var config in this.ViewModel.LaunchConfigurations) + { +
+
+
+ Identifier +
+
+ + + +
+
+
+
+ Name +
+
+ +
+
+
+
+ Executable +
+
+ + +
+ @(string.IsNullOrWhiteSpace(context) ? "Any Executable" : context) +
+
+
+
+
+
+
+ Credentials +
+
+ + + @(this.ViewModel.Credentials.FirstOrDefault(c => c.Identifier == context)?.Username ?? "Unknown") + + +
+
+
+
+ Custom Arguments +
+
+ +
+
+
+
+ Steam support +
+
+ +
+
+
+
+ Custom mod loadout +
+
+ + + Manage Mods + +
+
+ }
+
@using Daybreak.Shared.Models; @page "/launch-configurations" -@inherits ViewBase \ No newline at end of file +@inherits ViewBase diff --git a/Daybreak.Core/Views/LaunchConfigurationsView.razor.cs b/Daybreak.Core/Views/LaunchConfigurationsView.razor.cs index d42ae5c6..9d83d560 100644 --- a/Daybreak.Core/Views/LaunchConfigurationsView.razor.cs +++ b/Daybreak.Core/Views/LaunchConfigurationsView.razor.cs @@ -4,18 +4,22 @@ using Daybreak.Shared.Services.Credentials; using Daybreak.Shared.Services.ExecutableManagement; using Daybreak.Shared.Services.LaunchConfigurations; using System.Extensions; +using TrailBlazr.Services; using TrailBlazr.ViewModels; namespace Daybreak.Views; + public sealed class LaunchConfigurationsViewModel( ILaunchConfigurationService launchConfigurationService, ICredentialManager credentialManager, - IGuildWarsExecutableManager guildWarsExecutableManager) + IGuildWarsExecutableManager guildWarsExecutableManager, + IViewManager viewManager) : ViewModelBase { private readonly ILaunchConfigurationService launchConfigurationService = launchConfigurationService; private readonly ICredentialManager credentialManager = credentialManager; private readonly IGuildWarsExecutableManager guildWarsExecutableManager = guildWarsExecutableManager; + private readonly IViewManager viewManager = viewManager; public List Executables { get; } = []; public List Credentials { get; } = []; @@ -83,4 +87,15 @@ public sealed class LaunchConfigurationsViewModel( configuration.SteamSupport = isEnabled; this.launchConfigurationService.SaveConfiguration(configuration); } + + public void CustomModLoadoutChanged(LaunchConfigurationWithCredentials configuration, bool isEnabled) + { + configuration.CustomModLoadoutEnabled = isEnabled; + this.launchConfigurationService.SaveConfiguration(configuration); + } + + public void ManageCustomMods(LaunchConfigurationWithCredentials configuration) + { + this.viewManager.ShowView((nameof(ModsView.LaunchConfigurationIdentifier), configuration.Identifier ?? throw new InvalidOperationException("Managed configuration identifier cannot be null"))); + } } diff --git a/Daybreak.Core/Views/LaunchConfigurationsView.razor.css b/Daybreak.Core/Views/LaunchConfigurationsView.razor.css index 4ea4049e..fe6402d5 100644 --- a/Daybreak.Core/Views/LaunchConfigurationsView.razor.css +++ b/Daybreak.Core/Views/LaunchConfigurationsView.razor.css @@ -51,7 +51,8 @@ .launch-config-credentials, .launch-config-name, .launch-config-args, -.launch-config-steam { +.launch-config-steam, +.launch-config-custom-mods { display: flex; flex-direction: row; width: 100%; @@ -65,7 +66,8 @@ .launch-config-credentials-label, .launch-config-name-label, .launch-config-args-label, -.launch-config-steam-label { +.launch-config-steam-label, +.launch-config-custom-mods-label { width: 250px; } @@ -74,8 +76,13 @@ .launch-config-credentials-field, .launch-config-name-field, .launch-config-args-field, -.launch-config-steam-field { +.launch-config-steam-field, +.launch-config-custom-mods-field { width: 50%; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; } /* Limit dropdown height to prevent container overflow */ @@ -110,7 +117,8 @@ .launch-config-credentials-label ::deep *, .launch-config-name-label ::deep *, .launch-config-args-label ::deep *, -.launch-config-steam-label ::deep * { +.launch-config-steam-label ::deep *, +.launch-config-custom-mods-label ::deep * { font-size: var(--font-size-large) !important; } @@ -119,7 +127,8 @@ .launch-config-credentials-field ::deep fluent-text-field, .launch-config-name-field ::deep fluent-text-field, .launch-config-args-field ::deep fluent-text-field, -.launch-config-steam-field ::deep fluent-text-field { +.launch-config-steam-field ::deep fluent-text-field, +.launch-config-custom-mods-field ::deep fluent-text-field { width: 100%; box-sizing: border-box !important; font-size: var(--font-size-large); @@ -130,9 +139,10 @@ .launch-config-credentials-field ::deep fluent-select, .launch-config-name-field ::deep fluent-select, .launch-config-args-field ::deep fluent-select, -.launch-config-steam-field ::deep fluent-select { +.launch-config-steam-field ::deep fluent-select, +.launch-config-custom-mods-field ::deep fluent-select { width: 100%; box-sizing: border-box !important; font-size: var(--font-size-large) !important; --type-ramp-base-font-size: var(--font-size-large); -} \ No newline at end of file +} diff --git a/Daybreak.Core/Views/LaunchView.razor.cs b/Daybreak.Core/Views/LaunchView.razor.cs index a40a0d70..cae75d5c 100644 --- a/Daybreak.Core/Views/LaunchView.razor.cs +++ b/Daybreak.Core/Views/LaunchView.razor.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.Core.Extensions; using System.Diagnostics; using System.Runtime.CompilerServices; -using Daybreak.Configuration.Options; using Daybreak.Shared.Models; using Daybreak.Shared.Models.Api; using Daybreak.Shared.Models.LaunchConfigurations; @@ -13,7 +12,6 @@ using Daybreak.Shared.Services.ApplicationLauncher; using Daybreak.Shared.Services.LaunchConfigurations; using Daybreak.Shared.Services.Notifications; using Daybreak.Shared.Services.Onboarding; -using Microsoft.Extensions.Options; using TrailBlazr.Services; using TrailBlazr.ViewModels; @@ -25,8 +23,7 @@ public sealed class LaunchViewModel( IDaybreakApiService daybreakApiService, ILaunchConfigurationService launchConfigurationService, IOnboardingService onboardingService, - IApplicationLauncher applicationLauncher, - IOptionsMonitor focusViewOptions + IApplicationLauncher applicationLauncher ) : ViewModelBase, INotifyPropertyChanged, IDisposable { private static readonly TimeSpan LaunchTimeout = TimeSpan.FromSeconds(10); @@ -38,8 +35,6 @@ public sealed class LaunchViewModel( launchConfigurationService.ThrowIfNull(); private readonly IOnboardingService onboardingService = onboardingService.ThrowIfNull(); private readonly IApplicationLauncher applicationLauncher = applicationLauncher.ThrowIfNull(); - private readonly IOptionsMonitor focusViewOptions = - focusViewOptions.ThrowIfNull(); private CancellationTokenSource? cancellationTokenSource; private string? autoLaunchConfigurationId; @@ -476,11 +471,11 @@ public sealed class LaunchViewModel( } else { - // Game running with API mod - can attach or kill based on focus view setting + // Game running with API mod available - can attach to use FocusView launcherViewContext.GameRunning = true; launcherViewContext.CanLaunch = false; - launcherViewContext.CanAttach = this.focusViewOptions.CurrentValue.Enabled; - launcherViewContext.CanKill = !this.focusViewOptions.CurrentValue.Enabled; + launcherViewContext.CanAttach = true; + launcherViewContext.CanKill = false; } } @@ -546,7 +541,8 @@ public sealed class LaunchViewModel( CancellationToken cancellationToken ) { - if (!this.focusViewOptions.CurrentValue.Enabled) + // Only attach if Daybreak API was enabled for this launch + if (!context.EnabledMods.OfType().Any()) { return; } @@ -593,11 +589,7 @@ public sealed class LaunchViewModel( return; } - if (!this.focusViewOptions.CurrentValue.Enabled) - { - return; - } - + // If we have an apiContext, the Daybreak API is running and we can attach using var notificationToken = this.notificationService.NotifyInformation( title: "Attaching to Guild Wars process...", description: "Attempting to attach to Guild Wars process" @@ -677,7 +669,9 @@ public sealed class LaunchViewModel( this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials( launcherViewContext.Configuration ); - if (!this.focusViewOptions.CurrentValue.Enabled) + + // Only attach to focus view if Daybreak API was enabled for this launch + if (!launchedContext.EnabledMods.OfType().Any()) { return; } diff --git a/Daybreak.Core/Views/ModsView.razor b/Daybreak.Core/Views/ModsView.razor index f839f7e7..6d9572e4 100644 --- a/Daybreak.Core/Views/ModsView.razor +++ b/Daybreak.Core/Views/ModsView.razor @@ -1,66 +1,79 @@ 
-
- @foreach (var mod in this.ViewModel.Mods) - { - if (!mod.IsVisible) - { - continue; - } +
+

+ @if (this.ViewModel.LaunchConfiguration is not null) + { + @($"Mods - {this.ViewModel.LaunchConfiguration.Name ?? this.ViewModel.LaunchConfiguration.Identifier}") + } + else + { + @("Mods") + } +

+
-
-
-
@mod.Name
-
@mod.Description
-
-
-
- @(mod.IsEnabled ? "Enabled" : "Disabled") - -
- @if (mod.Loading) - { - - } - else if (!mod.IsInstalled) - { -
- Install -
- } - else if (mod.CanUpdate) - { -
- Update -
- } - else if (mod.IsEnabled && mod.CanManage) - { -
- Manage -
- } - else if (mod.CanUninstall) - { -
- Uninstall -
- } - else - { -
- Installed -
- } -
+
+ @foreach (var mod in this.ViewModel.Mods) + { + if (!mod.IsVisible) + { + continue; + } + +
+
+
@mod.Name
+
@mod.Description
+
+
+
+ @(mod.IsEnabled ? "Enabled" : "Disabled") + +
+ @if (mod.Loading) + { + + } + else if (!mod.IsInstalled) + { +
+ Install
- } -
+ } + else if (mod.CanUpdate) + { +
+ Update +
+ } + else if (mod.IsEnabled && mod.CanManage) + { +
+ Manage +
+ } + else if (mod.CanUninstall) + { +
+ Uninstall +
+ } + else + { +
+ Installed +
+ } +
+
+ } +
@page "/mods" -@inherits ViewBase \ No newline at end of file +@inherits ViewBase +@code +{ + [Parameter] + public string? LaunchConfigurationIdentifier { get; set; } +} diff --git a/Daybreak.Core/Views/ModsView.razor.cs b/Daybreak.Core/Views/ModsView.razor.cs index 5ed75891..ef18267a 100644 --- a/Daybreak.Core/Views/ModsView.razor.cs +++ b/Daybreak.Core/Views/ModsView.razor.cs @@ -1,5 +1,7 @@ using System.Extensions.Core; using Daybreak.Models; +using Daybreak.Shared.Models.LaunchConfigurations; +using Daybreak.Shared.Services.LaunchConfigurations; using Daybreak.Shared.Services.Mods; using Daybreak.Shared.Services.Notifications; using Microsoft.Extensions.Logging; @@ -7,25 +9,29 @@ using TrailBlazr.Services; using TrailBlazr.ViewModels; namespace Daybreak.Views; + public sealed class ModsViewModel( INotificationService notificationService, IViewManager viewManager, IModsManager modsManager, + ILaunchConfigurationService launchConfigurationService, ILogger logger) : ViewModelBase { private readonly INotificationService notificationService = notificationService; private readonly IViewManager viewManager = viewManager; private readonly IModsManager modService = modsManager; + private readonly ILaunchConfigurationService launchConfigurationService = launchConfigurationService; private readonly ILogger logger = logger; private CancellationTokenSource? cts; + public LaunchConfigurationWithCredentials? LaunchConfiguration { get; private set; } public IEnumerable Mods { get; private set; } = []; public override ValueTask ParametersSet(ModsView view, CancellationToken cancellationToken) { - this.Mods = [.. this.modService.GetMods().OrderBy(m => m.Name) + var globalMods = this.modService.GetMods().OrderBy(m => m.Name) .Select(m => new ModListEntry { Name = m.Name, @@ -36,9 +42,26 @@ public sealed class ModsViewModel( CanManage = m.CanCustomManage, IsInstalled = m.IsInstalled, CanUninstall = m.CanUninstall, + CanDisable = m.CanDisable, CanUpdate = false, Loading = true, - })]; + }).ToList(); + + if (this.launchConfigurationService.GetLaunchConfigurations().FirstOrDefault(l => l.Identifier == view.LaunchConfigurationIdentifier) is LaunchConfigurationWithCredentials config) + { + this.LaunchConfiguration = config; + foreach (var mod in globalMods) + { + // Mods that cannot be disabled are always enabled + mod.IsEnabled = !mod.CanDisable || config.EnabledMods?.Any(m => m == mod.Name) is true; + } + } + else + { + this.LaunchConfiguration = null; + } + + this.Mods = globalMods; this.viewManager.ShowViewRequested += this.ViewManager_ShowViewRequested; this.cts?.Cancel(); this.cts?.Dispose(); @@ -49,13 +72,34 @@ public sealed class ModsViewModel( public void ToggleMod(ModListEntry mod) { - if (!mod.IsInstalled) + if (!mod.IsInstalled || !mod.CanDisable) { return; } mod.IsEnabled = !mod.IsEnabled; - mod.ModService.IsEnabled = mod.IsEnabled; + if (this.LaunchConfiguration is not null) + { + if (mod.IsEnabled) + { + this.LaunchConfiguration.EnabledMods ??= []; + if (!this.LaunchConfiguration.EnabledMods.Any(m => m == mod.Name)) + { + this.LaunchConfiguration.EnabledMods.Add(mod.Name); + } + } + else + { + this.LaunchConfiguration.EnabledMods?.Remove(mod.Name); + } + + this.launchConfigurationService.SaveConfiguration(this.LaunchConfiguration); + } + else + { + mod.ModService.IsEnabled = mod.IsEnabled; + } + this.RefreshView(); } @@ -110,7 +154,7 @@ public sealed class ModsViewModel( if (!await mod.ModService.PerformUninstallation(this.cts?.Token ?? CancellationToken.None)) { this.notificationService.NotifyError($"{mod.Name} uninstallation failed", $"{mod.Name} has failed to uninstall. Please check logs for more details."); - + } mod.Loading = false; @@ -133,7 +177,7 @@ public sealed class ModsViewModel( private async ValueTask CheckForUpdates(CancellationToken cancellationToken) { - foreach(var mod in this.Mods) + foreach (var mod in this.Mods) { mod.Loading = true; } @@ -152,7 +196,7 @@ public sealed class ModsViewModel( mod.CanUpdate = false; } } - catch(Exception ex) + catch (Exception ex) { mod.CanUpdate = false; var modName = mod.Name ?? "Unknown mod"; diff --git a/Daybreak.Core/Views/ModsView.razor.css b/Daybreak.Core/Views/ModsView.razor.css index 26295e00..8bbb25c9 100644 --- a/Daybreak.Core/Views/ModsView.razor.css +++ b/Daybreak.Core/Views/ModsView.razor.css @@ -2,7 +2,8 @@ gap: 1rem; padding: 1rem; overflow-y: auto; - margin-top: 40px; + flex: 1; + min-height: 0; } .mod-row { diff --git a/Daybreak.Core/wwwroot/css/site.css b/Daybreak.Core/wwwroot/css/site.css index cd59e449..91be9d9a 100644 --- a/Daybreak.Core/wwwroot/css/site.css +++ b/Daybreak.Core/wwwroot/css/site.css @@ -133,6 +133,13 @@ html, body { background: color-mix(in srgb, var(--neutral-fill-strong-hover) 80%, transparent); } +.view-title { + text-align: center; + margin-bottom: 2rem; + color: var(--neutral-foreground-rest); + font-size: var(--font-size-large); +} + .option-view-title { text-align: center; margin-bottom: 2rem; diff --git a/Daybreak.Linux/Services/Wine/WinePrefixManager.cs b/Daybreak.Linux/Services/Wine/WinePrefixManager.cs index 6221a53c..00e5a00e 100644 --- a/Daybreak.Linux/Services/Wine/WinePrefixManager.cs +++ b/Daybreak.Linux/Services/Wine/WinePrefixManager.cs @@ -69,6 +69,9 @@ public sealed class WinePrefixManager( /// public bool CanUninstall => false; + /// + public bool CanDisable => false; + /// public IProgressAsyncOperation PerformInstallation(CancellationToken cancellationToken) { diff --git a/Daybreak.Shared/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs b/Daybreak.Shared/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs index 357767e4..1131ec42 100644 --- a/Daybreak.Shared/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs +++ b/Daybreak.Shared/Models/LaunchConfigurations/GuildWarsApplicationLaunchContext.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Daybreak.Shared.Services.Mods; namespace Daybreak.Shared.Models.LaunchConfigurations; @@ -8,6 +9,12 @@ public sealed record GuildWarsApplicationLaunchContext : IEquatable + /// The list of mod services that were enabled when this process was launched. + /// This is used to determine which mods are active for this specific instance. + /// + public IReadOnlyList EnabledMods { get; init; } = []; + public GuildWarsApplicationLaunchContext() { } diff --git a/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfiguration.cs b/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfiguration.cs index b211048f..3f255c47 100644 --- a/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfiguration.cs +++ b/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfiguration.cs @@ -21,4 +21,10 @@ public sealed class LaunchConfiguration [JsonPropertyName(nameof(SteamSupport))] public bool? SteamSupport { get; set; } = true; + + [JsonPropertyName(nameof(EnabledMods))] + public List? EnabledMods { get; set; } + + [JsonPropertyName(nameof(CustomModLoadoutEnabled))] + public bool? CustomModLoadoutEnabled { get; set; } } diff --git a/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfigurationWithCredentials.cs b/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfigurationWithCredentials.cs index 380e76fc..b66d3e4c 100644 --- a/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfigurationWithCredentials.cs +++ b/Daybreak.Shared/Models/LaunchConfigurations/LaunchConfigurationWithCredentials.cs @@ -8,6 +8,8 @@ public sealed class LaunchConfigurationWithCredentials : IEquatable? EnabledMods { get; set; } + public bool CustomModLoadoutEnabled { get; set; } public bool Equals(LaunchConfigurationWithCredentials? other) { @@ -27,7 +29,10 @@ public sealed class LaunchConfigurationWithCredentials : IEquatable bool CanUninstall { get; } + /// + /// Dictates if the mod can be disabled by the user. If false, the mod is always enabled when installed. + /// + bool CanDisable { get; } + /// /// Called by the mod manager to perform installation steps for the mod. /// diff --git a/Daybreak.Windows/Services/Screens/GuildwarsScreenPlacer.cs b/Daybreak.Windows/Services/Screens/GuildwarsScreenPlacer.cs index 49a485fd..e0b3912e 100644 --- a/Daybreak.Windows/Services/Screens/GuildwarsScreenPlacer.cs +++ b/Daybreak.Windows/Services/Screens/GuildwarsScreenPlacer.cs @@ -48,6 +48,7 @@ internal sealed class GuildwarsScreenPlacer( public bool IsInstalled => true; public bool CanUninstall => false; + public bool CanDisable => true; public IProgressAsyncOperation PerformUninstallation(CancellationToken cancellationToken) { diff --git a/Examples/Plugins/SimplePlugin/SimplePlugin/Services/SimpleNotificationMod.cs b/Examples/Plugins/SimplePlugin/SimplePlugin/Services/SimpleNotificationMod.cs index bad11119..f99c1c15 100644 --- a/Examples/Plugins/SimplePlugin/SimplePlugin/Services/SimpleNotificationMod.cs +++ b/Examples/Plugins/SimplePlugin/SimplePlugin/Services/SimpleNotificationMod.cs @@ -27,6 +27,7 @@ public sealed class SimpleNotificationMod( public bool IsVisible { get; } = true; public bool CanCustomManage { get; } = false; public bool CanUninstall { get; } = false; + public bool CanDisable { get; } = true; public IEnumerable GetCustomArguments() => [];