diff --git a/src/Starward.Language/Lang.Designer.cs b/src/Starward.Language/Lang.Designer.cs index 9a57c2925..702508b2e 100644 --- a/src/Starward.Language/Lang.Designer.cs +++ b/src/Starward.Language/Lang.Designer.cs @@ -7089,6 +7089,123 @@ public static string StartGameButton_Verify { } } + /// + /// 查找类似 Default 的本地化字符串。 + /// + public static string StartGameButton_DefaultLaunchOption { + get { + return ResourceManager.GetString("StartGameButton_DefaultLaunchOption", resourceCulture); + } + } + + /// + /// 查找类似 Choose launch option 的本地化字符串。 + /// + public static string StartGameButton_ChooseLaunchOption { + get { + return ResourceManager.GetString("StartGameButton_ChooseLaunchOption", resourceCulture); + } + } + + /// + /// 查找类似 Launch Options 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptions { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptions", resourceCulture); + } + } + + /// + /// 查找类似 Define multiple launch commands, and pick one from the dropdown next to the Start Game button when launching the game. 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionsDescription { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionsDescription", resourceCulture); + } + } + + /// + /// 查找类似 Add Launch Option 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_AddLaunchOption { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_AddLaunchOption", resourceCulture); + } + } + + /// + /// 查找类似 Name 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionName { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionName", resourceCulture); + } + } + + /// + /// 查找类似 Executable (leave empty to use the game's default executable) 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionExecutable { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionExecutable", resourceCulture); + } + } + + /// + /// 查找类似 Command Line Arguments 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionArguments { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionArguments", resourceCulture); + } + } + + /// + /// 查找类似 Run as administrator 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionRunAsAdmin { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionRunAsAdmin", resourceCulture); + } + } + + /// + /// 查找类似 Uses the settings above (custom executable + command line arguments). 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionDefaultTooltip { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionDefaultTooltip", resourceCulture); + } + } + + /// + /// 查找类似 Move up 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionMoveUp { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionMoveUp", resourceCulture); + } + } + + /// + /// 查找类似 Move down 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionMoveDown { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionMoveDown", resourceCulture); + } + } + + /// + /// 查找类似 Name is required 的本地化字符串。 + /// + public static string GameLauncherSettingDialog_LaunchOptionNameRequired { + get { + return ResourceManager.GetString("GameLauncherSettingDialog_LaunchOptionNameRequired", resourceCulture); + } + } + /// /// 查找类似 Waiting 的本地化字符串。 /// diff --git a/src/Starward.Language/Lang.resx b/src/Starward.Language/Lang.resx index 810bec26b..e3c61bb0f 100644 --- a/src/Starward.Language/Lang.resx +++ b/src/Starward.Language/Lang.resx @@ -1782,6 +1782,45 @@ Do you accept the risk and continue to use it? Verify + + Default + + + Choose launch option + + + Launch Options + + + Define multiple launch commands, and pick one from the dropdown next to the Start Game button when launching the game. + + + Add Launch Option + + + Name + + + Executable (leave empty to use the game's default executable) + + + Command Line Arguments + + + Run as administrator + + + Uses the settings above (custom executable + command line arguments). + + + Move up + + + Move down + + + Name is required + RPC Service is not Running diff --git a/src/Starward.Language/Lang.zh-CN.resx b/src/Starward.Language/Lang.zh-CN.resx index 5c98d4ebd..ce9af2e17 100644 --- a/src/Starward.Language/Lang.zh-CN.resx +++ b/src/Starward.Language/Lang.zh-CN.resx @@ -1782,6 +1782,45 @@ 校验 + + 默认 + + + 选择启动方式 + + + 启动方式 + + + 可以定义多个启动命令行,启动游戏时在“开始游戏”按钮旁的下拉菜单中选择。 + + + 添加启动方式 + + + 名称 + + + 启动程序(留空则使用游戏本体的默认启动程序) + + + 命令行参数 + + + 以管理员身份运行 + + + 使用上方设置项(自定义启动程序 + 命令行参数)。 + + + 上移 + + + 下移 + + + 名称不能为空 + RPC 服务未运行 diff --git a/src/Starward/AppConfig.Setting.cs b/src/Starward/AppConfig.Setting.cs index 47570fd91..cefb8919a 100644 --- a/src/Starward/AppConfig.Setting.cs +++ b/src/Starward/AppConfig.Setting.cs @@ -616,6 +616,41 @@ public static void SetEnableDX12(GameBiz biz, bool value) } + /// + /// 自定义启动预设列表(JSON 序列化的 数组), + /// 参考 + /// + public static string? GetLaunchSchemes(GameBiz biz) + { + return GetValue(default, $"launch_schemes_{biz}"); + } + + /// + /// 自定义启动预设列表(JSON 序列化的 数组) + /// + public static void SetLaunchSchemes(GameBiz biz, string? value) + { + SetValue(value, $"launch_schemes_{biz}"); + } + + + /// + /// 最近选择的启动预设 Id,为空或匹配 表示使用内置默认预设 + /// + public static string? GetSelectedLaunchSchemeId(GameBiz biz) + { + return GetValue(default, $"selected_launch_scheme_{biz}"); + } + + /// + /// 最近选择的启动预设 Id + /// + public static void SetSelectedLaunchSchemeId(GameBiz biz, string? value) + { + SetValue(value, $"selected_launch_scheme_{biz}"); + } + + #endregion diff --git a/src/Starward/Features/GameLauncher/GameLaunchScheme.cs b/src/Starward/Features/GameLauncher/GameLaunchScheme.cs new file mode 100644 index 000000000..f53e66247 --- /dev/null +++ b/src/Starward/Features/GameLauncher/GameLaunchScheme.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Starward.Features.GameLauncher; + + +/// +/// 自定义启动预设。用户可以定义多个启动命令行,例如通过 FPS Unlocker、脚本等方式启动游戏。 +/// 参考 +/// +public sealed class GameLaunchScheme +{ + + /// + /// 唯一标识 + /// + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + + + /// + /// 显示名称 + /// + public string Name { get; set; } = string.Empty; + + + /// + /// 启动程序的完整路径。为空时使用游戏本体的默认启动程序。 + /// + public string? ExecutablePath { get; set; } + + + /// + /// 附加的命令行参数。 + /// + public string? Arguments { get; set; } + + + /// + /// 需要以管理员权限运行。仅当 为 .exe 或 .bat 时有效。 + /// + public bool RunAsAdmin { get; set; } + + + /// + /// 是否是默认(内置)预设。内置预设不可删除,且始终采用当前设置页的 “自定义启动程序” 与 “命令行参数” 值。 + /// + [JsonIgnore] + public bool IsBuiltIn { get; set; } + + + /// + /// 内置默认预设 Id + /// + public const string BuiltInDefaultId = "__default__"; + + + public GameLaunchScheme Clone() + { + return new GameLaunchScheme + { + Id = Id, + Name = Name, + ExecutablePath = ExecutablePath, + Arguments = Arguments, + RunAsAdmin = RunAsAdmin, + IsBuiltIn = IsBuiltIn, + }; + } + + + internal static List Deserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + try + { + List? list = JsonSerializer.Deserialize>(json); + if (list is null) + { + return new List(); + } + for (int i = list.Count - 1; i >= 0; i--) + { + GameLaunchScheme item = list[i]; + if (string.IsNullOrEmpty(item.Id)) + { + item.Id = Guid.NewGuid().ToString("N"); + } + // 用户不能通过配置构造出内置预设 + if (item.Id == BuiltInDefaultId) + { + list.RemoveAt(i); + } + } + return list; + } + catch + { + return new List(); + } + } + + + internal static string? Serialize(List? list) + { + if (list is null || list.Count == 0) + { + return null; + } + return JsonSerializer.Serialize(list, AppConfig.JsonSerializerOptions); + } + +} diff --git a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml index ac0f17306..c3852efa9 100644 --- a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml +++ b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml @@ -552,6 +552,7 @@ HorizontalAlignment="Right" GameCommand="{x:Bind ClickStartGameButtonCommand}" GameState="{x:Bind GameState}" + LaunchOptionSelected="Button_StartGame_LaunchOptionSelected" RunningGameInfo="{x:Bind RunningGameInfo}" SettingCommand="{x:Bind OpenGameLauncherSettingDialogCommand}" /> diff --git a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs index 22d9a49a2..e5f026627 100644 --- a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs +++ b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs @@ -66,6 +66,7 @@ protected override void OnLoaded() CheckGameVersion(); UpdateGameInstallTask(); CheckCloudGame(); + RefreshLaunchOptions(); _ = InitializeGameServerAsync(); _ = InitializeBackgameImageSwitcherAsync(); WeakReferenceMessenger.Default.Register(this, OnGameInstallPathChanged); @@ -73,6 +74,7 @@ protected override void OnLoaded() WeakReferenceMessenger.Default.Register(this, OnRemovableStorageDeviceChanged); WeakReferenceMessenger.Default.Register(this, OnGameInstallTaskStarted); WeakReferenceMessenger.Default.Register(this, OnBackgroundChanged); + WeakReferenceMessenger.Default.Register(this, OnLaunchSchemesChanged); } @@ -544,7 +546,8 @@ private async Task StartGameAsync() { try { - var process = await _gameLauncherService.StartGameAsync(CurrentGameId); + GameLaunchScheme scheme = GameLauncherService.GetSelectedLaunchScheme(CurrentGameBiz); + var process = await _gameLauncherService.StartGameAsync(CurrentGameId, scheme: scheme); if (process is not null) { GameState = GameState.GameIsRunning; @@ -570,6 +573,73 @@ private async Task StartGameAsync() + #region Launch Options + + + /// + /// 刷新启动预设菜单并同步选中项 + /// + private void RefreshLaunchOptions() + { + try + { + List schemes = GameLauncherService.GetAllLaunchSchemes(CurrentGameBiz); + GameLaunchScheme selected = GameLauncherService.GetSelectedLaunchScheme(CurrentGameBiz); + Button_StartGame.UpdateLaunchOptions(schemes, selected.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Refresh launch options ({biz})", CurrentGameBiz); + } + } + + + private void OnLaunchSchemesChanged(object _, LaunchSchemesChangedMessage message) + { + RefreshLaunchOptions(); + } + + + /// + /// 点击启动预设菜单项:记住选择,并直接以该预设启动游戏。 + /// + private async void Button_StartGame_LaunchOptionSelected(string id) + { + try + { + List all = GameLauncherService.GetAllLaunchSchemes(CurrentGameBiz); + GameLaunchScheme scheme = all.FirstOrDefault(x => x.Id == id) ?? GameLauncherService.GetBuiltInDefaultScheme(); + GameLauncherService.SetSelectedLaunchScheme(CurrentGameBiz, scheme); + Button_StartGame.UpdateLaunchOptions(all, scheme.Id); + + if (GameState is not GameState.StartGame) + { + return; + } + var process = await _gameLauncherService.StartGameAsync(CurrentGameId, scheme: scheme); + if (process is not null) + { + GameState = GameState.GameIsRunning; + GameProcess = process; + WeakReferenceMessenger.Default.Send(new GameStartedMessage()); + } + } + catch (FileNotFoundException) + { + CheckGameVersion(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Start game with launch option {id}", id); + } + } + + + #endregion + + + + #region Install Game diff --git a/src/Starward/Features/GameLauncher/GameLauncherService.cs b/src/Starward/Features/GameLauncher/GameLauncherService.cs index 140358dba..e6511a456 100644 --- a/src/Starward/Features/GameLauncher/GameLauncherService.cs +++ b/src/Starward/Features/GameLauncher/GameLauncherService.cs @@ -6,6 +6,7 @@ using Starward.Features.PlayTime; using Starward.Helpers; using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -312,7 +313,7 @@ public static bool IsProcessPending(Process process) /// 启动游戏 /// /// - public async Task StartGameAsync(GameId gameId, string? installPath = null) + public async Task StartGameAsync(GameId gameId, string? installPath = null, GameLaunchScheme? scheme = null) { const int ERROR_CANCELLED = 0x000004C7; try @@ -321,6 +322,8 @@ public static bool IsProcessPending(Process process) { throw new Exception($"Game is running: {existingProcess.ProcessName}.exe ({existingProcess.Id})."); } + // 内置默认预设与未传入预设行为一致,均沿用旧的 AppConfig 中 “自定义启动程序 / 命令行参数” 设置。 + bool useCustomScheme = scheme is not null && !scheme.IsBuiltIn; string? exe = null, arg = null, verb = null; if (Directory.Exists(installPath)) { @@ -331,7 +334,31 @@ public static bool IsProcessPending(Process process) } } bool thirdPartyTool = false; - if (string.IsNullOrWhiteSpace(exe) && AppConfig.GetEnableThirdPartyTool(gameId.GameBiz)) + if (useCustomScheme && !string.IsNullOrWhiteSpace(scheme!.ExecutablePath)) + { + // 用户自定义预设优先级最高,跳过全局第三方工具设置。 + string schemeExe = GetFullPathIfRelativePath(scheme.ExecutablePath); + if (File.Exists(schemeExe)) + { + exe = schemeExe; + thirdPartyTool = true; + string ext = Path.GetExtension(exe); + if (scheme.RunAsAdmin && ext is ".exe" or ".bat") + { + verb = "runas"; + } + else + { + verb = ""; + } + } + else + { + _logger.LogWarning("Launch scheme executable not found: {path}", scheme.ExecutablePath); + exe = null; + } + } + if (!thirdPartyTool && !useCustomScheme && string.IsNullOrWhiteSpace(exe) && AppConfig.GetEnableThirdPartyTool(gameId.GameBiz)) { exe = GetThirdPartyToolPath(gameId); if (File.Exists(exe)) @@ -358,7 +385,14 @@ public static bool IsProcessPending(Process process) throw new FileNotFoundException("Game exe not found", name); } } - arg = AppConfig.GetStartArgument(gameId.GameBiz)?.Trim(); + if (useCustomScheme) + { + arg = scheme!.Arguments?.Trim(); + } + else + { + arg = AppConfig.GetStartArgument(gameId.GameBiz)?.Trim(); + } if (AppConfig.EnableLoginAuthTicket is true) { string? ticket = await _gameAuthLoginService.CreateAuthTicketByGameBiz(gameId); @@ -385,7 +419,7 @@ public static bool IsProcessPending(Process process) arg = $"""/c start "" /d "{Path.GetDirectoryName(exe)}" "{exe}" {arg}"""; exe = "cmd.exe"; } - _logger.LogInformation("Start game ({biz})\r\npath: {exe}\r\narg: {arg}", gameId, exe, arg); + _logger.LogInformation("Start game ({biz}) scheme: {scheme}\r\npath: {exe}\r\narg: {arg}", gameId, scheme?.Name ?? "default", exe, arg); var info = new ProcessStartInfo { FileName = exe, @@ -544,4 +578,76 @@ public static string GetFullPathIfRelativePath(string path) + /// + /// 获取内置默认启动预设,用于表示 “默认启动”,其行为完全由 AppConfig 中现有的 + /// 自定义启动程序 / 命令行参数设置决定。 + /// + public static GameLaunchScheme GetBuiltInDefaultScheme() + { + return new GameLaunchScheme + { + Id = GameLaunchScheme.BuiltInDefaultId, + Name = Lang.StartGameButton_DefaultLaunchOption, + IsBuiltIn = true, + }; + } + + + /// + /// 获取用户为指定游戏定义的自定义启动预设列表(不包含内置默认预设)。 + /// + public static List GetLaunchSchemes(GameBiz biz) + { + return GameLaunchScheme.Deserialize(AppConfig.GetLaunchSchemes(biz)); + } + + + /// + /// 获取包含内置默认预设的完整启动预设列表,默认预设位于首位。 + /// + public static List GetAllLaunchSchemes(GameBiz biz) + { + List list = GetLaunchSchemes(biz); + list.Insert(0, GetBuiltInDefaultScheme()); + return list; + } + + + /// + /// 保存用户自定义启动预设列表(不含内置默认预设)。 + /// + public static void SaveLaunchSchemes(GameBiz biz, IEnumerable? schemes) + { + List? list = schemes?.Where(x => !x.IsBuiltIn).ToList(); + AppConfig.SetLaunchSchemes(biz, GameLaunchScheme.Serialize(list)); + } + + + /// + /// 获取当前选择的启动预设。若没有选择或选择的预设不存在,返回内置默认预设。 + /// + public static GameLaunchScheme GetSelectedLaunchScheme(GameBiz biz) + { + string? id = AppConfig.GetSelectedLaunchSchemeId(biz); + if (!string.IsNullOrEmpty(id) && id != GameLaunchScheme.BuiltInDefaultId) + { + GameLaunchScheme? match = GetLaunchSchemes(biz).FirstOrDefault(x => x.Id == id); + if (match is not null) + { + return match; + } + } + return GetBuiltInDefaultScheme(); + } + + + /// + /// 记住选择的启动预设。 + /// + public static void SetSelectedLaunchScheme(GameBiz biz, GameLaunchScheme? scheme) + { + AppConfig.SetSelectedLaunchSchemeId(biz, scheme?.Id); + } + + } diff --git a/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml b/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml index 9dcbe4f3f..e77eed929 100644 --- a/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml +++ b/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml @@ -400,7 +400,10 @@ - + + @@ -489,7 +492,136 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml.cs b/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml.cs index 0d128e78b..b0585d5f9 100644 --- a/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml.cs +++ b/src/Starward/Features/GameLauncher/GameLauncherSettingDialog.xaml.cs @@ -16,6 +16,7 @@ using Starward.RPC.GameInstall; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -136,6 +137,7 @@ private async void GameLauncherSettingDialog_Loaded(object sender, RoutedEventAr CheckCanRepairGame(); await InitializeBasicInfoAsync(); InitializeStartArgument(); + InitializeLaunchSchemes(); InitializeCustomBg(); await InitializeGamePackagesAsync(); } @@ -622,6 +624,7 @@ private async Task TryStopGameInstallTaskAsync() partial void OnStartGameArgumentChanged(string? value) { AppConfig.SetStartArgument(CurrentGameBiz, value); + RefreshDefaultLaunchSchemeArguments(); } @@ -745,6 +748,212 @@ private void DeleteThirdPartyToolPath() + #region 启动预设 https://github.com/Scighost/Starward/issues/1858 + + + /// + /// 启动预设列表(含内置默认预设,位于首位) + /// + public ObservableCollection LaunchSchemes { get; } = new(); + + + private bool _launchSchemesInitializing; + + + private void InitializeLaunchSchemes() + { + _launchSchemesInitializing = true; + try + { + LaunchSchemes.Clear(); + List all = GameLauncherService.GetAllLaunchSchemes(CurrentGameBiz); + string? defaultArgumentsHint = _StartGameArgument; + foreach (GameLaunchScheme scheme in all) + { + LaunchSchemes.Add(new LaunchSchemeItem(scheme, defaultArgumentsHint)); + } + } + finally + { + _launchSchemesInitializing = false; + } + } + + + private void RefreshDefaultLaunchSchemeArguments() + { + foreach (LaunchSchemeItem item in LaunchSchemes) + { + item.RefreshDefaultArgumentsHint(_StartGameArgument); + } + } + + + /// + /// 保存当前预设列表到 AppConfig,并广播变更消息 + /// + private void PersistLaunchSchemes() + { + if (_launchSchemesInitializing) + { + return; + } + IEnumerable schemes = LaunchSchemes + .Where(x => !x.IsBuiltIn) + .Select(x => x.Scheme); + GameLauncherService.SaveLaunchSchemes(CurrentGameBiz, schemes); + WeakReferenceMessenger.Default.Send(new LaunchSchemesChangedMessage(CurrentGameBiz)); + } + + + private void Button_AddLaunchScheme_Click(object sender, RoutedEventArgs e) + { + try + { + var scheme = new GameLaunchScheme + { + Name = $"{Lang.GameLauncherSettingDialog_LaunchOptions} {LaunchSchemes.Count(x => !x.IsBuiltIn) + 1}", + }; + LaunchSchemes.Add(new LaunchSchemeItem(scheme, _StartGameArgument)); + PersistLaunchSchemes(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Add launch scheme ({biz})", CurrentGameBiz); + } + } + + + private void LaunchScheme_Name_TextChanged(object sender, TextChangedEventArgs e) + { + if (sender is TextBox { DataContext: LaunchSchemeItem item } textBox && !item.IsBuiltIn) + { + item.Name = textBox.Text ?? string.Empty; + PersistLaunchSchemes(); + } + } + + + private void LaunchScheme_Executable_TextChanged(object sender, TextChangedEventArgs e) + { + if (sender is TextBox { DataContext: LaunchSchemeItem item } textBox && !item.IsBuiltIn) + { + item.ExecutablePath = textBox.Text; + PersistLaunchSchemes(); + } + } + + + private void LaunchScheme_Arguments_TextChanged(object sender, TextChangedEventArgs e) + { + if (sender is TextBox { DataContext: LaunchSchemeItem item } textBox && !item.IsBuiltIn) + { + item.Arguments = textBox.Text; + PersistLaunchSchemes(); + } + } + + + private void LaunchScheme_RunAsAdmin_Changed(object sender, RoutedEventArgs e) + { + if (sender is CheckBox { DataContext: LaunchSchemeItem item } && !item.IsBuiltIn) + { + PersistLaunchSchemes(); + } + } + + + private async void LaunchScheme_BrowseExecutable_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is FrameworkElement { DataContext: LaunchSchemeItem item } && !item.IsBuiltIn) + { + string? file = await FileDialogHelper.PickSingleFileAsync(this.XamlRoot); + if (!string.IsNullOrWhiteSpace(file) && File.Exists(file)) + { + item.ExecutablePath = file; + PersistLaunchSchemes(); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Pick launch scheme executable ({biz})", CurrentGameBiz); + } + } + + + private void LaunchScheme_MoveUp_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is FrameworkElement { DataContext: LaunchSchemeItem item } && !item.IsBuiltIn) + { + int index = LaunchSchemes.IndexOf(item); + // 索引 0 是内置默认预设,用户自定义预设的最小索引是 1 + if (index > 1) + { + LaunchSchemes.Move(index, index - 1); + PersistLaunchSchemes(); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Move launch scheme up ({biz})", CurrentGameBiz); + } + } + + + private void LaunchScheme_MoveDown_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is FrameworkElement { DataContext: LaunchSchemeItem item } && !item.IsBuiltIn) + { + int index = LaunchSchemes.IndexOf(item); + if (index >= 1 && index < LaunchSchemes.Count - 1) + { + LaunchSchemes.Move(index, index + 1); + PersistLaunchSchemes(); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Move launch scheme down ({biz})", CurrentGameBiz); + } + } + + + private void LaunchScheme_Delete_Click(object sender, RoutedEventArgs e) + { + try + { + if (sender is FrameworkElement { DataContext: LaunchSchemeItem item } && !item.IsBuiltIn) + { + LaunchSchemes.Remove(item); + // 如果被删除的预设是当前选中的,回退到内置默认 + if (AppConfig.GetSelectedLaunchSchemeId(CurrentGameBiz) == item.Scheme.Id) + { + AppConfig.SetSelectedLaunchSchemeId(CurrentGameBiz, null); + } + PersistLaunchSchemes(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Delete launch scheme ({biz})", CurrentGameBiz); + } + } + + + #endregion + + + + #region 自定义背景 diff --git a/src/Starward/Features/GameLauncher/LaunchSchemeItem.cs b/src/Starward/Features/GameLauncher/LaunchSchemeItem.cs new file mode 100644 index 000000000..9869e9dc5 --- /dev/null +++ b/src/Starward/Features/GameLauncher/LaunchSchemeItem.cs @@ -0,0 +1,131 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Starward.Features.GameLauncher; + + +/// +/// 启动预设列表 UI 包装类:将 数据与设置对话框中显示相关的 +/// 本地化字符串 / 只读状态 / 输入控件文本等聚合为一个可绑定实体,简化 XAML DataTemplate。 +/// +public sealed partial class LaunchSchemeItem : ObservableObject +{ + + public LaunchSchemeItem(GameLaunchScheme scheme, string? defaultArgumentsHint) + { + Scheme = scheme; + _defaultArgumentsHint = defaultArgumentsHint; + } + + + public GameLaunchScheme Scheme { get; } + + + private string? _defaultArgumentsHint; + + + public bool IsBuiltIn => Scheme.IsBuiltIn; + + + public string Name + { + get => Scheme.Name; + set + { + if (Scheme.Name != value) + { + Scheme.Name = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(DisplayName)); + } + } + } + + + public string? ExecutablePath + { + get => Scheme.ExecutablePath; + set + { + if (Scheme.ExecutablePath != value) + { + Scheme.ExecutablePath = value; + OnPropertyChanged(); + } + } + } + + + public string? Arguments + { + get => Scheme.Arguments; + set + { + if (Scheme.Arguments != value) + { + Scheme.Arguments = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(DisplayArguments)); + } + } + } + + + public bool RunAsAdmin + { + get => Scheme.RunAsAdmin; + set + { + if (Scheme.RunAsAdmin != value) + { + Scheme.RunAsAdmin = value; + OnPropertyChanged(); + } + } + } + + + /// + /// 内置预设显示的名称固定为本地化的 “默认”。 + /// + public string DisplayName => IsBuiltIn ? Lang.StartGameButton_DefaultLaunchOption : Scheme.Name; + + + /// + /// 内置预设的命令行参数直接展示当前设置页面上方的默认命令行参数,供用户参考,不可编辑。 + /// + public string? DisplayArguments => IsBuiltIn ? _defaultArgumentsHint : Scheme.Arguments; + + + public string NameHeader => Lang.GameLauncherSettingDialog_LaunchOptionName; + + public string NamePlaceholder => IsBuiltIn ? Lang.StartGameButton_DefaultLaunchOption : Lang.GameLauncherSettingDialog_LaunchOptionName; + + public string ExecutableHeader => Lang.GameLauncherSettingDialog_LaunchOptionExecutable; + + public string ArgumentsHeader => Lang.GameLauncherSettingDialog_LaunchOptionArguments; + + public string RunAsAdminText => Lang.GameLauncherSettingDialog_LaunchOptionRunAsAdmin; + + public string DefaultTooltip => Lang.GameLauncherSettingDialog_LaunchOptionDefaultTooltip; + + public string MoveUpTooltip => Lang.GameLauncherSettingDialog_LaunchOptionMoveUp; + + public string MoveDownTooltip => Lang.GameLauncherSettingDialog_LaunchOptionMoveDown; + + + /// + /// 当上方 “命令行参数” 文本框内容发生变化时,重新刷新内置预设显示。 + /// + public void RefreshDefaultArgumentsHint(string? hint) + { + if (_defaultArgumentsHint != hint) + { + _defaultArgumentsHint = hint; + if (IsBuiltIn) + { + OnPropertyChanged(nameof(DisplayArguments)); + } + } + } + +} diff --git a/src/Starward/Features/GameLauncher/LaunchSchemesChangedMessage.cs b/src/Starward/Features/GameLauncher/LaunchSchemesChangedMessage.cs new file mode 100644 index 000000000..2a60826ad --- /dev/null +++ b/src/Starward/Features/GameLauncher/LaunchSchemesChangedMessage.cs @@ -0,0 +1,10 @@ +using Starward.Core; + +namespace Starward.Features.GameLauncher; + + +/// +/// 通知启动预设列表发生变化。 +/// +/// 发生变化的游戏区服;如为 则代表全部游戏。 +public sealed record LaunchSchemesChangedMessage(GameBiz GameBiz); diff --git a/src/Starward/Features/GameLauncher/StartGameButton.xaml b/src/Starward/Features/GameLauncher/StartGameButton.xaml index 0de18d631..d0e96e384 100644 --- a/src/Starward/Features/GameLauncher/StartGameButton.xaml +++ b/src/Starward/Features/GameLauncher/StartGameButton.xaml @@ -135,7 +135,7 @@ Visibility="{x:Bind IsGameActionCommandRunning}" /> @@ -261,6 +261,34 @@ + + + + GameState is GameState.Installing; @@ -86,6 +90,86 @@ public StartGameButton() }; + public Brush LaunchOptionsButtonForeground => (IsLaunchOptionsButtonEnabled, IsAccentColorBackgroundVisible, LaunchOptionsButtonPointerOver) switch + { + (false, _, _) => TextOnAccentFillColorDisabled, + (true, false, true) => AccentFillColorDefaultBrush, + (true, false, false) => TextOnAccentFillColorDisabled, + _ => TextOnAccentFillColorPrimaryBrush + }; + + + /// + /// 启动预设下拉按钮可见性。仅当存在自定义启动预设时显示。 + /// + public bool IsLaunchOptionsButtonVisible { get; set { if (SetProperty(ref field, value)) OnPropertyChanged(nameof(StartGameTextMargin)); } } + + + /// + /// 启动预设下拉按钮启用条件,与 是否为 相关 + /// + public bool IsLaunchOptionsButtonEnabled => GameState is GameState.StartGame; + + + /// + /// 启动按钮文本左右边距。当启动预设下拉按钮可见时,右侧需要留出额外空间。 + /// + public Thickness StartGameTextMargin => IsLaunchOptionsButtonVisible + ? new Thickness(41, 5, 81, 6) + : new Thickness(41, 5, 53, 6); + + + /// + /// 启动预设选择回调。参数为预设 Id(内置默认预设为 )。 + /// + public event Action? LaunchOptionSelected; + + + /// + /// 更新启动预设菜单。 + /// + public void UpdateLaunchOptions(IReadOnlyList schemes, string? selectedId) + { + Flyout_LaunchOptions.Items.Clear(); + if (schemes is null || schemes.Count <= 1) + { + IsLaunchOptionsButtonVisible = false; + return; + } + foreach (GameLaunchScheme scheme in schemes) + { + var item = new MenuFlyoutItem + { + Text = string.IsNullOrWhiteSpace(scheme.Name) ? scheme.Id : scheme.Name, + Tag = scheme.Id, + }; + if (scheme.Id == selectedId) + { + item.Icon = new FontIcon { Glyph = "\uE73E", FontSize = 14 }; + } + item.Click += LaunchOptionMenuItem_Click; + Flyout_LaunchOptions.Items.Add(item); + } + IsLaunchOptionsButtonVisible = true; + OnPropertyChanged(nameof(LaunchOptionsButtonForeground)); + } + + + private void LaunchOptionMenuItem_Click(object sender, RoutedEventArgs e) + { + if (sender is MenuFlyoutItem { Tag: string id }) + { + LaunchOptionSelected?.Invoke(id); + } + } + + + private void Button_LaunchOptions_Click(object sender, RoutedEventArgs e) + { + // MenuFlyout 会自动展开;这里保留 Click 事件以维持 Style 一致的视觉反馈。 + } + + private void UpdateActionButtonState() { @@ -98,6 +182,8 @@ private void UpdateActionButtonState() OnPropertyChanged(nameof(IsAccentColorBackgroundVisible)); OnPropertyChanged(nameof(IsGameActionCommandRunning)); OnPropertyChanged(nameof(StartGameButtonText)); + OnPropertyChanged(nameof(IsLaunchOptionsButtonEnabled)); + OnPropertyChanged(nameof(LaunchOptionsButtonForeground)); UpdateButtonForeground(); } @@ -107,6 +193,7 @@ private void UpdateButtonForeground() { OnPropertyChanged(nameof(ActionButtonForeground)); OnPropertyChanged(nameof(SettingButtonForeground)); + OnPropertyChanged(nameof(LaunchOptionsButtonForeground)); } @@ -138,6 +225,10 @@ private void Control_PointerEntered(object sender, PointerRoutedEventArgs e) { SettingButtonPointerOver = true; } + else if (sender as Button == Button_LaunchOptions) + { + LaunchOptionsButtonPointerOver = true; + } } @@ -155,6 +246,10 @@ private void Control_PointerExited(object sender, PointerRoutedEventArgs e) { SettingButtonPointerOver = false; } + else if (sender as Button == Button_LaunchOptions) + { + LaunchOptionsButtonPointerOver = false; + } } @@ -355,6 +450,7 @@ private void StartGameButton_ActualThemeChanged(FrameworkElement sender, object { OnPropertyChanged(nameof(ActionButtonForeground)); OnPropertyChanged(nameof(SettingButtonForeground)); + OnPropertyChanged(nameof(LaunchOptionsButtonForeground)); }