diff --git a/res/HutaoIcon.psd b/res/HutaoIcon.psd deleted file mode 100644 index 42f3a7f7..00000000 Binary files a/res/HutaoIcon.psd and /dev/null differ diff --git a/res/HutaoIcon2.jpg b/res/HutaoIcon2.jpg new file mode 100644 index 00000000..358ca485 Binary files /dev/null and b/res/HutaoIcon2.jpg differ diff --git a/res/HutaoIcon2.png b/res/HutaoIcon2.png new file mode 100644 index 00000000..00fadf48 Binary files /dev/null and b/res/HutaoIcon2.png differ diff --git a/res/HutaoIconSource.jpg b/res/HutaoIconSource.jpg new file mode 100644 index 00000000..2d6e0676 Binary files /dev/null and b/res/HutaoIconSource.jpg differ diff --git a/res/HutaoIconSourceTransparentBackground.png b/res/HutaoIconSourceTransparentBackground.png new file mode 100644 index 00000000..30229904 Binary files /dev/null and b/res/HutaoIconSourceTransparentBackground.png differ diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs new file mode 100644 index 00000000..7282591d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Abstraction; + +namespace Snap.Hutao.Core.DependencyInjection; + +/// +/// 命名服务扩展 +/// +internal static class NamedServiceExtension +{ + /// + /// 选择对应的服务 + /// + /// 服务类型 + /// 服务集合 + /// 名称 + /// 对应的服务 + public static TService Pick(this IEnumerable services, string name) + where TService : INamedService + { + return services.Single(s => s.Name == name); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs index f0a90f3e..126801ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs @@ -24,14 +24,14 @@ internal static class FileOperation { if (overwrite) { - File.Move(sourceFileName, destFileName, overwrite); + File.Move(sourceFileName, destFileName, true); return true; } else { if (!File.Exists(destFileName)) { - File.Move(sourceFileName, destFileName, overwrite); + File.Move(sourceFileName, destFileName, false); return true; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Option/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Option/AppOptions.cs deleted file mode 100644 index ae98a9cb..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Option/AppOptions.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.UI.Windowing; -using Snap.Hutao.Core.Database; -using Snap.Hutao.Model.Entity; -using Snap.Hutao.Model.Entity.Database; -using System.Globalization; -using Windows.Globalization; - -namespace Snap.Hutao.Option; - -/// -/// 应用程序选项 -/// -[Injection(InjectAs.Singleton)] -internal sealed class AppOptions : IOptions -{ - private readonly IServiceScopeFactory serviceScopeFactory; - - /// - /// 构造一个新的应用程序选项 - /// - /// 服务范围工厂 - public AppOptions(IServiceScopeFactory serviceScopeFactory) - { - this.serviceScopeFactory = serviceScopeFactory; - } - - /// - /// 游戏路径 - /// - public string? GamePath - { - get - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - return appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.GamePath)?.Value; - } - } - - set - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.GamePath); - appDbContext.Settings.AddAndSave(new(SettingEntry.GamePath, value)); - } - } - } - - /// - /// 游戏路径 - /// - public bool IsEmptyHistoryWishVisible - { - get - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible)?.Value; - return value != null && bool.Parse(value); - } - } - - set - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible); - appDbContext.Settings.AddAndSave(new(SettingEntry.IsEmptyHistoryWishVisible, value.ToString())); - } - } - } - - /// - /// 背景类型 默认 Mica - /// - public Core.Windowing.BackdropType BackdropType - { - get - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.SystemBackdropType)?.Value; - return Enum.Parse(value ?? nameof(Core.Windowing.BackdropType.Mica)); - } - } - - set - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.SystemBackdropType); - appDbContext.Settings.AddAndSave(new(SettingEntry.SystemBackdropType, value.ToString())); - } - } - } - - /// - /// 当前语言 - /// - public CultureInfo CurrentCulture - { - get - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.Culture)?.Value; - return value != null ? CultureInfo.GetCultureInfo(value) : CultureInfo.CurrentCulture; - } - } - - set - { - using (IServiceScope scope = serviceScopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.Culture); - appDbContext.Settings.AddAndSave(new(SettingEntry.Culture, value.Name)); - } - } - } - - /// - public AppOptions Value { get => this; } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index 83eb4dfb..c5d108a1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -4,8 +4,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; -using Snap.Hutao.Core.Setting; -using Snap.Hutao.Option; +using Snap.Hutao.Service; using System.Globalization; using System.Runtime.InteropServices; using Windows.Globalization; @@ -33,8 +32,7 @@ public static partial class Program // by adding the using statement, we can dispose the injected services when we closing using (ServiceProvider serviceProvider = InitializeDependencyInjection()) { - AppOptions options = serviceProvider.GetRequiredService(); - InitializeCulture(options.CurrentCulture); + InitializeCulture(serviceProvider.GetRequiredService().CurrentCulture); // In a Desktop app this runs a message pump internally, // and does not return until the application shuts down. diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs index b3ea980d..737cbf0c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs @@ -3724,7 +3724,7 @@ namespace Snap.Hutao.Resource.Localization { } /// - /// 查找类似 多倍启动你的原神,你可以使用胡桃来多次打开原神并且不受到影响 的本地化字符串。 + /// 查找类似 同时运行多个游戏客户端 的本地化字符串。 /// internal static string ViewPageLaunchGameMultipleInstancesDescription { get { @@ -3733,7 +3733,7 @@ namespace Snap.Hutao.Resource.Localization { } /// - /// 查找类似 多倍启动 的本地化字符串。 + /// 查找类似 多客户端 的本地化字符串。 /// internal static string ViewPageLaunchGameMultipleInstancesHeader { get { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 52e80233..29cf45c9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1339,10 +1339,10 @@ 显示器 - 多倍启动你的原神,你可以使用胡桃来多次打开原神并且不受到影响 + 同时运行多个游戏客户端 - 多倍启动 + 多客户端 游戏选项 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs new file mode 100644 index 00000000..fa0dd172 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs @@ -0,0 +1,176 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using System.Globalization; + +namespace Snap.Hutao.Service; + +/// +/// 应用程序选项 +/// +[Injection(InjectAs.Singleton)] +internal sealed class AppOptions : ObservableObject, IOptions +{ + private readonly IServiceScopeFactory serviceScopeFactory; + + private string? gamePath; + private bool? isEmptyHistoryWishVisible; + private Core.Windowing.BackdropType? backdropType; + private CultureInfo? currentCulture; + + /// + /// 构造一个新的应用程序选项 + /// + /// 服务范围工厂 + public AppOptions(IServiceScopeFactory serviceScopeFactory) + { + this.serviceScopeFactory = serviceScopeFactory; + } + + /// + /// 游戏路径 + /// + public string GamePath + { + get + { + if (gamePath == null) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + gamePath = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.GamePath)?.Value ?? string.Empty; + } + } + + return gamePath; + } + + set + { + if (SetProperty(ref gamePath, value)) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.GamePath); + appDbContext.Settings.AddAndSave(new(SettingEntry.GamePath, value)); + } + } + } + } + + /// + /// 游戏路径 + /// + public bool IsEmptyHistoryWishVisible + { + get + { + if (isEmptyHistoryWishVisible == null) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible)?.Value; + isEmptyHistoryWishVisible = value != null && bool.Parse(value); + } + } + + return isEmptyHistoryWishVisible.Value; + } + + set + { + if (SetProperty(ref isEmptyHistoryWishVisible, value)) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible); + appDbContext.Settings.AddAndSave(new(SettingEntry.IsEmptyHistoryWishVisible, value.ToString())); + } + } + } + } + + /// + /// 背景类型 默认 Mica + /// + public Core.Windowing.BackdropType BackdropType + { + get + { + if (backdropType == null) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.SystemBackdropType)?.Value; + backdropType = Enum.Parse(value ?? nameof(Core.Windowing.BackdropType.Mica)); + } + } + + return backdropType.Value; + } + + set + { + if (SetProperty(ref backdropType, value)) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.SystemBackdropType); + appDbContext.Settings.AddAndSave(new(SettingEntry.SystemBackdropType, value.ToString())); + + scope.ServiceProvider.GetRequiredService().Send(new Message.BackdropTypeChangedMessage(value)); + } + } + } + } + + /// + /// 当前语言 + /// + public CultureInfo CurrentCulture + { + get + { + if (currentCulture == null) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.Culture)?.Value; + currentCulture = value != null ? CultureInfo.GetCultureInfo(value) : CultureInfo.CurrentCulture; + } + } + + return currentCulture; + } + + set + { + if (SetProperty(ref currentCulture, value)) + { + using (IServiceScope scope = serviceScopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.Culture); + appDbContext.Settings.AddAndSave(new(SettingEntry.Culture, value.Name)); + } + } + } + } + + /// + public AppOptions Value { get => this; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index aa5bdda8..97d78cc2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Snap.Hutao.Core; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO.Ini; using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Model.Entity; @@ -37,7 +38,9 @@ internal sealed class GameService : IGameService private readonly IServiceScopeFactory scopeFactory; private readonly IMemoryCache memoryCache; private readonly PackageConverter packageConverter; - private readonly SemaphoreSlim gameSemaphore = new(1); + private readonly LaunchOptions launchOptions; + private readonly AppOptions appOptions; + private volatile int runningGamesCounter; private ObservableCollection? gameAccounts; @@ -47,107 +50,69 @@ internal sealed class GameService : IGameService /// 范围工厂 /// 内存缓存 /// 游戏文件包转换器 - public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter) + /// 启动游戏选项 + /// 应用选项 + public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter, LaunchOptions launchOptions, AppOptions appOptions) { this.scopeFactory = scopeFactory; this.memoryCache = memoryCache; this.packageConverter = packageConverter; + this.launchOptions = launchOptions; + this.appOptions = appOptions; } /// public async ValueTask> GetGamePathAsync() { - if (memoryCache.TryGetValue(GamePathKey, out object? value)) - { - return new(true, (value as string)!); - } - else - { - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - SettingEntry entry = await appDbContext.Settings.SingleOrAddAsync(SettingEntry.GamePath, string.Empty).ConfigureAwait(false); - - // Cannot find in setting - if (string.IsNullOrEmpty(entry.Value)) - { - IEnumerable gameLocators = scope.ServiceProvider.GetRequiredService>(); - - // Try locate by unity log - IGameLocator locator = gameLocators.Single(l => l.Name == nameof(UnityLogGameLocator)); - ValueResult result = await locator.LocateGamePathAsync().ConfigureAwait(false); - - if (!result.IsOk) - { - // Try locate by registry - locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator)); - result = await locator.LocateGamePathAsync().ConfigureAwait(false); - } - - if (result.IsOk) - { - // Save result. - entry.Value = result.Value; - await appDbContext.Settings.UpdateAndSaveAsync(entry).ConfigureAwait(false); - } - else - { - return new(false, SH.ServiceGamePathLocateFailed); - } - } - - if (entry.Value == null) - { - return new(false, null!); - } - - // Set cache and return. - string path = memoryCache.Set(GamePathKey, entry.Value); - return new(true, path); - } - } - } - - /// - public string GetGamePathSkipLocator() - { - if (memoryCache.TryGetValue(GamePathKey, out object? value)) - { - return (value as string)!; - } - else - { - using (IServiceScope scope = scopeFactory.CreateScope()) - { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); - SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty); - - // Set cache and return. - return memoryCache.Set(GamePathKey, entry.Value!); - } - } - } - - /// - public void OverwriteGamePath(string path) - { - // sync cache - memoryCache.Set(GamePathKey, path); - using (IServiceScope scope = scopeFactory.CreateScope()) { - AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + // Cannot find in setting + if (string.IsNullOrEmpty(appOptions.GamePath)) + { + IEnumerable gameLocators = scope.ServiceProvider.GetRequiredService>(); - SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty); - entry.Value = path; - appDbContext.Settings.UpdateAndSave(entry); + // Try locate by unity log + ValueResult result = await gameLocators + .Pick(nameof(UnityLogGameLocator)) + .LocateGamePathAsync() + .ConfigureAwait(false); + + if (!result.IsOk) + { + // Try locate by registry + result = await gameLocators + .Pick(nameof(RegistryLauncherLocator)) + .LocateGamePathAsync() + .ConfigureAwait(false); + } + + if (result.IsOk) + { + // Save result. + await ThreadHelper.SwitchToMainThreadAsync(); + appOptions.GamePath = result.Value; + } + else + { + return new(false, SH.ServiceGamePathLocateFailed); + } + } + + if (!string.IsNullOrEmpty(appOptions.GamePath)) + { + return new(true, appOptions.GamePath); + } + else + { + return new(false, null!); + } } } /// public MultiChannel GetMultiChannel() { - string gamePath = GetGamePathSkipLocator(); + string gamePath = appOptions.GamePath; string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); if (!File.Exists(configPath)) @@ -157,9 +122,9 @@ internal sealed class GameService : IGameService using (FileStream stream = File.OpenRead(configPath)) { - List elements = IniSerializer.Deserialize(stream).ToList(); - string? channel = elements.OfType().FirstOrDefault(p => p.Key == "channel")?.Value; - string? subChannel = elements.OfType().FirstOrDefault(p => p.Key == "sub_channel")?.Value; + IEnumerable parameters = IniSerializer.Deserialize(stream).ToList().OfType(); + string? channel = parameters.FirstOrDefault(p => p.Key == "channel")?.Value; + string? subChannel = parameters.FirstOrDefault(p => p.Key == "sub_channel")?.Value; return new(channel, subChannel); } @@ -168,7 +133,7 @@ internal sealed class GameService : IGameService /// public bool SetMultiChannel(LaunchScheme scheme) { - string gamePath = GetGamePathSkipLocator(); + string gamePath = appOptions.GamePath; string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName); List elements = null!; @@ -232,7 +197,7 @@ internal sealed class GameService : IGameService /// public async Task EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { - string gamePath = GetGamePathSkipLocator(); + string gamePath = appOptions.GamePath; string gameFolder = Path.GetDirectoryName(gamePath)!; string gameFileName = Path.GetFileName(gamePath); @@ -260,7 +225,9 @@ internal sealed class GameService : IGameService { // We need to change the gamePath if we switched. string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; - OverwriteGamePath(Path.Combine(gameFolder, exeName)); + + await ThreadHelper.SwitchToMainThreadAsync(); + appOptions.GamePath = Path.Combine(gameFolder, exeName); } else { @@ -284,9 +251,15 @@ internal sealed class GameService : IGameService /// public bool IsGameRunning() { - if (gameSemaphore.CurrentCount == 0) + if (runningGamesCounter == 0) { - return true; + return false; + } + + if (launchOptions.MultipleInstances) + { + // If multiple instances is enabled, always treat as not running. + return false; } return Process.GetProcessesByName(YuanShenProcessName).Any() @@ -310,105 +283,45 @@ internal sealed class GameService : IGameService } /// - public async ValueTask LaunchAsync(LaunchOptions options) + public async ValueTask LaunchAsync() { - if (!options.MultipleInstances && IsGameRunning()) + if (IsGameRunning()) { return; } - string gamePath = GetGamePathSkipLocator(); - + string gamePath = appOptions.GamePath; if (string.IsNullOrWhiteSpace(gamePath)) { return; } - // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html - string commandLine = new CommandLineBuilder() - .AppendIf("-popupwindow", options.IsBorderless) - .AppendIf("-window-mode", options.IsExclusive, "exclusive") - .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) - .Append("-screen-width", options.ScreenWidth) - .Append("-screen-height", options.ScreenHeight) - .Append("-monitor", options.Monitor.Value) - .ToString(); + Process game = ProcessInterop.PrepareGameProcess(launchOptions, gamePath); - Process game = new() + try { - StartInfo = new() - { - Arguments = commandLine, - FileName = gamePath, - UseShellExecute = true, - Verb = "runas", - WorkingDirectory = Path.GetDirectoryName(gamePath), - }, - }; + Interlocked.Increment(ref runningGamesCounter); + bool isElevated = Activation.GetElevated(); - using (await gameSemaphore.EnterAsync().ConfigureAwait(false)) - { - if (options.MultipleInstances && Activation.GetElevated()) + game.Start(); + if (isElevated && launchOptions.MultipleInstances) { - await LaunchGameAsync(game, gamePath); + await ProcessInterop.DisableProtectionAsync(gamePath).ConfigureAwait(false); + } + + if (isElevated && launchOptions.UnlockFps) + { + await ProcessInterop.UnlockFpsAsync(game, launchOptions).ConfigureAwait(false); } else { - await LaunchGameAsync(game); - } - - if (options.UnlockFps) - { - IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, options.TargetFps); - - TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); - TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); - TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); - - await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false); + await game.WaitForExitAsync().ConfigureAwait(false); } } - } - - /// - /// 为了实现多开 需要修改mhypbase.dll名称 这是必须的步骤 - /// - /// 游戏线程 - /// 游戏路径 - /// 是否成功替换文件 - public async Task LaunchMultipleInstancesGameAsync(Process gameProcess, string? gamePath) - { - if (gamePath == null) + finally { - return false; + Interlocked.Decrement(ref runningGamesCounter); } - - DirectoryInfo directoryInfo = new DirectoryInfo(gamePath); - if (directoryInfo.Parent == null) - { - return false; - } - - string? gameDirectory = directoryInfo.Parent.FullName.ToString(); - string? mhypbasePath = $@"{gameDirectory}\mhypbase.dll"; - string? tempPath = $@"{gameDirectory}\mhypbase.dll.backup"; - if (File.Exists(mhypbasePath)) - { - File.Move(mhypbasePath, tempPath); - } - else if (!File.Exists(tempPath)) - { - return false; - } - - gameProcess.Start(); - - // wait 12sec for loading library files - await Task.Delay(12000); - - File.Move(tempPath, mhypbasePath); - - return false; } /// @@ -511,24 +424,4 @@ internal sealed class GameService : IGameService return (launchScheme.IsOversea && gameFileName == GenshinImpactFileName) || (!launchScheme.IsOversea && gameFileName == YuanShenFileName); } - - private async Task LaunchGameAsync(Process gameProcess, string? gamePath = null) - { - try - { - if (gamePath == null) - { - gameProcess.Start(); - } - else - { - await LaunchMultipleInstancesGameAsync(gameProcess, gamePath); - return; - } - } - catch - { - return; - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs index 5a32b18e..7e44433c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs @@ -39,12 +39,6 @@ internal interface IGameService /// 结果 ValueTask> GetGamePathAsync(); - /// - /// 获取游戏路径,跳过异步定位器 - /// - /// 游戏路径,当路径无效时会设置并返回 - string GetGamePathSkipLocator(); - /// /// 获取多通道值 /// @@ -60,9 +54,8 @@ internal interface IGameService /// /// 异步启动 /// - /// 启动配置 /// 任务 - ValueTask LaunchAsync(LaunchOptions options); + ValueTask LaunchAsync(); /// /// 异步修改游戏账号名称 @@ -71,12 +64,6 @@ internal interface IGameService /// 任务 ValueTask ModifyGameAccountAsync(GameAccount gameAccount); - /// - /// 重写游戏路径 - /// - /// 路径 - void OverwriteGamePath(string path); - /// /// 异步尝试移除账号 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index 6451d8cd..749aef85 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -25,7 +25,6 @@ internal sealed class PackageConverter /// /// 构造一个新的游戏文件转换器 /// - /// 资源客户端 /// Json序列化选项 /// http客户端 public PackageConverter(JsonSerializerOptions options, HttpClient httpClient) @@ -45,7 +44,6 @@ internal sealed class PackageConverter /// 替换结果与资源 public async Task EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress progress) { - await ThreadHelper.SwitchToBackgroundAsync(); string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath; Uri pkgVersionUri = $"{scatteredFilesUrl}/pkg_version".ToUri(); ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese; @@ -85,8 +83,8 @@ internal sealed class PackageConverter { string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup"); string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll"); - string sdkVersionBackup = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version.backup"); - string sdkVersion = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version"); + string sdkVersionBackup = Path.Combine(gameFolder, "sdk_pkg_version.backup"); + string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version"); // Only bilibili's sdk is not null if (resource.Sdk != null) @@ -195,6 +193,7 @@ internal sealed class PackageConverter { const int bufferSize = 81920; + int reportCounter = 0; long totalBytesRead = 0; int bytesRead; Memory buffer = new byte[bufferSize]; @@ -205,7 +204,11 @@ internal sealed class PackageConverter await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false); totalBytesRead += bytesRead; - progress.Report(new(name, totalBytesRead, totalBytes)); + + if ((++reportCounter) % 10 == 0) + { + progress.Report(new(name, totalBytesRead, totalBytes)); + } } while (bytesRead > 0); } @@ -318,7 +321,7 @@ internal sealed class PackageConverter if (versionFileName == "sdk_pkg_version") { - // Skiping the sdk_pkg_version file, + // Skipping the sdk_pkg_version file, // it can't be claimed from remote. continue; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs new file mode 100644 index 00000000..8c284af0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs @@ -0,0 +1,86 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Service.Game.Unlocker; +using System.Diagnostics; +using System.IO; + +namespace Snap.Hutao.Service.Game; + +/// +/// 进程互操作 +/// +internal static class ProcessInterop +{ + /// + /// 获取初始化后的游戏进程 + /// + /// 启动选项 + /// 游戏路径 + /// 初始化后的游戏进程 + public static Process PrepareGameProcess(LaunchOptions options, string gamePath) + { + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html + string commandLine = new CommandLineBuilder() + .AppendIf("-popupwindow", options.IsBorderless) + .AppendIf("-window-mode", options.IsExclusive, "exclusive") + .Append("-screen-fullscreen", options.IsFullScreen ? 1 : 0) + .Append("-screen-width", options.ScreenWidth) + .Append("-screen-height", options.ScreenHeight) + .Append("-monitor", options.Monitor.Value) + .ToString(); + + return new() + { + StartInfo = new() + { + Arguments = commandLine, + FileName = gamePath, + UseShellExecute = true, + Verb = "runas", + WorkingDirectory = Path.GetDirectoryName(gamePath), + }, + }; + } + + /// + /// 解锁帧率 + /// + /// 游戏进程 + /// 启动选项 + /// 任务 + public static Task UnlockFpsAsync(Process game, LaunchOptions options) + { + IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, options.TargetFps); + + TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); + TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); + TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); + + return unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay); + } + + /// + /// 尝试禁用mhypbase + /// + /// 游戏路径 + /// 是否禁用成功 + public static async Task DisableProtectionAsync(string gamePath) + { + string? gameFolder = Path.GetDirectoryName(gamePath); + if (!string.IsNullOrEmpty(gameFolder)) + { + string mhypbaseDll = Path.Combine(gameFolder, "mhypbase.dll"); + string mhypbaseDllBackup = Path.Combine(gameFolder, "mhypbase.dll.backup"); + + File.Move(mhypbaseDll, mhypbaseDllBackup, true); + await Task.Delay(TimeSpan.FromSeconds(12)).ConfigureAwait(false); + File.Move(mhypbaseDllBackup, mhypbaseDll, true); + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index d33f1ace..05972186 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -260,7 +260,7 @@ + Icon=""> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -143,7 +139,7 @@ Message="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}" Severity="Informational"/> diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index e0776b52..d3b072ca 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -8,6 +8,7 @@ using Snap.Hutao.Control.Extension; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Navigation; @@ -54,7 +55,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel Options = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; - LaunchCommand = new AsyncRelayCommand(LaunchAsync); + LaunchCommand = new AsyncRelayCommand(LaunchAsync, AsyncRelayCommandOptions.AllowConcurrentExecutions); DetectGameAccountCommand = new AsyncRelayCommand(DetectGameAccountAsync); ModifyGameAccountCommand = new AsyncRelayCommand(ModifyGameAccountAsync); RemoveGameAccountCommand = new AsyncRelayCommand(RemoveGameAccountAsync); @@ -143,7 +144,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel /// protected override async Task OpenUIAsync() { - if (File.Exists(gameService.GetGamePathSkipLocator())) + if (File.Exists(serviceProvider.GetRequiredService().GamePath)) { try { @@ -205,11 +206,6 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel { IInfoBarService infoBarService = serviceProvider.GetRequiredService(); - if (!Options.MultipleInstances && gameService.IsGameRunning()) - { - return; - } - if (SelectedScheme != null) { try @@ -239,7 +235,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel } } - await gameService.LaunchAsync(Options).ConfigureAwait(false); + await gameService.LaunchAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -300,7 +296,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel private async Task OpenScreenshotFolderAsync() { - string game = gameService.GetGamePathSkipLocator(); + string game = serviceProvider.GetRequiredService().GamePath; string screenshot = Path.Combine(Path.GetDirectoryName(game)!, "ScreenShot"); if (Directory.Exists(screenshot)) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs index ff721ff0..8d7e671a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs @@ -14,6 +14,7 @@ using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Model; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.GachaLog.QueryProvider; using Snap.Hutao.Service.Game; @@ -36,8 +37,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel private readonly AppDbContext appDbContext; private readonly IGameService gameService; private readonly ILogger logger; - private readonly SettingEntry isEmptyHistoryWishVisibleEntry; - private readonly SettingEntry selectedBackdropTypeEntry; + private readonly List> backdropTypes = new() { new("Acrylic", BackdropType.Acrylic), @@ -55,7 +55,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel private bool isEmptyHistoryWishVisible; private string gamePath; - private NameValue selectedBackdropType; + private NameValue? selectedBackdropType; private NameValue? selectedCulture; /// @@ -68,22 +68,11 @@ internal sealed class SettingViewModel : Abstraction.ViewModel gameService = serviceProvider.GetRequiredService(); logger = serviceProvider.GetRequiredService>(); Experimental = serviceProvider.GetRequiredService(); + Options = serviceProvider.GetRequiredService(); this.serviceProvider = serviceProvider; - isEmptyHistoryWishVisibleEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.IsEmptyHistoryWishVisible, Core.StringLiterals.False); - IsEmptyHistoryWishVisible = bool.Parse(isEmptyHistoryWishVisibleEntry.Value!); - - string? cultureName = appDbContext.Settings.SingleOrAdd(SettingEntry.Culture, CultureInfo.CurrentCulture.Name).Value; - selectedCulture = cultures.FirstOrDefault(c => c.Value == cultureName); - - selectedBackdropTypeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString()); - BackdropType type = Enum.Parse(selectedBackdropTypeEntry.Value!); - - // prevent unnecessary backdrop setting. - selectedBackdropType = backdropTypes.Single(t => t.Value == type); - OnPropertyChanged(nameof(SelectedBackdropType)); - - GamePath = gameService.GetGamePathSkipLocator(); + selectedCulture = cultures.FirstOrDefault(c => c.Value == Options.CurrentCulture.Name); + selectedBackdropType = backdropTypes.Single(t => t.Value == Options.BackdropType); SetGamePathCommand = new AsyncRelayCommand(SetGamePathAsync); UpdateCheckCommand = new AsyncRelayCommand(CheckUpdateAsync); @@ -122,30 +111,9 @@ internal sealed class SettingViewModel : Abstraction.ViewModel } /// - /// 空的历史卡池是否可见 + /// 应用程序设置 /// - public bool IsEmptyHistoryWishVisible - { - get => isEmptyHistoryWishVisible; - set - { - if (SetProperty(ref isEmptyHistoryWishVisible, value)) - { - isEmptyHistoryWishVisibleEntry.Value = value.ToString(); - appDbContext.Settings.UpdateAndSave(isEmptyHistoryWishVisibleEntry); - } - } - } - - /// - /// 游戏路径 - /// - public string GamePath - { - get => gamePath; - [MemberNotNull(nameof(gamePath))] - set => SetProperty(ref gamePath, value); - } + public AppOptions Options { get; } /// /// 背景类型 @@ -155,17 +123,14 @@ internal sealed class SettingViewModel : Abstraction.ViewModel /// /// 选中的背景类型 /// - public NameValue SelectedBackdropType + public NameValue? SelectedBackdropType { get => selectedBackdropType; - [MemberNotNull(nameof(selectedBackdropType))] set { if (SetProperty(ref selectedBackdropType, value) && value != null) { - selectedBackdropTypeEntry.Value = value.Value.ToString(); - appDbContext.Settings.UpdateAndSave(selectedBackdropTypeEntry); - serviceProvider.GetRequiredService().Send(new Message.BackdropTypeChangedMessage(value.Value)); + Options.BackdropType = value.Value; } } } @@ -185,9 +150,11 @@ internal sealed class SettingViewModel : Abstraction.ViewModel { if (SetProperty(ref selectedCulture, value)) { - SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.Culture, CultureInfo.CurrentCulture.Name); - entry.Value = selectedCulture?.Value; - appDbContext.Settings.UpdateAndSave(entry); + if (value != null) + { + Options.CurrentCulture = CultureInfo.GetCultureInfo(value.Value); + } + AppInstance.Restart(string.Empty); } } @@ -246,22 +213,19 @@ internal sealed class SettingViewModel : Abstraction.ViewModel private async Task SetGamePathAsync() { - IGameLocator locator = serviceProvider.GetRequiredService>() - .Single(l => l.Name == nameof(ManualGameLocator)); + IGameLocator locator = serviceProvider.GetRequiredService>().Pick(nameof(ManualGameLocator)); (bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false); if (isOk) { - gameService.OverwriteGamePath(path); await ThreadHelper.SwitchToMainThreadAsync(); - GamePath = path; + Options.GamePath = path; } } private void DeleteGameWebCache() { - IGameService gameService = serviceProvider.GetRequiredService(); - string gamePath = gameService.GetGamePathSkipLocator(); + string gamePath = Options.GamePath; if (!string.IsNullOrEmpty(gamePath)) {