mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
support multi-clienting
This commit is contained in:
Binary file not shown.
BIN
res/HutaoIcon2.jpg
Normal file
BIN
res/HutaoIcon2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 785 KiB |
BIN
res/HutaoIcon2.png
Normal file
BIN
res/HutaoIcon2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
res/HutaoIconSource.jpg
Normal file
BIN
res/HutaoIconSource.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 733 KiB |
BIN
res/HutaoIconSourceTransparentBackground.png
Normal file
BIN
res/HutaoIconSourceTransparentBackground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 命名服务扩展
|
||||
/// </summary>
|
||||
internal static class NamedServiceExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 选择对应的服务
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务类型</typeparam>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
public static TService Pick<TService>(this IEnumerable<TService> services, string name)
|
||||
where TService : INamedService
|
||||
{
|
||||
return services.Single(s => s.Name == name);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序选项
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class AppOptions : IOptions<AppOptions>
|
||||
{
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序选项
|
||||
/// </summary>
|
||||
/// <param name="serviceScopeFactory">服务范围工厂</param>
|
||||
public AppOptions(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
this.serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public string? GamePath
|
||||
{
|
||||
get
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.GamePath)?.Value;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.GamePath);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.GamePath, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public bool IsEmptyHistoryWishVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.IsEmptyHistoryWishVisible, value.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 背景类型 默认 Mica
|
||||
/// </summary>
|
||||
public Core.Windowing.BackdropType BackdropType
|
||||
{
|
||||
get
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.SystemBackdropType)?.Value;
|
||||
return Enum.Parse<Core.Windowing.BackdropType>(value ?? nameof(Core.Windowing.BackdropType.Mica));
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.SystemBackdropType);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.SystemBackdropType, value.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前语言
|
||||
/// </summary>
|
||||
public CultureInfo CurrentCulture
|
||||
{
|
||||
get
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.Culture);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.Culture, value.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AppOptions Value { get => this; }
|
||||
}
|
||||
@@ -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<AppOptions>();
|
||||
InitializeCulture(options.CurrentCulture);
|
||||
InitializeCulture(serviceProvider.GetRequiredService<AppOptions>().CurrentCulture);
|
||||
|
||||
// In a Desktop app this runs a message pump internally,
|
||||
// and does not return until the application shuts down.
|
||||
|
||||
@@ -3724,7 +3724,7 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 多倍启动你的原神,你可以使用胡桃来多次打开原神并且不受到影响 的本地化字符串。
|
||||
/// 查找类似 同时运行多个游戏客户端 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewPageLaunchGameMultipleInstancesDescription {
|
||||
get {
|
||||
@@ -3733,7 +3733,7 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 多倍启动 的本地化字符串。
|
||||
/// 查找类似 多客户端 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewPageLaunchGameMultipleInstancesHeader {
|
||||
get {
|
||||
|
||||
@@ -1339,10 +1339,10 @@
|
||||
<value>显示器</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMultipleInstancesDescription" xml:space="preserve">
|
||||
<value>多倍启动你的原神,你可以使用胡桃来多次打开原神并且不受到影响</value>
|
||||
<value>同时运行多个游戏客户端</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMultipleInstancesHeader" xml:space="preserve">
|
||||
<value>多倍启动</value>
|
||||
<value>多客户端</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
|
||||
<value>游戏选项</value>
|
||||
|
||||
176
src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs
Normal file
176
src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序选项
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class AppOptions : ObservableObject, IOptions<AppOptions>
|
||||
{
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
|
||||
private string? gamePath;
|
||||
private bool? isEmptyHistoryWishVisible;
|
||||
private Core.Windowing.BackdropType? backdropType;
|
||||
private CultureInfo? currentCulture;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序选项
|
||||
/// </summary>
|
||||
/// <param name="serviceScopeFactory">服务范围工厂</param>
|
||||
public AppOptions(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
this.serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public string GamePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (gamePath == null)
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.GamePath);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.GamePath, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public bool IsEmptyHistoryWishVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
if (isEmptyHistoryWishVisible == null)
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.IsEmptyHistoryWishVisible, value.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 背景类型 默认 Mica
|
||||
/// </summary>
|
||||
public Core.Windowing.BackdropType BackdropType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (backdropType == null)
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == SettingEntry.SystemBackdropType)?.Value;
|
||||
backdropType = Enum.Parse<Core.Windowing.BackdropType>(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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.SystemBackdropType);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.SystemBackdropType, value.ToString()));
|
||||
|
||||
scope.ServiceProvider.GetRequiredService<IMessenger>().Send(new Message.BackdropTypeChangedMessage(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前语言
|
||||
/// </summary>
|
||||
public CultureInfo CurrentCulture
|
||||
{
|
||||
get
|
||||
{
|
||||
if (currentCulture == null)
|
||||
{
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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>();
|
||||
appDbContext.Settings.ExecuteDeleteWhere(e => e.Key == SettingEntry.Culture);
|
||||
appDbContext.Settings.AddAndSave(new(SettingEntry.Culture, value.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AppOptions Value { get => this; }
|
||||
}
|
||||
@@ -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<GameAccount>? gameAccounts;
|
||||
|
||||
@@ -47,48 +50,47 @@ internal sealed class GameService : IGameService
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
/// <param name="memoryCache">内存缓存</param>
|
||||
/// <param name="packageConverter">游戏文件包转换器</param>
|
||||
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter)
|
||||
/// <param name="launchOptions">启动游戏选项</param>
|
||||
/// <param name="appOptions">应用选项</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ValueResult<bool, string>> 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<AppDbContext>();
|
||||
SettingEntry entry = await appDbContext.Settings.SingleOrAddAsync(SettingEntry.GamePath, string.Empty).ConfigureAwait(false);
|
||||
|
||||
// Cannot find in setting
|
||||
if (string.IsNullOrEmpty(entry.Value))
|
||||
if (string.IsNullOrEmpty(appOptions.GamePath))
|
||||
{
|
||||
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
|
||||
|
||||
// Try locate by unity log
|
||||
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(UnityLogGameLocator));
|
||||
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
ValueResult<bool, string> result = await gameLocators
|
||||
.Pick(nameof(UnityLogGameLocator))
|
||||
.LocateGamePathAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!result.IsOk)
|
||||
{
|
||||
// Try locate by registry
|
||||
locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
|
||||
result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
result = await gameLocators
|
||||
.Pick(nameof(RegistryLauncherLocator))
|
||||
.LocateGamePathAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (result.IsOk)
|
||||
{
|
||||
// Save result.
|
||||
entry.Value = result.Value;
|
||||
await appDbContext.Settings.UpdateAndSaveAsync(entry).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
appOptions.GamePath = result.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -96,58 +98,21 @@ internal sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.Value == null)
|
||||
if (!string.IsNullOrEmpty(appOptions.GamePath))
|
||||
{
|
||||
return new(false, null!);
|
||||
}
|
||||
|
||||
// Set cache and return.
|
||||
string path = memoryCache.Set(GamePathKey, entry.Value);
|
||||
return new(true, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetGamePathSkipLocator()
|
||||
{
|
||||
if (memoryCache.TryGetValue(GamePathKey, out object? value))
|
||||
{
|
||||
return (value as string)!;
|
||||
return new(true, appOptions.GamePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty);
|
||||
|
||||
// Set cache and return.
|
||||
return memoryCache.Set(GamePathKey, entry.Value!);
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OverwriteGamePath(string path)
|
||||
{
|
||||
// sync cache
|
||||
memoryCache.Set(GamePathKey, path);
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty);
|
||||
entry.Value = path;
|
||||
appDbContext.Settings.UpdateAndSave(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<IniElement> elements = IniSerializer.Deserialize(stream).ToList();
|
||||
string? channel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "channel")?.Value;
|
||||
string? subChannel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "sub_channel")?.Value;
|
||||
IEnumerable<IniParameter> parameters = IniSerializer.Deserialize(stream).ToList().OfType<IniParameter>();
|
||||
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
|
||||
/// <inheritdoc/>
|
||||
public bool SetMultiChannel(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string gamePath = appOptions.GamePath;
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName);
|
||||
|
||||
List<IniElement> elements = null!;
|
||||
@@ -232,7 +197,7 @@ internal sealed class GameService : IGameService
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> 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
|
||||
/// <inheritdoc/>
|
||||
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,106 +283,46 @@ internal sealed class GameService : IGameService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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))
|
||||
game.Start();
|
||||
if (isElevated && launchOptions.MultipleInstances)
|
||||
{
|
||||
if (options.MultipleInstances && Activation.GetElevated())
|
||||
await ProcessInterop.DisableProtectionAsync(gamePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (isElevated && launchOptions.UnlockFps)
|
||||
{
|
||||
await LaunchGameAsync(game, gamePath);
|
||||
await ProcessInterop.UnlockFpsAsync(game, launchOptions).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await LaunchGameAsync(game);
|
||||
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (options.UnlockFps)
|
||||
}
|
||||
finally
|
||||
{
|
||||
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);
|
||||
Interlocked.Decrement(ref runningGamesCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为了实现多开 需要修改mhypbase.dll名称 这是必须的步骤
|
||||
/// </summary>
|
||||
/// <param name="gameProcess">游戏线程</param>
|
||||
/// <param name="gamePath">游戏路径</param>
|
||||
/// <returns>是否成功替换文件</returns>
|
||||
public async Task<bool> LaunchMultipleInstancesGameAsync(Process gameProcess, string? gamePath)
|
||||
{
|
||||
if (gamePath == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DetectGameAccountAsync()
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,12 +39,6 @@ internal interface IGameService
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ValueResult<bool, string>> GetGamePathAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏路径,跳过异步定位器
|
||||
/// </summary>
|
||||
/// <returns>游戏路径,当路径无效时会设置并返回 <see cref="string.Empty"/></returns>
|
||||
string GetGamePathSkipLocator();
|
||||
|
||||
/// <summary>
|
||||
/// 获取多通道值
|
||||
/// </summary>
|
||||
@@ -60,9 +54,8 @@ internal interface IGameService
|
||||
/// <summary>
|
||||
/// 异步启动
|
||||
/// </summary>
|
||||
/// <param name="options">启动配置</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask LaunchAsync(LaunchOptions options);
|
||||
ValueTask LaunchAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
@@ -71,12 +64,6 @@ internal interface IGameService
|
||||
/// <returns>任务</returns>
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 重写游戏路径
|
||||
/// </summary>
|
||||
/// <param name="path">路径</param>
|
||||
void OverwriteGamePath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 异步尝试移除账号
|
||||
/// </summary>
|
||||
|
||||
@@ -25,7 +25,6 @@ internal sealed class PackageConverter
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏文件转换器
|
||||
/// </summary>
|
||||
/// <param name="resourceClient">资源客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
public PackageConverter(JsonSerializerOptions options, HttpClient httpClient)
|
||||
@@ -45,7 +44,6 @@ internal sealed class PackageConverter
|
||||
/// <returns>替换结果与资源</returns>
|
||||
public async Task<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageReplaceStatus> 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<byte> buffer = new byte[bufferSize];
|
||||
@@ -205,8 +204,12 @@ internal sealed class PackageConverter
|
||||
await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
86
src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs
Normal file
86
src/Snap.Hutao/Snap.Hutao/Service/Game/ProcessInterop.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 进程互操作
|
||||
/// </summary>
|
||||
internal static class ProcessInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取初始化后的游戏进程
|
||||
/// </summary>
|
||||
/// <param name="options">启动选项</param>
|
||||
/// <param name="gamePath">游戏路径</param>
|
||||
/// <returns>初始化后的游戏进程</returns>
|
||||
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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解锁帧率
|
||||
/// </summary>
|
||||
/// <param name="game">游戏进程</param>
|
||||
/// <param name="options">启动选项</param>
|
||||
/// <returns>任务</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试禁用mhypbase
|
||||
/// </summary>
|
||||
/// <param name="gamePath">游戏路径</param>
|
||||
/// <returns>是否禁用成功</returns>
|
||||
public static async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@
|
||||
<wsc:Setting
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameMultipleInstancesDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameMultipleInstancesHeader}"
|
||||
Icon="">
|
||||
Icon="">
|
||||
<wsc:Setting.ActionContent>
|
||||
<ToggleSwitch
|
||||
Width="120"
|
||||
|
||||
@@ -30,23 +30,20 @@
|
||||
<ColumnDefinition MaxWidth="1000"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Margin="16,-16,24,16">
|
||||
<wsc:SettingsGroup Header="{shcm:ResourceString Name=ViewPageSettingAboutHeader}">
|
||||
<Grid Margin="0,4,0,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Width="80"
|
||||
Background="{StaticResource CardBackgroundFillColorDefault}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefault}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}">
|
||||
<Image Source="ms-appx:///Assets/StoreLogo.scale-400.png"/>
|
||||
<StackPanel Margin="16,16,24,16">
|
||||
<Border Height="240" Style="{StaticResource BorderCardStyle}">
|
||||
<Grid>
|
||||
<Image
|
||||
VerticalAlignment="Center"
|
||||
Source="ms-appx:///Assets/Square44x44Logo.targetsize-256.png"
|
||||
Stretch="UniformToFill"/>
|
||||
<Grid Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}">
|
||||
<Image Margin="48" Source="ms-appx:///Assets/Square44x44Logo.targetsize-256.png"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="1" Margin="16,0,0,0">
|
||||
<wsc:SettingsGroup Header="{shcm:ResourceString Name=ViewPageSettingAboutHeader}">
|
||||
<Grid Margin="0,4,0,16" RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
@@ -78,7 +75,6 @@
|
||||
Content="{shcm:ResourceString Name=ViewPageSettingSponsorNavigate}"
|
||||
NavigateUri="{StaticResource Sponsor_Afadian}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
<wsc:Setting
|
||||
@@ -129,7 +125,7 @@
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleHeader}"
|
||||
Icon="">
|
||||
<ToggleSwitch
|
||||
IsOn="{Binding IsEmptyHistoryWishVisible, Mode=TwoWay}"
|
||||
IsOn="{Binding Options.IsEmptyHistoryWishVisible, Mode=TwoWay}"
|
||||
OffContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOff}"
|
||||
OnContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOn}"
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"/>
|
||||
@@ -143,7 +139,7 @@
|
||||
Message="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"
|
||||
Severity="Informational"/>
|
||||
<wsc:Setting
|
||||
Description="{Binding GamePath}"
|
||||
Description="{Binding Options.GamePath}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingSetGamePathHeader}"
|
||||
Icon="">
|
||||
<wsc:Setting.ActionContent>
|
||||
|
||||
@@ -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<LaunchOptions>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
LaunchCommand = new AsyncRelayCommand(LaunchAsync);
|
||||
LaunchCommand = new AsyncRelayCommand(LaunchAsync, AsyncRelayCommandOptions.AllowConcurrentExecutions);
|
||||
DetectGameAccountCommand = new AsyncRelayCommand(DetectGameAccountAsync);
|
||||
ModifyGameAccountCommand = new AsyncRelayCommand<GameAccount>(ModifyGameAccountAsync);
|
||||
RemoveGameAccountCommand = new AsyncRelayCommand<GameAccount>(RemoveGameAccountAsync);
|
||||
@@ -143,7 +144,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OpenUIAsync()
|
||||
{
|
||||
if (File.Exists(gameService.GetGamePathSkipLocator()))
|
||||
if (File.Exists(serviceProvider.GetRequiredService<AppOptions>().GamePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -205,11 +206,6 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel
|
||||
{
|
||||
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
|
||||
|
||||
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<AppOptions>().GamePath;
|
||||
string screenshot = Path.Combine(Path.GetDirectoryName(game)!, "ScreenShot");
|
||||
if (Directory.Exists(screenshot))
|
||||
{
|
||||
|
||||
@@ -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<SettingViewModel> logger;
|
||||
private readonly SettingEntry isEmptyHistoryWishVisibleEntry;
|
||||
private readonly SettingEntry selectedBackdropTypeEntry;
|
||||
|
||||
private readonly List<NameValue<BackdropType>> backdropTypes = new()
|
||||
{
|
||||
new("Acrylic", BackdropType.Acrylic),
|
||||
@@ -55,7 +55,7 @@ internal sealed class SettingViewModel : Abstraction.ViewModel
|
||||
|
||||
private bool isEmptyHistoryWishVisible;
|
||||
private string gamePath;
|
||||
private NameValue<BackdropType> selectedBackdropType;
|
||||
private NameValue<BackdropType>? selectedBackdropType;
|
||||
private NameValue<string>? selectedCulture;
|
||||
|
||||
/// <summary>
|
||||
@@ -68,22 +68,11 @@ internal sealed class SettingViewModel : Abstraction.ViewModel
|
||||
gameService = serviceProvider.GetRequiredService<IGameService>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<SettingViewModel>>();
|
||||
Experimental = serviceProvider.GetRequiredService<ExperimentalFeaturesViewModel>();
|
||||
Options = serviceProvider.GetRequiredService<AppOptions>();
|
||||
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<BackdropType>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 空的历史卡池是否可见
|
||||
/// 应用程序设置
|
||||
/// </summary>
|
||||
public bool IsEmptyHistoryWishVisible
|
||||
{
|
||||
get => isEmptyHistoryWishVisible;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref isEmptyHistoryWishVisible, value))
|
||||
{
|
||||
isEmptyHistoryWishVisibleEntry.Value = value.ToString();
|
||||
appDbContext.Settings.UpdateAndSave(isEmptyHistoryWishVisibleEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public string GamePath
|
||||
{
|
||||
get => gamePath;
|
||||
[MemberNotNull(nameof(gamePath))]
|
||||
set => SetProperty(ref gamePath, value);
|
||||
}
|
||||
public AppOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景类型
|
||||
@@ -155,17 +123,14 @@ internal sealed class SettingViewModel : Abstraction.ViewModel
|
||||
/// <summary>
|
||||
/// 选中的背景类型
|
||||
/// </summary>
|
||||
public NameValue<BackdropType> SelectedBackdropType
|
||||
public NameValue<BackdropType>? 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<IMessenger>().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<IEnumerable<IGameLocator>>()
|
||||
.Single(l => l.Name == nameof(ManualGameLocator));
|
||||
IGameLocator locator = serviceProvider.GetRequiredService<IEnumerable<IGameLocator>>().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<IGameService>();
|
||||
string gamePath = gameService.GetGamePathSkipLocator();
|
||||
string gamePath = Options.GamePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user