mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-17 15:06:39 +08:00
Compare commits
12 Commits
1.17.4.0
...
1.18.1.0_T
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fad9ad855 | ||
|
|
1ed2f4f29e | ||
|
|
db6df72791 | ||
|
|
bd9f188ac1 | ||
|
|
56c36a01ae | ||
|
|
da6f248509 | ||
|
|
068eb65fef | ||
|
|
09a8cded2f | ||
|
|
c38fdf30d0 | ||
|
|
bc1ff03d0a | ||
|
|
b288860c3b | ||
|
|
1e40a6e576 |
43
README.md
43
README.md
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
|
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
|
||||||
|
|
||||||
|
有条件的话可以加入discord服务器:https://discord.gg/ucH3mgeWpQ
|
||||||
|
|
||||||
**English**
|
**English**
|
||||||
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
|
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
|
||||||
|
|
||||||
@@ -25,28 +27,16 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
|||||||
项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx
|
项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> 要使该项目可以长期运行,我们需要以下资源
|
> 要使该项目可以长期运行,我们需要以下资源
|
||||||
> 1. `src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs`中的新签名值
|
> 1. 元数据的编写
|
||||||
> 2. 元数据的编写
|
> 2. 图片资源
|
||||||
> 3. 图片资源
|
|
||||||
|
已同步原作者的元数据
|
||||||
|
|
||||||
V6.2的元数据已在编写中
|
|
||||||
测试仓库位置:http://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata.Test
|
|
||||||
**目前元数据的编写进度:**
|
**目前元数据的编写进度:**
|
||||||
|
|
||||||
| 项目(V6.2) | 是否完成 |
|
| 项目(V6.2) | 是否完成 |
|
||||||
| ----------- | ----------- |
|
| ----------- | ----------- |
|
||||||
| 新角色的基本数据 | ✔️ |
|
| 总体数据 | ✔️ |
|
||||||
| 新版本角色/怪物基础数值 | ❔ |
|
|
||||||
| 新角色的详细资料、名片等 | ❌ |
|
|
||||||
| 新武器 | ✔️ |
|
|
||||||
| 新材料 | ❇️ |
|
|
||||||
| 新怪物 | ❇️ |
|
|
||||||
| 新圣遗物 | / |
|
|
||||||
| 新卡池 | ❇️ |
|
|
||||||
| 新成就 | ✔️ |
|
|
||||||
| 深境螺旋 | 💠 |
|
|
||||||
| 幻想真境剧诗 | 💠 |
|
|
||||||
| 幽境危战 | ✔️ |
|
|
||||||
|
|
||||||
✔️:已完成
|
✔️:已完成
|
||||||
❌:未编写
|
❌:未编写
|
||||||
@@ -81,30 +71,21 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
|||||||
https://github.com/wangdage12/Snap.Metadata
|
https://github.com/wangdage12/Snap.Metadata
|
||||||
|
|
||||||
镜像:
|
镜像:
|
||||||

|

|
||||||
|
|
||||||
http://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata
|
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
http://serverjp.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**临时API:**
|
**临时API:**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
http://server.wdg.cloudns.ch:5222/
|
https://htserver.wdg.cloudns.ch/api/
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
http://serverjp.wdg.cloudns.ch:5222/
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**临时资源站:**
|
**临时资源站:**
|
||||||
http://server.wdg.cloudns.ch:8007/
|
|
||||||
|
|
||||||
http://serverjp.wdg.cloudns.ch:8001/
|
https://htserver.wdg.cloudns.ch/
|
||||||
|
|||||||
BIN
bin/unlockfps.exe
Normal file
BIN
bin/unlockfps.exe
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
<Package
|
<Package
|
||||||
Name="Snap.Hutao"
|
Name="Snap.Hutao"
|
||||||
Manufacturer="Millennium Science Technology R-D Inst"
|
Manufacturer="Millennium Science Technology R-D Inst"
|
||||||
Version="1.17.4.0"
|
Version="1.18.1.0"
|
||||||
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||||
Scope="perMachine">
|
Scope="perMachine">
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ internal static class DependencyInjection
|
|||||||
.AddJsonOptions()
|
.AddJsonOptions()
|
||||||
.AddDatabase()
|
.AddDatabase()
|
||||||
.AddServices()
|
.AddServices()
|
||||||
|
.AddThirdPartyToolService()
|
||||||
.AddResponseValidation()
|
.AddResponseValidation()
|
||||||
.AddConfiguredHttpClients()
|
.AddConfiguredHttpClients()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Snap.Hutao.Core.Text.Json;
|
using Snap.Hutao.Core.Text.Json;
|
||||||
using Snap.Hutao.Factory.Process;
|
using Snap.Hutao.Factory.Process;
|
||||||
using Snap.Hutao.Model.Entity.Database;
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
|
using Snap.Hutao.Service.ThirdPartyTool;
|
||||||
using Snap.Hutao.Win32;
|
using Snap.Hutao.Win32;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
|
||||||
@@ -66,5 +67,10 @@ internal static partial class ServiceCollectionExtension
|
|||||||
.UseSqlite(sqlConnectionString);
|
.UseSqlite(sqlConnectionString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IServiceCollection AddThirdPartyToolService()
|
||||||
|
{
|
||||||
|
return services.AddSingleton<IThirdPartyToolService, ThirdPartyToolService>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
45
src/Snap.Hutao/Snap.Hutao/Factory/Process/NullProcess.cs
Normal file
45
src/Snap.Hutao/Snap.Hutao/Factory/Process/NullProcess.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.Diagnostics;
|
||||||
|
using Snap.Hutao.Win32.Foundation;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Factory.Process;
|
||||||
|
|
||||||
|
internal sealed class NullProcess : IProcess
|
||||||
|
{
|
||||||
|
public int Id => 0;
|
||||||
|
|
||||||
|
public nint Handle => 0;
|
||||||
|
|
||||||
|
public HWND MainWindowHandle => default;
|
||||||
|
|
||||||
|
public bool HasExited => true;
|
||||||
|
|
||||||
|
public int ExitCode => 0;
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeMainThread()
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WaitForExit()
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Kill()
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -193,11 +193,39 @@ internal sealed class ProcessFactory
|
|||||||
|
|
||||||
public static void StartUsingShellExecuteRunAs(string fileName)
|
public static void StartUsingShellExecuteRunAs(string fileName)
|
||||||
{
|
{
|
||||||
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
|
// 尝试从app包中启动
|
||||||
|
try
|
||||||
{
|
{
|
||||||
FileName = fileName,
|
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
|
||||||
UseShellExecute = true,
|
{
|
||||||
Verb = "runas",
|
FileName = fileName,
|
||||||
});
|
UseShellExecute = true,
|
||||||
|
Verb = "runas",
|
||||||
|
});
|
||||||
|
}catch
|
||||||
|
{
|
||||||
|
// 如果失败且filename含有Snap.Hutao.Unpackaged,就直接用Snap.Hutao.exe重启
|
||||||
|
if (fileName.Contains("Snap.Hutao.Unpackaged"))
|
||||||
|
{
|
||||||
|
string currentDirectory = Directory.GetCurrentDirectory();
|
||||||
|
string unpackagedPath = Path.Combine(currentDirectory, "Snap.Hutao.exe");
|
||||||
|
if (File.Exists(unpackagedPath))
|
||||||
|
{
|
||||||
|
fileName = unpackagedPath;
|
||||||
|
}
|
||||||
|
// 否则抛出异常
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
// 重新尝试启动
|
||||||
|
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ internal static class WeaponIds
|
|||||||
13502U, 13505U,
|
13502U, 13505U,
|
||||||
14501U, 14502U,
|
14501U, 14502U,
|
||||||
15501U, 15502U,
|
15501U, 15502U,
|
||||||
15515U, 15518U
|
15515U, 11518U
|
||||||
];
|
];
|
||||||
|
|
||||||
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<Package
|
<Package
|
||||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="60568DGPStudio.SnapHutao"
|
Name="60568DGPStudio.SnapHutao"
|
||||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||||
Version="1.17.4.0" />
|
Version="1.18.1.0" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Snap Hutao</DisplayName>
|
<DisplayName>Snap Hutao</DisplayName>
|
||||||
|
|||||||
@@ -1208,6 +1208,12 @@
|
|||||||
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
|
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
|
||||||
<value>正在等待游戏数据</value>
|
<value>正在等待游戏数据</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ServiceThirdPartyToolNoExecutableFound" xml:space="preserve">
|
||||||
|
<value>未找到可执行文件</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServiceThirdPartyToolFileNotFound" xml:space="preserve">
|
||||||
|
<value>文件不存在:{0}</value>
|
||||||
|
</data>
|
||||||
<data name="UIViewMainTitleBarBackgroundActivityAction" xml:space="preserve">
|
<data name="UIViewMainTitleBarBackgroundActivityAction" xml:space="preserve">
|
||||||
<value>后台任务</value>
|
<value>后台任务</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1586,6 +1592,12 @@
|
|||||||
<data name="ViewDialogLaunchGamePackageConvertTitle" xml:space="preserve">
|
<data name="ViewDialogLaunchGamePackageConvertTitle" xml:space="preserve">
|
||||||
<value>正在转换客户端</value>
|
<value>正在转换客户端</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewDialogThirdPartyToolDescription" xml:space="preserve">
|
||||||
|
<value>工具描述:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewDialogThirdPartyToolLaunch" xml:space="preserve">
|
||||||
|
<value>启动</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||||
<value>使用米游社扫描二维码</value>
|
<value>使用米游社扫描二维码</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2916,6 +2928,9 @@
|
|||||||
<data name="ViewPageLaunchGameInjectionHeader" xml:space="preserve">
|
<data name="ViewPageLaunchGameInjectionHeader" xml:space="preserve">
|
||||||
<value>注入</value>
|
<value>注入</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameThirdPartyTools" xml:space="preserve">
|
||||||
|
<value>第三方注入工具:</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageLaunchGameIslandConnected" xml:space="preserve">
|
<data name="ViewPageLaunchGameIslandConnected" xml:space="preserve">
|
||||||
<value>已连接到游戏,更改设置将会动态反映到游戏中</value>
|
<value>已连接到游戏,更改设置将会动态反映到游戏中</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Island;
|
||||||
|
|
||||||
|
internal static class FpsConfigTest
|
||||||
|
{
|
||||||
|
// 测试用,手动更新FPS配置文件
|
||||||
|
public static void TestConfigUpdate()
|
||||||
|
{
|
||||||
|
// 直接从LocalSetting读取当前FPS设置
|
||||||
|
int currentFps = LocalSetting.Get(SettingKeys.LaunchTargetFps, 60);
|
||||||
|
|
||||||
|
// 配置文件路径
|
||||||
|
string configPath = Path.Combine(AppContext.BaseDirectory, "fps_config.ini");
|
||||||
|
|
||||||
|
// 读取当前配置
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
string[] lines = File.ReadAllLines(configPath);
|
||||||
|
int configFps = 60;
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("FPS="))
|
||||||
|
{
|
||||||
|
configFps = int.Parse(line.Substring(4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Current FPS from LocalSetting: {currentFps}");
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Current FPS from config file: {configFps}");
|
||||||
|
|
||||||
|
if (currentFps != configFps)
|
||||||
|
{
|
||||||
|
// 更新配置文件
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
if (lines[i].StartsWith("FPS="))
|
||||||
|
{
|
||||||
|
lines[i] = $"FPS={currentFps}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllLines(configPath, lines);
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Updated config file with FPS: {currentFps}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.Diagnostics;
|
||||||
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
|
using Snap.Hutao.Service.Game.FileSystem;
|
||||||
|
using Snap.Hutao.Service.Game.Launching.Context;
|
||||||
|
using Snap.Hutao.Web.Hutao;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Island;
|
||||||
|
|
||||||
|
internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
||||||
|
{
|
||||||
|
private const string UnlockerExecutableName = "unlockfps.exe";
|
||||||
|
private const string UnlockerConfigName = "fps_config.ini";
|
||||||
|
|
||||||
|
private readonly bool resume;
|
||||||
|
|
||||||
|
private string? unlockerPath;
|
||||||
|
private string? gamePath;
|
||||||
|
private Process? unlockerProcess;
|
||||||
|
|
||||||
|
public GameFpsUnlockInterop(bool resume)
|
||||||
|
{
|
||||||
|
this.resume = resume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask BeforeAsync(BeforeLaunchExecutionContext context)
|
||||||
|
{
|
||||||
|
if (resume)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取unlocker.exe路径,放在Snap.Hutao同一目录下
|
||||||
|
string hutaoDirectory = AppContext.BaseDirectory;
|
||||||
|
unlockerPath = Path.Combine(hutaoDirectory, UnlockerExecutableName);
|
||||||
|
|
||||||
|
if (!File.Exists(unlockerPath))
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation("未找到unlockfps.exe文件,请将genshin-fps-unlock-master编译后的unlockfps.exe放置在Snap.Hutao同目录下");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取游戏路径
|
||||||
|
gamePath = context.FileSystem.GameFilePath;
|
||||||
|
|
||||||
|
// 验证游戏路径
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Game path from Snap.Hutao: {gamePath}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
|
||||||
|
|
||||||
|
if (!File.Exists(gamePath))
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation($"游戏文件不存在: {gamePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建配置文件
|
||||||
|
await CreateUnlockerConfigAsync(context).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 启动解锁器进程
|
||||||
|
await StartUnlockerProcessAsync(context, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask WaitForExitAsync(LaunchExecutionContext context, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (resume)
|
||||||
|
{
|
||||||
|
// 恢复模式下,尝试连接已存在的解锁器进程
|
||||||
|
await MonitorExistingUnlockerAsync(context, token).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监控解锁器进程状态(解锁器会自动启动并监控游戏)
|
||||||
|
await MonitorUnlockerProcessAsync(context, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask CreateUnlockerConfigAsync(BeforeLaunchExecutionContext context)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(gamePath))
|
||||||
|
{
|
||||||
|
throw HutaoException.NotSupported("游戏路径未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接在unlocker同目录创建配置文件
|
||||||
|
string unlockerConfigPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
||||||
|
int targetFps = context.LaunchOptions.TargetFps.Value;
|
||||||
|
|
||||||
|
string configContent = $"[Setting]\nPath={gamePath}\nFPS={targetFps}";
|
||||||
|
|
||||||
|
// 添加重试机制处理可能的权限问题
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(unlockerConfigPath, configContent).ConfigureAwait(false);
|
||||||
|
break; // 成功写入,退出循环
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
if (i == 2)
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation($"无法写入配置文件 {unlockerConfigPath},请检查权限");
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
if (i == 2)
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation($"无法写入配置文件 {unlockerConfigPath},文件可能被占用");
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask StartUnlockerProcessAsync(BeforeLaunchExecutionContext context, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
string configPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation($"配置文件不存在: {configPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
string configContent = await File.ReadAllTextAsync(configPath).ConfigureAwait(false);
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Starting unlocker with config: {configContent}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
|
||||||
|
// 构建游戏启动参数,传递给 unlockfps.exe
|
||||||
|
string gameArguments = BuildGameArguments(context);
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Game arguments for unlocker: {gameArguments}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
|
||||||
|
ProcessStartInfo startInfo = new()
|
||||||
|
{
|
||||||
|
FileName = unlockerPath,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(unlockerPath),
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Normal,
|
||||||
|
Arguments = gameArguments,
|
||||||
|
};
|
||||||
|
|
||||||
|
unlockerProcess = new Process { StartInfo = startInfo };
|
||||||
|
|
||||||
|
|
||||||
|
unlockerProcess.Start();
|
||||||
|
|
||||||
|
|
||||||
|
Task outputTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!unlockerProcess.StandardOutput.EndOfStream)
|
||||||
|
{
|
||||||
|
string line = await unlockerProcess.StandardOutput.ReadLineAsync().ConfigureAwait(false);
|
||||||
|
if (line != null)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Unlocker output: {line}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Task errorTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!unlockerProcess.StandardError.EndOfStream)
|
||||||
|
{
|
||||||
|
string line = await unlockerProcess.StandardError.ReadLineAsync().ConfigureAwait(false);
|
||||||
|
if (line != null)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Unlocker error: {line}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待解锁器初始化
|
||||||
|
await Task.Delay(5000).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw HutaoException.Throw($"启动FPS解锁器失败: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildGameArguments(BeforeLaunchExecutionContext context)
|
||||||
|
{
|
||||||
|
LaunchOptions launchOptions = context.LaunchOptions;
|
||||||
|
|
||||||
|
if (!launchOptions.AreCommandLineArgumentsEnabled.Value)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder arguments = new();
|
||||||
|
|
||||||
|
// 构建与 GameProcessFactory.CreateForDefault 相同的命令行参数
|
||||||
|
if (launchOptions.IsBorderless.Value)
|
||||||
|
{
|
||||||
|
arguments.Append(" -popupwindow");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchOptions.IsExclusive.Value)
|
||||||
|
{
|
||||||
|
arguments.Append(" -window-mode exclusive");
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments.Append($" -screen-fullscreen {(launchOptions.IsFullScreen.Value ? "1" : "0")}");
|
||||||
|
|
||||||
|
if (launchOptions.IsScreenWidthEnabled.Value)
|
||||||
|
{
|
||||||
|
arguments.Append($" -screen-width {launchOptions.ScreenWidth.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchOptions.IsScreenHeightEnabled.Value)
|
||||||
|
{
|
||||||
|
arguments.Append($" -screen-height {launchOptions.ScreenHeight.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchOptions.IsMonitorEnabled.Value)
|
||||||
|
{
|
||||||
|
arguments.Append($" -monitor {launchOptions.Monitor.Value?.Value ?? 1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchOptions.IsPlatformTypeEnabled.Value)
|
||||||
|
{
|
||||||
|
arguments.Append($" -platform_type {launchOptions.PlatformType.Value:G}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask MonitorExistingUnlockerAsync(LaunchExecutionContext context, CancellationToken token)
|
||||||
|
{
|
||||||
|
// 恢复模式下,检查是否有解锁器进程在运行
|
||||||
|
Process[] processes = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(UnlockerExecutableName));
|
||||||
|
if (processes.Length == 0)
|
||||||
|
{
|
||||||
|
// 没有找到解锁器进程,但游戏在运行,这是正常情况
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockerProcess = processes[0];
|
||||||
|
await MonitorUnlockerProcessAsync(context, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask MonitorUnlockerProcessAsync(LaunchExecutionContext context, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (unlockerProcess is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (PeriodicTimer timer = new(TimeSpan.FromSeconds(2)))
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
// 检查解锁器进程状态
|
||||||
|
if (unlockerProcess.HasExited)
|
||||||
|
{
|
||||||
|
// 解锁器已退出,这意味着游戏也已退出
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步FPS设置(如果用户在运行时修改了)
|
||||||
|
await SyncFpsSettingsAsync(context.LaunchOptions).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保解锁器进程已清理
|
||||||
|
CleanupUnlockerProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask SyncFpsSettingsAsync(LaunchOptions launchOptions)
|
||||||
|
{
|
||||||
|
if (unlockerProcess is null || unlockerProcess.HasExited)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string configPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
string[] lines = await File.ReadAllLinesAsync(configPath).ConfigureAwait(false);
|
||||||
|
int currentFps = launchOptions.TargetFps.Value;
|
||||||
|
|
||||||
|
bool needsUpdate = false;
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
if (lines[i].StartsWith("FPS="))
|
||||||
|
{
|
||||||
|
int configFps = int.Parse(lines[i].Substring(4));
|
||||||
|
if (configFps != currentFps)
|
||||||
|
{
|
||||||
|
lines[i] = $"FPS={currentFps}";
|
||||||
|
needsUpdate = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUpdate)
|
||||||
|
{
|
||||||
|
// 添加重试机制处理可能的权限问题
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllLinesAsync(configPath, lines).ConfigureAwait(false);
|
||||||
|
break; // 成功写入,退出循环
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
if (i == 2) // 最后一次尝试
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"无法写入配置文件 {configPath},请检查权限",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false); // 等待500ms后重试
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
if (i == 2) // 最后一次尝试
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"无法写入配置文件 {configPath},文件可能被占用",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false); // 等待500ms后重试
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 同步配置失败,记录但不影响主流程
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Failed to sync FPS settings: {ex.Message}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void CleanupUnlockerProcess()
|
||||||
|
{
|
||||||
|
if (unlockerProcess is not null && !unlockerProcess.HasExited)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unlockerProcess.Kill();
|
||||||
|
unlockerProcess.WaitForExit();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 忽略清理过程中的错误
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Failed to cleanup unlocker process: {ex.Message}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
unlockerProcess.Dispose();
|
||||||
|
unlockerProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
CleanupUnlockerProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ using Snap.Hutao.Service.Game.FileSystem;
|
|||||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||||
using Snap.Hutao.Win32;
|
using Snap.Hutao.Win32;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game;
|
namespace Snap.Hutao.Service.Game;
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ internal sealed partial class LaunchOptions : DbStoreOptions, IRestrictedGamePat
|
|||||||
public IObservableProperty<bool> IsSetTargetFrameRateEnabled { get => field ??= CreateProperty(SettingKeys.LaunchIsSetTargetFrameRateEnabled, true); }
|
public IObservableProperty<bool> IsSetTargetFrameRateEnabled { get => field ??= CreateProperty(SettingKeys.LaunchIsSetTargetFrameRateEnabled, true); }
|
||||||
|
|
||||||
[field: MaybeNull]
|
[field: MaybeNull]
|
||||||
public IObservableProperty<int> TargetFps { get => field ??= CreateProperty(SettingKeys.LaunchTargetFps, InitializeTargetFpsWithScreenFps); }
|
public IObservableProperty<int> TargetFps { get => field ??= CreateProperty(SettingKeys.LaunchTargetFps, InitializeTargetFpsWithScreenFps).WithValueChangedCallback(OnTargetFpsChanged); }
|
||||||
|
|
||||||
[field: MaybeNull]
|
[field: MaybeNull]
|
||||||
public IObservableProperty<bool> RemoveOpenTeamProgress { get => field ??= CreateProperty(SettingKeys.LaunchRemoveOpenTeamProgress, false); }
|
public IObservableProperty<bool> RemoveOpenTeamProgress { get => field ??= CreateProperty(SettingKeys.LaunchRemoveOpenTeamProgress, false); }
|
||||||
@@ -165,6 +167,98 @@ internal sealed partial class LaunchOptions : DbStoreOptions, IRestrictedGamePat
|
|||||||
return HutaoNative.Instance.MakeDeviceCapabilities().GetPrimaryScreenVerticalRefreshRate();
|
return HutaoNative.Instance.MakeDeviceCapabilities().GetPrimaryScreenVerticalRefreshRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnTargetFpsChanged(int newFps)
|
||||||
|
{
|
||||||
|
// 异步更新配置文件,避免阻塞UI线程
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string configPath = Path.Combine(AppContext.BaseDirectory, "fps_config.ini");
|
||||||
|
|
||||||
|
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
string[] lines = await File.ReadAllLinesAsync(configPath).ConfigureAwait(false);
|
||||||
|
bool needsUpdate = true;
|
||||||
|
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("FPS="))
|
||||||
|
{
|
||||||
|
int configFps = int.Parse(line.Substring(4));
|
||||||
|
if (configFps == newFps)
|
||||||
|
{
|
||||||
|
needsUpdate = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置文件
|
||||||
|
if (needsUpdate)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
if (lines[i].StartsWith("FPS="))
|
||||||
|
{
|
||||||
|
lines[i] = $"FPS={newFps}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllLinesAsync(configPath, lines).ConfigureAwait(false);
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Updated fps_config.ini with new FPS: {newFps}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
if (i == 2)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"无法写入配置文件 {configPath},请检查权限",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
if (i == 2)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"无法写入配置文件 {configPath},文件可能被占用",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 记录错误
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Failed to update fps_config.ini: {ex.Message}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static ImmutableArray<NameValue<int>> InitializeMonitors()
|
private static ImmutableArray<NameValue<int>> InitializeMonitors()
|
||||||
{
|
{
|
||||||
ImmutableArray<NameValue<int>>.Builder monitors = ImmutableArray.CreateBuilder<NameValue<int>>();
|
ImmutableArray<NameValue<int>>.Builder monitors = ImmutableArray.CreateBuilder<NameValue<int>>();
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ using Snap.Hutao.Service.Notification;
|
|||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||||
|
|
||||||
internal sealed class LaunchExecutionGameIslandHandler : AbstractLaunchExecutionHandler
|
internal sealed class LaunchExecutionGameIslandHandler : AbstractLaunchExecutionHandler, IDisposable
|
||||||
{
|
{
|
||||||
private readonly bool resume;
|
private readonly bool resume;
|
||||||
private GameIslandInterop? interop;
|
private GameFpsUnlockInterop? interop;
|
||||||
|
|
||||||
public LaunchExecutionGameIslandHandler(bool resume)
|
public LaunchExecutionGameIslandHandler(bool resume)
|
||||||
{
|
{
|
||||||
@@ -63,4 +63,9 @@ internal sealed class LaunchExecutionGameIslandHandler : AbstractLaunchExecution
|
|||||||
GameLifeCycle.IsIslandConnected.Value = false;
|
GameLifeCycle.IsIslandConnected.Value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
interop?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,14 @@ internal sealed class LaunchExecutionGameProcessStartHandler : AbstractLaunchExe
|
|||||||
|
|
||||||
public override async ValueTask ExecuteAsync(LaunchExecutionContext context)
|
public override async ValueTask ExecuteAsync(LaunchExecutionContext context)
|
||||||
{
|
{
|
||||||
|
// 如果启用了Island(FPS解锁),则跳过启动游戏进程
|
||||||
|
// 因为unlockfps.exe会负责启动游戏
|
||||||
|
if (context.LaunchOptions.IsIslandEnabled.Value)
|
||||||
|
{
|
||||||
|
context.Progress.Report(new(SH.ServiceGameLaunchPhaseProcessStarted));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.Process.Start();
|
context.Process.Start();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using Snap.Hutao.Core.Diagnostics;
|
using Snap.Hutao.Core.Diagnostics;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Factory.Progress;
|
using Snap.Hutao.Factory.Progress;
|
||||||
|
using Snap.Hutao.Factory.Process;
|
||||||
using Snap.Hutao.Service.Game.FileSystem;
|
using Snap.Hutao.Service.Game.FileSystem;
|
||||||
using Snap.Hutao.Service.Game.Launching.Context;
|
using Snap.Hutao.Service.Game.Launching.Context;
|
||||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
using Snap.Hutao.Service.Game.Launching.Handler;
|
||||||
@@ -100,9 +101,16 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
|
|
||||||
fileSystemReference.Exchange(beforeContext.FileSystem);
|
fileSystemReference.Exchange(beforeContext.FileSystem);
|
||||||
|
|
||||||
using (IProcess? process = CreateProcess(beforeContext))
|
// unlockfps.exe会负责启动游戏
|
||||||
|
IProcess? process = null;
|
||||||
|
if (!beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||||
{
|
{
|
||||||
if (process is null)
|
process = CreateProcess(beforeContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (process)
|
||||||
|
{
|
||||||
|
if (process is null && !beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,7 +122,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
TaskContext = taskContext,
|
TaskContext = taskContext,
|
||||||
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
|
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
|
||||||
LaunchOptions = context.LaunchOptions,
|
LaunchOptions = context.LaunchOptions,
|
||||||
Process = process,
|
Process = process ?? new NullProcess(),
|
||||||
IsOversea = targetScheme.IsOversea,
|
IsOversea = targetScheme.IsOversea,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,7 +131,8 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
|
await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.IsRunning)
|
// 只有在没有启用Island且进程存在时才等待退出
|
||||||
|
if (process is { IsRunning: true })
|
||||||
{
|
{
|
||||||
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||||
try
|
try
|
||||||
@@ -139,6 +148,12 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||||
|
{
|
||||||
|
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
await Task.Delay(30000).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited));
|
progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited));
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ internal sealed class DefaultLaunchExecutionInvoker : AbstractLaunchExecutionInv
|
|||||||
new LaunchExecutionGameResourceHandler(convertOnly: false),
|
new LaunchExecutionGameResourceHandler(convertOnly: false),
|
||||||
new LaunchExecutionGameIdentityHandler(),
|
new LaunchExecutionGameIdentityHandler(),
|
||||||
new LaunchExecutionWindowsHDRHandler(),
|
new LaunchExecutionWindowsHDRHandler(),
|
||||||
new LaunchExecutionGameProcessStartHandler(),
|
|
||||||
new LaunchExecutionGameIslandHandler(resume: false),
|
new LaunchExecutionGameIslandHandler(resume: false),
|
||||||
|
new LaunchExecutionGameProcessStartHandler(),
|
||||||
new LaunchExecutionOverlayHandler(),
|
new LaunchExecutionOverlayHandler(),
|
||||||
new LaunchExecutionStarwardPlayTimeStatisticsHandler(),
|
new LaunchExecutionStarwardPlayTimeStatisticsHandler(),
|
||||||
new LaunchExecutionBetterGenshinImpactAutomationHandler()
|
new LaunchExecutionBetterGenshinImpactAutomationHandler()
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.ThirdPartyTool;
|
||||||
|
|
||||||
|
internal interface IThirdPartyToolService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取第三方工具列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">取消令牌</param>
|
||||||
|
/// <returns>工具列表</returns>
|
||||||
|
ValueTask<ImmutableArray<ToolInfo>> GetToolsAsync(CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载工具文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">工具信息</param>
|
||||||
|
/// <param name="progress">进度报告</param>
|
||||||
|
/// <param name="token">取消令牌</param>
|
||||||
|
/// <returns>是否下载成功</returns>
|
||||||
|
ValueTask<bool> DownloadToolAsync(ToolInfo tool, IProgress<double>? progress = null, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动工具
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">工具信息</param>
|
||||||
|
/// <returns>是否启动成功</returns>
|
||||||
|
ValueTask<bool> LaunchToolAsync(ToolInfo tool);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查工具是否已下载
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">工具信息</param>
|
||||||
|
/// <returns>是否已下载</returns>
|
||||||
|
bool IsToolDownloaded(ToolInfo tool);
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||||
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
|
using Snap.Hutao.Service.Notification;
|
||||||
|
using Snap.Hutao.Web.Request.Builder;
|
||||||
|
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||||
|
using Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.ThirdPartyTool;
|
||||||
|
|
||||||
|
[HttpClient(HttpClientConfiguration.Default)]
|
||||||
|
[Service(ServiceLifetime.Singleton, typeof(IThirdPartyToolService))]
|
||||||
|
internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
||||||
|
{
|
||||||
|
private const string ApiBaseUrl = "https://htserver.wdg.cloudns.ch/api";
|
||||||
|
private const string ToolsEndpoint = "/tools";
|
||||||
|
|
||||||
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
|
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||||
|
private readonly IMessenger messenger;
|
||||||
|
|
||||||
|
[GeneratedConstructor]
|
||||||
|
public partial ThirdPartyToolService(IServiceProvider serviceProvider, HttpClient httpClient);
|
||||||
|
|
||||||
|
public async ValueTask<ImmutableArray<ToolInfo>> GetToolsAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpClient httpClient = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
SentrySdk.AddBreadcrumb($"Creating request to: {ApiBaseUrl}{ToolsEndpoint}", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
|
.SetRequestUri($"{ApiBaseUrl}{ToolsEndpoint}")
|
||||||
|
.Get();
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb($"Sending HTTP request", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
ToolApiResponse? response = await builder
|
||||||
|
.SendAsync<ToolApiResponse>(httpClient, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb($"Request completed", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb("Response is null", category: "ThirdPartyTool");
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb($"Response received: Code={response.Code}, Message={response.Message}, Data.Length={response.Data.Length}", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
if (response.Code != 0)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"API returned error code: {response.Code}, Message: {response.Message}", category: "ThirdPartyTool");
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"HTTP request failed: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"Request timed out or was cancelled: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"Failed to get third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> DownloadToolAsync(ToolInfo tool, IProgress<double>? progress = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
Directory.CreateDirectory(toolDirectory);
|
||||||
|
|
||||||
|
int totalFiles = tool.Files.Count;
|
||||||
|
int downloadedFiles = 0;
|
||||||
|
|
||||||
|
using (HttpClient httpClient = httpClientFactory.CreateClient())
|
||||||
|
{
|
||||||
|
foreach (string fileName in tool.Files)
|
||||||
|
{
|
||||||
|
string fileUrl = $"{tool.Url}{fileName}";
|
||||||
|
string localFilePath = Path.Combine(toolDirectory, fileName);
|
||||||
|
|
||||||
|
// 如果文件已存在,跳过下载
|
||||||
|
if (File.Exists(localFilePath))
|
||||||
|
{
|
||||||
|
downloadedFiles++;
|
||||||
|
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||||
|
using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadedFiles++;
|
||||||
|
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Error(ex));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> LaunchToolAsync(ToolInfo tool)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
|
||||||
|
// 查找可执行文件(.exe)
|
||||||
|
string? executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (string.IsNullOrEmpty(executablePath))
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullPath = Path.Combine(toolDirectory, executablePath);
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试以管理员权限启动
|
||||||
|
ProcessStartInfo startInfo = new()
|
||||||
|
{
|
||||||
|
FileName = fullPath,
|
||||||
|
WorkingDirectory = toolDirectory,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas", // 请求管理员权限
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
catch (System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
// 用户拒绝了管理员权限,尝试以普通权限启动
|
||||||
|
startInfo.Verb = string.Empty;
|
||||||
|
startInfo.UseShellExecute = false;
|
||||||
|
Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Error(ex));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsToolDownloaded(ToolInfo tool)
|
||||||
|
{
|
||||||
|
string toolDirectory = GetToolDirectory(tool);
|
||||||
|
if (!Directory.Exists(toolDirectory))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查所有文件是否存在
|
||||||
|
foreach (string fileName in tool.Files)
|
||||||
|
{
|
||||||
|
string filePath = Path.Combine(toolDirectory, fileName);
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetToolDirectory(ToolInfo tool)
|
||||||
|
{
|
||||||
|
// 使用数据目录/工具名作为存储路径
|
||||||
|
return Path.Combine(HutaoRuntime.DataDirectory, "Tools", tool.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<UseWinUI>true</UseWinUI>
|
<UseWinUI>true</UseWinUI>
|
||||||
<UseWPF>False</UseWPF>
|
<UseWPF>False</UseWPF>
|
||||||
<!-- 配置版本号 -->
|
<!-- 配置版本号 -->
|
||||||
<Version>1.17.4.0</Version>
|
<Version>1.18.1.0</Version>
|
||||||
|
|
||||||
<UseWindowsForms>False</UseWindowsForms>
|
<UseWindowsForms>False</UseWindowsForms>
|
||||||
<ImplicitUsings>False</ImplicitUsings>
|
<ImplicitUsings>False</ImplicitUsings>
|
||||||
@@ -71,6 +71,14 @@
|
|||||||
<Delete Files="@(LibFiles)" />
|
<Delete Files="@(LibFiles)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<!-- 复制unlockfps.exe到输出目录 -->
|
||||||
|
<Target Name="CopyUnlockFpsExe" AfterTargets="Build">
|
||||||
|
<ItemGroup>
|
||||||
|
<UnlockFpsExeSource Include="$(MSBuildThisFileDirectory)..\..\..\bin\unlockfps.exe" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Copy SourceFiles="@(UnlockFpsExeSource)" DestinationFolder="$(OutputPath)" ContinueOnError="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<!-- Analyzer Files -->
|
<!-- Analyzer Files -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="ApiEndpoints.csv" />
|
<AdditionalFiles Include="ApiEndpoints.csv" />
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<ContentDialog
|
||||||
|
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.ThirdPartyToolDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
|
||||||
|
Title="{x:Bind Tool.Name, Mode=OneWay}"
|
||||||
|
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
PrimaryButtonText="{shuxm:ResourceString Name=ViewDialogThirdPartyToolLaunch}"
|
||||||
|
Style="{StaticResource DefaultContentDialogStyle}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
|
Text="{shuxm:ResourceString Name=ViewDialogThirdPartyToolDescription}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind Tool.Description, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
Grid.Row="2"
|
||||||
|
Height="4"
|
||||||
|
IsIndeterminate="{x:Bind IsDownloading.Value, Mode=OneWay, FallbackValue=False}"
|
||||||
|
Visibility="{x:Bind IsDownloading.Value, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
</Grid>
|
||||||
|
</ContentDialog>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
|
using Snap.Hutao.Service.Notification;
|
||||||
|
using Snap.Hutao.Service.ThirdPartyTool;
|
||||||
|
using Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
|
||||||
|
[DependencyProperty<ToolInfo>("Tool")]
|
||||||
|
[DependencyProperty<bool>("IsDownloading", DefaultValue = false)]
|
||||||
|
internal sealed partial class ThirdPartyToolDialog : ContentDialog
|
||||||
|
{
|
||||||
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
|
private readonly IThirdPartyToolService thirdPartyToolService;
|
||||||
|
private readonly IMessenger messenger;
|
||||||
|
|
||||||
|
[GeneratedConstructor(InitializeComponent = true)]
|
||||||
|
public partial ThirdPartyToolDialog(IServiceProvider serviceProvider);
|
||||||
|
|
||||||
|
public ThirdPartyToolDialog(IServiceProvider serviceProvider, ToolInfo tool)
|
||||||
|
: this(serviceProvider)
|
||||||
|
{
|
||||||
|
Tool = tool;
|
||||||
|
PrimaryButtonClick += OnPrimaryButtonClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
HandleLaunchAsync().SafeForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleLaunchAsync()
|
||||||
|
{
|
||||||
|
// 在 UI 线程上获取 Tool 的引用,避免后续跨线程访问依赖属性
|
||||||
|
ToolInfo? tool = Tool;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsDownloading = true;
|
||||||
|
|
||||||
|
// 检查工具是否已下载
|
||||||
|
if (tool is not null && !thirdPartyToolService.IsToolDownloaded(tool))
|
||||||
|
{
|
||||||
|
// 下载工具
|
||||||
|
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
|
||||||
|
if (!downloadSuccess)
|
||||||
|
{
|
||||||
|
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||||
|
IsDownloading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动工具
|
||||||
|
if (tool is not null)
|
||||||
|
{
|
||||||
|
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
|
||||||
|
if (launchSuccess)
|
||||||
|
{
|
||||||
|
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||||
|
Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Error(ex));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||||
|
IsDownloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<shuxc:ScopedPage
|
<shuxc:ScopedPage
|
||||||
x:Class="Snap.Hutao.UI.Xaml.View.Page.LaunchGamePage"
|
x:Class="Snap.Hutao.UI.Xaml.View.Page.LaunchGamePage"
|
||||||
|
x:Name="PageRoot"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:cw="using:CommunityToolkit.WinUI"
|
xmlns:cw="using:CommunityToolkit.WinUI"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
xmlns:shsg="using:Snap.Hutao.Service.Game"
|
xmlns:shsg="using:Snap.Hutao.Service.Game"
|
||||||
xmlns:shsgp="using:Snap.Hutao.Service.Game.PathAbstraction"
|
xmlns:shsgp="using:Snap.Hutao.Service.Game.PathAbstraction"
|
||||||
xmlns:shux="using:Snap.Hutao.UI.Xaml"
|
xmlns:shux="using:Snap.Hutao.UI.Xaml"
|
||||||
|
xmlns:shwt="using:Snap.Hutao.Web.ThirdPartyTool"
|
||||||
xmlns:shuxb="using:Snap.Hutao.UI.Xaml.Behavior"
|
xmlns:shuxb="using:Snap.Hutao.UI.Xaml.Behavior"
|
||||||
xmlns:shuxba="using:Snap.Hutao.UI.Xaml.Behavior.Action"
|
xmlns:shuxba="using:Snap.Hutao.UI.Xaml.Behavior.Action"
|
||||||
xmlns:shuxc="using:Snap.Hutao.UI.Xaml.Control"
|
xmlns:shuxc="using:Snap.Hutao.UI.Xaml.Control"
|
||||||
@@ -558,13 +560,49 @@
|
|||||||
Margin="16"
|
Margin="16"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||||
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
|
<StackPanel Spacing="8">
|
||||||
<Grid.RowDefinitions>
|
<!-- 第三方注入工具 -->
|
||||||
<RowDefinition Height="auto"/>
|
<Grid Style="{ThemeResource AcrylicGridCardStyle}" Visibility="{Binding ThirdPartyTools.Value, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
|
||||||
<RowDefinition Height="auto"/>
|
<Grid Padding="16,12" ColumnSpacing="12">
|
||||||
<RowDefinition Height="auto"/>
|
<Grid.ColumnDefinitions>
|
||||||
<RowDefinition/>
|
<ColumnDefinition Width="auto"/>
|
||||||
</Grid.RowDefinitions>
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{shuxm:ResourceString Name=ViewPageLaunchGameThirdPartyTools}"/>
|
||||||
|
<ItemsControl
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ItemsSource="{Binding ThirdPartyTools.Value, Mode=OneWay}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="shwt:ToolInfo">
|
||||||
|
<Button
|
||||||
|
Padding="12,6"
|
||||||
|
Command="{Binding DataContext.ShowThirdPartyToolDialogCommand, ElementName=PageRoot}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Content="{Binding Name}"
|
||||||
|
Style="{ThemeResource AccentButtonStyle}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="auto"/>
|
||||||
|
<RowDefinition Height="auto"/>
|
||||||
|
<RowDefinition Height="auto"/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Padding="16,8"
|
Padding="16,8"
|
||||||
@@ -641,30 +679,31 @@
|
|||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHotSwitchHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHotSwitchHeader}"
|
||||||
IsOn="{Binding LaunchOptions.IsSetFieldOfViewEnabled.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/>
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
<NumberBox
|
<NumberBox
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Padding="10,8,6,6"
|
Padding="10,8,6,6"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}"
|
||||||
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled.Value}"
|
IsEnabled="False"
|
||||||
LargeChange="10"
|
LargeChange="10"
|
||||||
Maximum="100"
|
Maximum="100"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
SmallChange="1"
|
SmallChange="1"
|
||||||
SpinButtonPlacementMode="Inline"
|
SpinButtonPlacementMode="Inline"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovDescription}"
|
ToolTipService.ToolTip=""
|
||||||
Value="{Binding LaunchOptions.TargetFov.Value, Mode=TwoWay}"/>
|
Value="45"/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}"
|
||||||
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled}"
|
IsEnabled="False"
|
||||||
IsOn="{Binding LaunchOptions.DisableFog.Value, Mode=TwoWay}"
|
IsOn="False"
|
||||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogDescription}"/>
|
ToolTipService.ToolTip=""/>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
x:Name="TargetFpsToggleSwitch"
|
x:Name="TargetFpsToggleSwitch"
|
||||||
@@ -703,24 +742,30 @@
|
|||||||
Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}"
|
Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}"
|
||||||
Target="{x:Bind TargetFpsToggleSwitch}"/>
|
Target="{x:Bind TargetFpsToggleSwitch}"/>
|
||||||
|
|
||||||
<NumberBox
|
<Grid Grid.Row="1" Grid.Column="1">
|
||||||
Grid.Row="1"
|
<Grid.RowDefinitions>
|
||||||
Grid.Column="1"
|
<RowDefinition Height="Auto"/>
|
||||||
Padding="10,8,6,6"
|
<RowDefinition Height="Auto"/>
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}"
|
</Grid.RowDefinitions>
|
||||||
IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}"
|
<NumberBox
|
||||||
Maximum="120"
|
Grid.Row="0"
|
||||||
Minimum="1"
|
Padding="10,8,6,6"
|
||||||
SpinButtonPlacementMode="Inline"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}"
|
||||||
Value="{Binding LaunchOptions.TargetFps.Value, Mode=TwoWay}"/>
|
Maximum="120"
|
||||||
|
Minimum="1"
|
||||||
|
SpinButtonPlacementMode="Inline"
|
||||||
|
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
||||||
|
Value="{Binding LaunchOptions.TargetFps.Value, Mode=TwoWay}"/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
x:Name="FixLowFovSceneToggleSwitch"
|
x:Name="FixLowFovSceneToggleSwitch"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled}"
|
IsEnabled="False"
|
||||||
IsOn="{Binding LaunchOptions.FixLowFovScene.Value, Mode=TwoWay}"
|
IsOn="False"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameFixLowFovSceneDescription}">
|
ToolTipService.ToolTip="">
|
||||||
<mxi:Interaction.Behaviors>
|
<mxi:Interaction.Behaviors>
|
||||||
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
||||||
</mxi:Interaction.Behaviors>
|
</mxi:Interaction.Behaviors>
|
||||||
@@ -756,10 +801,11 @@
|
|||||||
x:Name="HideQuestBannerToggleSwitch"
|
x:Name="HideQuestBannerToggleSwitch"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsOn="{Binding LaunchOptions.HideQuestBanner.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}">
|
ToolTipService.ToolTip="">
|
||||||
<mxi:Interaction.Behaviors>
|
<mxi:Interaction.Behaviors>
|
||||||
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
||||||
</mxi:Interaction.Behaviors>
|
</mxi:Interaction.Behaviors>
|
||||||
@@ -798,34 +844,38 @@
|
|||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressHeader}"
|
||||||
IsOn="{Binding LaunchOptions.RemoveOpenTeamProgress.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressDescription}"/>
|
ToolTipService.ToolTip=""/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameEventCameraMoveHotSwitchHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameEventCameraMoveHotSwitchHeader}"
|
||||||
IsOn="{Binding LaunchOptions.DisableEventCameraMove.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/>
|
ToolTipService.ToolTip=""/>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Header="{shuxm:ResourceString Name=ViewOverlayDisableShowDamageTextToolTip}"
|
Header="{shuxm:ResourceString Name=ViewOverlayDisableShowDamageTextToolTip}"
|
||||||
IsOn="{Binding LaunchOptions.DisableShowDamageText.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/>
|
ToolTipService.ToolTip=""/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
x:Name="RedirectCombineEntryToggleSwitch"
|
x:Name="RedirectCombineEntryToggleSwitch"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
IsOn="{Binding LaunchOptions.RedirectCombineEntry.Value, Mode=TwoWay}"
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
Style="{ThemeResource DefaultToggleSwitchStyle}"
|
Style="{ThemeResource DefaultToggleSwitchStyle}"
|
||||||
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameIslandRedirectCombineEntryDescription}">
|
ToolTipService.ToolTip="">
|
||||||
<mxi:Interaction.Behaviors>
|
<mxi:Interaction.Behaviors>
|
||||||
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
|
||||||
</mxi:Interaction.Behaviors>
|
</mxi:Interaction.Behaviors>
|
||||||
@@ -867,39 +917,51 @@
|
|||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandUsingTouchScreenHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandUsingTouchScreenHeader}"
|
||||||
IsEnabled="{Binding LaunchOptions.IsGameRunning.Value, Converter={StaticResource BoolNegationConverter}}"
|
IsEnabled="False"
|
||||||
IsOn="{Binding LaunchOptions.UsingTouchScreen.Value, Mode=TwoWay}"/>
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowOriginalResinHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowOriginalResinHeader}"
|
||||||
IsOn="{Binding LaunchOptions.ResinListItemId000106Allowed.Value, Mode=TwoWay}"/>
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowPrimogemHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowPrimogemHeader}"
|
||||||
IsOn="{Binding LaunchOptions.ResinListItemId000201Allowed.Value, Mode=TwoWay}"/>
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowFragileResinHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowFragileResinHeader}"
|
||||||
IsOn="{Binding LaunchOptions.ResinListItemId107009Allowed.Value, Mode=TwoWay}"/>
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="5"
|
Grid.Row="5"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowTransientResinHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowTransientResinHeader}"
|
||||||
IsOn="{Binding LaunchOptions.ResinListItemId107012Allowed.Value, Mode=TwoWay}"/>
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
Grid.Row="5"
|
Grid.Row="5"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowCondensedResinHeader}"
|
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowCondensedResinHeader}"
|
||||||
IsOn="{Binding LaunchOptions.ResinListItemId220007Allowed.Value, Mode=TwoWay}"/>
|
IsEnabled="False"
|
||||||
|
IsOn="False"
|
||||||
|
ToolTipService.ToolTip=""/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentControl>
|
</ContentControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
</Pivot>
|
</Pivot>
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ using Snap.Hutao.Service.Game.PathAbstraction;
|
|||||||
using Snap.Hutao.Service.Game.Scheme;
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
using Snap.Hutao.Service.Navigation;
|
using Snap.Hutao.Service.Navigation;
|
||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
|
using Snap.Hutao.Service.ThirdPartyTool;
|
||||||
using Snap.Hutao.Service.User;
|
using Snap.Hutao.Service.User;
|
||||||
using Snap.Hutao.UI.Input.LowLevel;
|
using Snap.Hutao.UI.Input.LowLevel;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
using Snap.Hutao.UI.Xaml.View.Window;
|
using Snap.Hutao.UI.Xaml.View.Window;
|
||||||
using Snap.Hutao.ViewModel.User;
|
using Snap.Hutao.ViewModel.User;
|
||||||
|
using Snap.Hutao.Web.ThirdPartyTool;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
@@ -54,6 +56,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
|||||||
|
|
||||||
public ImmutableArray<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Values;
|
public ImmutableArray<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Values;
|
||||||
|
|
||||||
|
private IObservableProperty<ImmutableArray<ToolInfo>> thirdPartyToolsField = new ObservableProperty<ImmutableArray<ToolInfo>>(ImmutableArray<ToolInfo>.Empty);
|
||||||
|
public IObservableProperty<ImmutableArray<ToolInfo>> ThirdPartyTools { get => thirdPartyToolsField; }
|
||||||
|
|
||||||
LaunchScheme? IViewModelSupportLaunchExecution.TargetScheme { get => TargetSchemeFilteredGameAccountsView.Scheme; }
|
LaunchScheme? IViewModelSupportLaunchExecution.TargetScheme { get => TargetSchemeFilteredGameAccountsView.Scheme; }
|
||||||
|
|
||||||
LaunchScheme? IViewModelSupportLaunchExecution.CurrentScheme { get => Shared.GetCurrentLaunchSchemeFromConfigurationFile(); }
|
LaunchScheme? IViewModelSupportLaunchExecution.CurrentScheme { get => Shared.GetCurrentLaunchSchemeFromConfigurationFile(); }
|
||||||
@@ -123,6 +128,20 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
|||||||
|
|
||||||
await HandleGamePathEntryChangeAsync().ConfigureAwait(false);
|
await HandleGamePathEntryChangeAsync().ConfigureAwait(false);
|
||||||
Shared.ResumeLaunchExecutionAsync(this).SafeForget();
|
Shared.ResumeLaunchExecutionAsync(this).SafeForget();
|
||||||
|
|
||||||
|
// 初始化第三方工具列表
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ImmutableArray<ToolInfo> tools = await InitializeThirdPartyToolsAsync().ConfigureAwait(false);
|
||||||
|
SentrySdk.AddBreadcrumb($"Initialized {tools.Length} third party tools", category: "ThirdPartyTool");
|
||||||
|
thirdPartyToolsField.Value = tools;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,4 +321,40 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
|||||||
|
|
||||||
await GameLifeCycle.TryKillGameProcessAsync(taskContext).ConfigureAwait(false);
|
await GameLifeCycle.TryKillGameProcessAsync(taskContext).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command("ShowThirdPartyToolDialogCommand")]
|
||||||
|
private async Task ShowThirdPartyToolDialogAsync(ToolInfo tool)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateUI("Show third party tool dialog", "LaunchGameViewModel.Command"));
|
||||||
|
|
||||||
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
ThirdPartyToolDialog dialog = await scope.ServiceProvider
|
||||||
|
.GetRequiredService<IContentDialogFactory>()
|
||||||
|
.CreateInstanceAsync<ThirdPartyToolDialog>(scope.ServiceProvider, tool);
|
||||||
|
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<ImmutableArray<ToolInfo>> InitializeThirdPartyToolsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb("Starting to initialize third party tools", category: "ThirdPartyTool");
|
||||||
|
IThirdPartyToolService thirdPartyToolService = serviceProvider.GetRequiredService<IThirdPartyToolService>();
|
||||||
|
SentrySdk.AddBreadcrumb("Got IThirdPartyToolService instance", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
ImmutableArray<ToolInfo> tools = await thirdPartyToolService.GetToolsAsync().ConfigureAwait(false);
|
||||||
|
SentrySdk.AddBreadcrumb($"Got {tools.Length} tools from service", category: "ThirdPartyTool");
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Snap.Hutao.Web.Response;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
|
||||||
|
internal sealed class ToolApiResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int Code { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public ImmutableArray<ToolInfo> Data { get; set; } = ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
19
src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.ThirdPartyTool;
|
||||||
|
|
||||||
|
internal sealed class ToolInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("desc")]
|
||||||
|
public string Description { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("url")]
|
||||||
|
public string Url { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("files")]
|
||||||
|
public List<string> Files { get; set; } = default!;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user