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))
{