refactor launch game view model

This commit is contained in:
Lightczx
2024-04-08 15:33:42 +08:00
parent f5dd5f4c1d
commit 65252f1f69
12 changed files with 136 additions and 89 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Snap.Hutao.Test.PlatformExtensions;
@@ -11,6 +12,7 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
[TestMethod]
@@ -41,6 +43,13 @@ public sealed class DependencyInjectionTest
}
}
[TestMethod]
public void LoggerWithInterfaceTypeCanBeResolved()
{
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
}
private interface IService
{
Guid Id { get; }

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />

View File

@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposedSlow())
if (serviceProvider.IsDisposed())
{
return;
}
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposedSlow())
if (serviceProvider.IsDisposed())
{
return;
}

View File

@@ -18,13 +18,22 @@ internal static class ServiceProviderExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
public static bool IsDisposed(this IServiceProvider? serviceProvider)
{
if (serviceProvider is null)
{
return true;
}
if (serviceProvider is ServiceProvider serviceProviderImpl)
{
return GetPrivateDisposed(serviceProviderImpl);
}
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
}
// private bool _disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
}

View File

@@ -17,4 +17,5 @@ internal enum HutaoExceptionKind
// Service
GachaStatisticsInvalidItemId,
GameFpsUnlockingFailed,
GameConfigInvalidChannelOptions,
}

View File

@@ -27,4 +27,27 @@ internal sealed partial class IdentifyMonitorWindow : Window
}
public string Monitor { get; private set; }
public static async ValueTask IdentifyAllMonitorsAsync(int secondsDelay)
{
List<IdentifyMonitorWindow> windows = [];
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{
windows.Add(new IdentifyMonitorWindow(displayAreas[i], i + 1));
}
foreach (IdentifyMonitorWindow window in windows)
{
window.Activate();
}
await Delay.FromSeconds(secondsDelay).ConfigureAwait(true);
foreach (IdentifyMonitorWindow window in windows)
{
window.Close();
}
}
}

View File

@@ -36,4 +36,6 @@ internal sealed class GameFileSystem
public string GameConfigFilePath { get => gameConfigFilePath ??= Path.Combine(GameDirectory, GameConstants.ConfigFileName); }
public string PCGameSDKFilePath { get => pcGameSDKFilePath ??= Path.Combine(GameDirectory, GameConstants.PCGameSDKFilePath); }
public string ScreenShotDirectory { get => Path.Combine(GameDirectory, "ScreenShot"); }
}

View File

@@ -1,12 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Notification;
using System.Collections.Immutable;
namespace Snap.Hutao.ViewModel.Game;
internal interface IViewModelSupportLaunchExecution
{
void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry);
LaunchGameShared Shared { get; }
GameAccount? SelectedGameAccount { get; }
void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry)
{
// Do nothing
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Launching;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.ViewModel.Game;
internal static class LaunchGameLaunchExecution
{
public static async ValueTask LaunchExecutionAsync(this IViewModelSupportLaunchExecution launchExecution)
{
IServiceProvider root = Ioc.Default;
IInfoBarService infoBarService = root.GetRequiredService<IInfoBarService>();
ILogger<IViewModelSupportLaunchExecution> logger = root.GetRequiredService<ILogger<IViewModelSupportLaunchExecution>>();
LaunchScheme? scheme = launchExecution.Shared.GetCurrentLaunchSchemeFromConfigFile();
try
{
// Root service provider is required.
LaunchExecutionContext context = new(root, launchExecution, scheme, launchExecution.SelectedGameAccount);
LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false);
if (result.Kind is not LaunchExecutionResultKind.Ok)
{
infoBarService.Warning(result.ErrorMessage);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
}
}

View File

@@ -41,7 +41,7 @@ internal sealed partial class LaunchGameShared
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}");
HutaoException.Throw(HutaoExceptionKind.GameConfigInvalidChannelOptions, $"不支持的 ChannelOptions: {options}");
}
}
@@ -70,7 +70,9 @@ internal sealed partial class LaunchGameShared
string persistentScriptVersionFile = Path.Combine(gameFileSystem.GameDirectory, dataFolder, "Persistent", "ScriptVersion");
string version = await File.ReadAllTextAsync(persistentScriptVersionFile).ConfigureAwait(false);
LaunchGameConfigurationFixDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameConfigurationFixDialog>().ConfigureAwait(false);
LaunchGameConfigurationFixDialog dialog = await contentDialogFactory
.CreateInstanceAsync<LaunchGameConfigurationFixDialog>()
.ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
dialog.KnownSchemes = KnownLaunchSchemes.Get().Where(scheme => scheme.IsOversea == isOversea);

View File

@@ -60,6 +60,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
private GamePathEntry? selectedGamePathEntry;
private GameAccountFilter? gameAccountFilter;
LaunchGameShared IViewModelSupportLaunchExecution.Shared { get => launchGameShared; }
public LaunchOptions LaunchOptions { get => launchOptions; }
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
@@ -83,7 +85,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
[AlsoAsyncSets(nameof(SelectedScheme), nameof(GameAccountsView))]
public bool GamePathSelectedAndValid
{
get => gamePathSelectedAndValid;
@@ -149,7 +150,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
[AlsoSets(nameof(GamePathSelectedAndValid))]
public GamePathEntry? SelectedGamePathEntry
{
get => selectedGamePathEntry;
@@ -184,25 +184,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
[Command("IdentifyMonitorsCommand")]
private static async Task IdentifyMonitorsAsync()
{
List<IdentifyMonitorWindow> windows = [];
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{
windows.Add(new IdentifyMonitorWindow(displayAreas[i], i + 1));
}
foreach (IdentifyMonitorWindow window in windows)
{
window.Activate();
}
await Delay.FromSeconds(3).ConfigureAwait(true);
foreach (IdentifyMonitorWindow window in windows)
{
window.Close();
}
await IdentifyMonitorWindow.IdentifyAllMonitorsAsync(3);
}
[Command("SetGamePathCommand")]
@@ -234,21 +216,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
[Command("LaunchCommand")]
private async Task LaunchAsync()
{
try
{
LaunchExecutionContext context = new(Ioc.Default, this, SelectedScheme, SelectedGameAccount);
LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false);
if (result.Kind is not LaunchExecutionResultKind.Ok)
{
infoBarService.Warning(result.ErrorMessage);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
await this.LaunchExecutionAsync().ConfigureAwait(false);
}
[Command("DetectGameAccountCommand")]
@@ -269,7 +237,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
SelectedGameAccount = account;
}
}
catch (UserdataCorruptedException ex)
catch (Exception ex)
{
infoBarService.Error(ex);
}
@@ -278,47 +246,54 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
[Command("AttachGameAccountCommand")]
private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
{
if (gameAccount is not null)
if (gameAccount is null)
{
if (userService.Current?.SelectedUserGameRole is { } role)
{
gameService.AttachGameAccountToUid(gameAccount, role.GameUid);
}
else
{
infoBarService.Warning(SH.MustSelectUserAndUid);
}
return;
}
if (userService.Current?.SelectedUserGameRole is { } role)
{
gameService.AttachGameAccountToUid(gameAccount, role.GameUid);
}
else
{
infoBarService.Warning(SH.MustSelectUserAndUid);
}
}
[Command("ModifyGameAccountCommand")]
private async Task ModifyGameAccountAsync(GameAccount? gameAccount)
{
if (gameAccount is not null)
if (gameAccount is null)
{
await gameService.ModifyGameAccountAsync(gameAccount).ConfigureAwait(false);
return;
}
await gameService.ModifyGameAccountAsync(gameAccount).ConfigureAwait(false);
}
[Command("RemoveGameAccountCommand")]
private async Task RemoveGameAccountAsync(GameAccount? gameAccount)
{
if (gameAccount is not null)
if (gameAccount is null)
{
await gameService.RemoveGameAccountAsync(gameAccount).ConfigureAwait(false);
return;
}
await gameService.RemoveGameAccountAsync(gameAccount).ConfigureAwait(false);
}
[Command("OpenScreenshotFolderCommand")]
private async Task OpenScreenshotFolderAsync()
{
string game = LaunchOptions.GamePath;
string? directory = Path.GetDirectoryName(game);
ArgumentException.ThrowIfNullOrEmpty(directory);
string screenshot = Path.Combine(directory, "ScreenShot");
if (Directory.Exists(screenshot))
if (!launchOptions.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
await Windows.System.Launcher.LaunchFolderPathAsync(screenshot);
return;
}
if (Directory.Exists(gameFileSystem.ScreenShotDirectory))
{
await Windows.System.Launcher.LaunchFolderPathAsync(gameFileSystem.ScreenShotDirectory);
}
}
@@ -326,12 +301,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
{
if (SetProperty(ref selectedScheme, value, nameof(SelectedScheme)))
{
UpdateGameResourceAsync(value).SafeForget();
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
// Clear the selected game account to prevent setting
// incorrect CN/OS account when scheme not match
SelectedGameAccount = default;
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
UpdateGameResourceAsync(value).SafeForget();
}
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)

View File

@@ -22,7 +22,6 @@ namespace Snap.Hutao.ViewModel.Game;
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>, IViewModelSupportLaunchExecution
{
private readonly LaunchStatusOptions launchStatusOptions;
private readonly ILogger<LaunchGameViewModelSlim> logger;
private readonly LaunchGameShared launchGameShared;
private readonly IInfoBarService infoBarService;
private readonly IGameServiceFacade gameService;
@@ -32,6 +31,8 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
private GameAccount? selectedGameAccount;
private GameAccountFilter? gameAccountFilter;
LaunchGameShared IViewModelSupportLaunchExecution.Shared { get => launchGameShared; }
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
public AdvancedCollectionView<GameAccount>? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); }
@@ -41,10 +42,6 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
/// </summary>
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry)
{
}
/// <inheritdoc/>
protected override async Task OpenUIAsync()
{
@@ -59,7 +56,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
}
}
catch (UserdataCorruptedException ex)
catch (Exception ex)
{
infoBarService.Error(ex);
}
@@ -76,23 +73,6 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
[Command("LaunchCommand")]
private async Task LaunchAsync()
{
IInfoBarService infoBarService = ServiceProvider.GetRequiredService<IInfoBarService>();
LaunchScheme? scheme = launchGameShared.GetCurrentLaunchSchemeFromConfigFile();
try
{
LaunchExecutionContext context = new(Ioc.Default, this, scheme, SelectedGameAccount);
LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false);
if (result.Kind is not LaunchExecutionResultKind.Ok)
{
infoBarService.Warning(result.ErrorMessage);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex);
}
await this.LaunchExecutionAsync().ConfigureAwait(false);
}
}