support multi-clienting

This commit is contained in:
DismissedLight
2023-03-13 19:13:35 +08:00
parent dc8d7ac913
commit 5aba2eab97
20 changed files with 458 additions and 471 deletions

Binary file not shown.

BIN
res/HutaoIcon2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

BIN
res/HutaoIcon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
res/HutaoIconSource.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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.

View File

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

View File

@@ -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>

View 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; }
}

View File

@@ -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,107 +50,69 @@ 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))
{
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);
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);
}
}
}
/// <inheritdoc/>
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<AppDbContext>();
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty);
// Set cache and return.
return memoryCache.Set(GamePathKey, entry.Value!);
}
}
}
/// <inheritdoc/>
public void OverwriteGamePath(string path)
{
// sync cache
memoryCache.Set(GamePathKey, path);
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Cannot find in setting
if (string.IsNullOrEmpty(appOptions.GamePath))
{
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.GamePath, string.Empty);
entry.Value = path;
appDbContext.Settings.UpdateAndSave(entry);
// Try locate by unity log
ValueResult<bool, string> 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!);
}
}
}
/// <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,105 +283,45 @@ 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))
{
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);
}
}
}
/// <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)
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;
}
/// <inheritdoc/>
@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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,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;
}

View 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;
}
}

View File

@@ -260,7 +260,7 @@
<wsc:Setting
Description="{shcm:ResourceString Name=ViewPageLaunchGameMultipleInstancesDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameMultipleInstancesHeader}"
Icon="&#xE14D;">
Icon="&#xE7C4;">
<wsc:Setting.ActionContent>
<ToggleSwitch
Width="120"

View File

@@ -30,55 +30,51 @@
<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"/>
</Border>
<Grid Grid.Column="1" Margin="16,0,0,0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Text="Copyright © 2022 - 2023 DGP Studio. All Rights Reserved."
TextWrapping="Wrap"/>
<StackPanel
Grid.Row="1"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{shcm:ResourceString Name=ViewPageSettingLinks}"/>
<HyperlinkButton
Margin="12,0,0,0"
Command="{Binding UpdateCheckCommand}"
Content="{shcm:ResourceString Name=ViewPageSettingUpdateCheckAction}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingFeedbackNavigate}"
NavigateUri="{StaticResource DocumentLink_BugReport}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingTranslateNavigate}"
NavigateUri="{StaticResource DocumentLink_Translate}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingSponsorNavigate}"
NavigateUri="{StaticResource Sponsor_Afadian}"/>
</StackPanel>
<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>
<wsc:SettingsGroup Header="{shcm:ResourceString Name=ViewPageSettingAboutHeader}">
<Grid Margin="0,4,0,16" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Text="Copyright © 2022 - 2023 DGP Studio. All Rights Reserved."
TextWrapping="Wrap"/>
<StackPanel
Grid.Row="1"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{shcm:ResourceString Name=ViewPageSettingLinks}"/>
<HyperlinkButton
Margin="12,0,0,0"
Command="{Binding UpdateCheckCommand}"
Content="{shcm:ResourceString Name=ViewPageSettingUpdateCheckAction}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingFeedbackNavigate}"
NavigateUri="{StaticResource DocumentLink_BugReport}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingTranslateNavigate}"
NavigateUri="{StaticResource DocumentLink_Translate}"/>
<HyperlinkButton
Margin="12,0,0,0"
Content="{shcm:ResourceString Name=ViewPageSettingSponsorNavigate}"
NavigateUri="{StaticResource Sponsor_Afadian}"/>
</StackPanel>
</Grid>
<wsc:Setting
@@ -129,7 +125,7 @@
Header="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleHeader}"
Icon="&#xE81C;">
<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="&#xE7FC;">
<wsc:Setting.ActionContent>

View File

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

View File

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