From ac78df369cd632f280a1484b7d6ef1cde1f24dde Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 14 Dec 2023 14:48:56 +0800 Subject: [PATCH] impl #526 --- .../Model/Entity/Primitive/SchemeType.cs | 2 +- .../Model/Entity/SettingEntry.Constant.cs | 2 + .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../Snap.Hutao/Service/AppOptions.cs | 18 +- .../Snap.Hutao/Service/AppOptionsExtension.cs | 35 +- .../GameChannelOptionsService.cs | 6 +- .../Service/Game/GameServiceFacade.cs | 1 + .../Snap.Hutao/Service/Game/LaunchOptions.cs | 25 + .../Service/Game/LaunchOptionsExtension.cs | 87 +++ .../Game/Package/GamePackageService.cs | 6 +- .../Game/PathAbstraction/GamePathEntry.cs | 26 + .../Game/PathAbstraction/GamePathKind.cs | 13 + .../{ => PathAbstraction}/GamePathService.cs | 13 +- .../{ => PathAbstraction}/IGamePathService.cs | 2 +- .../Game/Process/GameProcessService.cs | 4 +- .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 527 +++++++++--------- .../Snap.Hutao/View/Page/SettingPage.xaml | 25 +- .../ViewModel/Game/LaunchGameViewModel.cs | 172 ++++-- .../ViewModel/Setting/SettingViewModel.cs | 27 +- 19 files changed, 587 insertions(+), 407 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => PathAbstraction}/GamePathService.cs (78%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/{ => PathAbstraction}/IGamePathService.cs (79%) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs index 12971805..80696756 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs @@ -12,7 +12,7 @@ internal enum SchemeType /// /// 国际服 /// - Mihoyo, + Hoyoverse, /// /// 国服官服 diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs index 97474eb7..28a54a49 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs @@ -13,6 +13,8 @@ internal sealed partial class SettingEntry /// public const string GamePath = "GamePath"; + public const string GamePathEntries = "GamePathEntries"; + /// /// PowerShell 路径 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index ab31fad0..1c7f44cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -2189,6 +2189,9 @@ 打开截图文件夹 + + 选择游戏路径 + 关于 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs index e02352d9..8c9adcfa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -6,6 +6,8 @@ using Snap.Hutao.Core.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using System.IO; @@ -15,20 +17,12 @@ namespace Snap.Hutao.Service; [Injection(InjectAs.Singleton)] internal sealed partial class AppOptions : DbStoreOptions { - private string? gamePath; private string? powerShellPath; private bool? isEmptyHistoryWishVisible; private BackdropType? backdropType; private CultureInfo? currentCulture; - private bool? isAdvancedLaunchOptionsEnabled; private string? geetestCustomCompositeUrl; - public string GamePath - { - get => GetOption(ref gamePath, SettingEntry.GamePath); - set => SetOption(ref gamePath, SettingEntry.GamePath, value); - } - public string PowerShellPath { get @@ -80,14 +74,6 @@ internal sealed partial class AppOptions : DbStoreOptions set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name); } - public bool IsAdvancedLaunchOptionsEnabled - { - // DO NOT MOVE TO OTHER CLASS - // We use this property in SettingPage binding - get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled); - set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value); - } - public string GeetestCustomCompositeUrl { get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs index 0359ed1d..34c731f1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Model; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using System.IO; @@ -9,39 +11,6 @@ namespace Snap.Hutao.Service; internal static class AppOptionsExtension { - public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) - { - string gamePath = appOptions.GamePath; - - gameFolder = Path.GetDirectoryName(gamePath); - if (string.IsNullOrEmpty(gameFolder)) - { - gameFileName = default; - return false; - } - - gameFileName = Path.GetFileName(gamePath); - if (string.IsNullOrEmpty(gameFileName)) - { - return false; - } - - return true; - } - - public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName) - { - gamePath = appOptions.GamePath; - - gameFileName = Path.GetFileName(gamePath); - if (string.IsNullOrEmpty(gameFileName)) - { - return false; - } - - return true; - } - public static NameValue? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions) { return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs index 80fd68d3..2b63c89a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -13,11 +13,11 @@ namespace Snap.Hutao.Service.Game.Configuration; [Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))] internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService { - private readonly AppOptions appOptions; + private readonly LaunchOptions launchOptions; public ChannelOptions GetChannelOptions() { - string gamePath = appOptions.GamePath; + string gamePath = launchOptions.GamePath; string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); @@ -38,7 +38,7 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer public bool SetChannelOptions(LaunchScheme scheme) { - string gamePath = appOptions.GamePath; + string gamePath = launchOptions.GamePath; string? directory = Path.GetDirectoryName(gamePath); ArgumentException.ThrowIfNullOrEmpty(directory); string configPath = Path.Combine(directory, ConfigFileName); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index cb20776b..115d1ee7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game.Account; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Service.Game.Process; using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs index 4241bba3..b4cbe11a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs @@ -6,6 +6,8 @@ using Microsoft.UI.Windowing; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; using System.Globalization; using Windows.Graphics; using Windows.Win32.Foundation; @@ -24,7 +26,10 @@ internal sealed class LaunchOptions : DbStoreOptions private readonly int primaryScreenHeight; private readonly int primaryScreenFps; + private string? gamePath; + private ImmutableList? gamePathEntries; private bool? isEnabled; + private bool? isAdvancedLaunchOptionsEnabled; private bool? isFullScreen; private bool? isBorderless; private bool? isExclusive; @@ -85,12 +90,32 @@ internal sealed class LaunchOptions : DbStoreOptions } } + public string GamePath + { + get => GetOption(ref gamePath, SettingEntry.GamePath); + set => SetOption(ref gamePath, SettingEntry.GamePath, value); + } + + public ImmutableList GamePathEntries + { + // Because DbStoreOptions can't detect collection change, We use + // ImmutableList to imply that the whole list needs to be replaced + get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize>(raw), []); + set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value)); + } + public bool IsEnabled { get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true); set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value); } + public bool IsAdvancedLaunchOptionsEnabled + { + get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled); + set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value); + } + public bool IsFullScreen { get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs new file mode 100644 index 00000000..5913e00e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs @@ -0,0 +1,87 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; +using System.IO; + +namespace Snap.Hutao.Service.Game; + +internal static class LaunchOptionsExtension +{ + public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) + { + string gamePath = options.GamePath; + + gameFolder = Path.GetDirectoryName(gamePath); + if (string.IsNullOrEmpty(gameFolder)) + { + gameFileName = default; + return false; + } + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } + + public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName) + { + gamePath = options.GamePath; + + gameFileName = Path.GetFileName(gamePath); + if (string.IsNullOrEmpty(gameFileName)) + { + return false; + } + + return true; + } + + public static ImmutableList GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry) + { + string gamePath = options.GamePath; + + if (string.IsNullOrEmpty(gamePath)) + { + entry = default; + return options.GamePathEntries; + } + + if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed) + { + entry = existed; + return options.GamePathEntries; + } + + entry = GamePathEntry.Create(options.GamePath); + return [.. options.GamePathEntries, entry]; + } + + public static ImmutableList RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected) + { + if (entry is not null) + { + if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase)) + { + options.GamePath = string.Empty; + } + + options.GamePathEntries = options.GamePathEntries.Remove(entry); + } + + return options.GetGamePathEntries(out selected); + } + + public static ImmutableList UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath) + { + options.GamePath = gamePath; + ImmutableList entries = options.GetGamePathEntries(out _); + options.GamePathEntries = entries; + return entries; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs index f230c984..09be982f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -16,12 +16,12 @@ internal sealed partial class GamePackageService : IGamePackageService { private readonly PackageConverter packageConverter; private readonly IServiceProvider serviceProvider; + private readonly LaunchOptions launchOptions; private readonly ITaskContext taskContext; - private readonly AppOptions appOptions; public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { - if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) + if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) { return false; } @@ -58,7 +58,7 @@ internal sealed partial class GamePackageService : IGamePackageService string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; await taskContext.SwitchToMainThreadAsync(); - appOptions.GamePath = Path.Combine(gameFolder, exeName); + launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName)); } await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs new file mode 100644 index 00000000..8d8dfd2e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.PathAbstraction; + +internal sealed class GamePathEntry +{ + [JsonPropertyName("Path")] + public string Path { get; set; } = default!; + + [JsonIgnore] + public GamePathKind Kind { get => GetKind(Path); } + + public static GamePathEntry Create(string path) + { + return new() + { + Path = path, + }; + } + + private static GamePathKind GetKind(string path) + { + return GamePathKind.None; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs new file mode 100644 index 00000000..3271c606 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.PathAbstraction; + +internal enum GamePathKind +{ + None, + ChineseClient, + OverseaClient, + ChineseCloud, + OverseaCloud, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs similarity index 78% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs index 7f773c77..c1c9eff4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs @@ -2,20 +2,21 @@ // Licensed under the MIT license. using Snap.Hutao.Service.Game.Locator; +using Snap.Hutao.Service.Game.PathAbstraction; -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.PathAbstraction; [ConstructorGenerated] [Injection(InjectAs.Singleton, typeof(IGamePathService))] internal sealed partial class GamePathService : IGamePathService { private readonly IServiceProvider serviceProvider; - private readonly AppOptions appOptions; + private readonly LaunchOptions launchOptions; public async ValueTask> SilentGetGamePathAsync() { // Cannot find in setting - if (string.IsNullOrEmpty(appOptions.GamePath)) + if (string.IsNullOrEmpty(launchOptions.GamePath)) { IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); @@ -40,7 +41,7 @@ internal sealed partial class GamePathService : IGamePathService if (isOk) { // Save result. - appOptions.GamePath = path; + launchOptions.UpdateGamePathAndRefreshEntries(path); } else { @@ -48,9 +49,9 @@ internal sealed partial class GamePathService : IGamePathService } } - if (!string.IsNullOrEmpty(appOptions.GamePath)) + if (!string.IsNullOrEmpty(launchOptions.GamePath)) { - return new(true, appOptions.GamePath); + return new(true, launchOptions.GamePath); } else { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs index c0b09ccc..3c01c01c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.Game; +namespace Snap.Hutao.Service.Game.PathAbstraction; internal interface IGamePathService { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index e9d6e28a..c55ee88e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -52,7 +52,7 @@ internal sealed partial class GameProcessService : IGameProcessService return; } - if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) + if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) { ArgumentException.ThrowIfNullOrEmpty(gamePath); return; // null check passing, actually never reach. @@ -73,7 +73,7 @@ internal sealed partial class GameProcessService : IGameProcessService await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false); } - if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) + if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) { progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); try diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 9fd729b1..b9deafdf 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -38,7 +38,12 @@ - + + + + + + + Orientation="Horizontal">