14 Commits

Author SHA1 Message Date
fanbook-wangdage
cb6d728c35 提升版本号、解决CI报错 2026-02-07 13:17:31 +08:00
fanbook-wangdage
f87b80cc9e Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2026-02-07 13:08:35 +08:00
fanbook-wangdage
4b313b134e 新增msi安装界面,修复WebView2权限问题,修复切换服务器时会显示等待进程退出的问题,为页面添加缓存来提示频繁切换页面时的性能 2026-02-07 13:07:51 +08:00
wangdage12
0c775a5d3d Revise README for injection features and dependencies
Updated README to clarify injection functionality and added links to required repositories.
2026-01-29 11:54:32 +08:00
wangdage12
00cd5a8c07 Revise README for installation and server status
Updated installation instructions and server status information.
2026-01-27 14:00:53 +08:00
wangdage12
d93ae2bb83 Update README.md 2026-01-20 11:40:34 +08:00
fanbook-wangdage
2f148488f4 修复工具异步加载问题、添加武器和角色id、提示版本号 2026-01-16 12:33:26 +08:00
fanbook-wangdage
df92894307 支持公告中的发行版字段 2026-01-16 11:39:28 +08:00
fanbook-wangdage
5fad9ad855 提升版本号 2026-01-13 16:49:20 +08:00
fanbook-wangdage
1ed2f4f29e 支持注入时传命令行参数 2026-01-13 16:33:17 +08:00
fanbook-wangdage
db6df72791 添加using 2026-01-13 15:30:51 +08:00
fanbook-wangdage
bd9f188ac1 添加第三方工具功能 2026-01-13 15:17:20 +08:00
fanbook-wangdage
56c36a01ae Merge branches 'main' and 'main' of https://github.com/wangdage12/Snap.Hutao 2026-01-08 13:58:14 +08:00
fanbook-wangdage
da6f248509 支持调用genshin-fps-unlock项目来调整帧率、修复武器id问题 2026-01-08 13:57:17 +08:00
37 changed files with 1394 additions and 96 deletions

View File

@@ -4,7 +4,7 @@
**中文** **中文**
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。 胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用 自带的注入功能只有FPS调整只保证FPS调整长期可用你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
有条件的话可以加入discord服务器https://discord.gg/ucH3mgeWpQ 有条件的话可以加入discord服务器https://discord.gg/ucH3mgeWpQ
@@ -15,8 +15,6 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
## 🚀 安装 / Installation ## 🚀 安装 / Installation
> 如果你的设备不支持ipv6请下载末尾带有`ipv4`的压缩包,正常情况下请尽量下载普通包(服务器速度快)
目前 Sanp.Hutao.Rev 更新了打包方式,并采用了标准现代的 msi 安装,方便程序获取管理员权限和更多的功能设置,不再需要原 Depolyment 目前 Sanp.Hutao.Rev 更新了打包方式,并采用了标准现代的 msi 安装,方便程序获取管理员权限和更多的功能设置,不再需要原 Depolyment
只有`.msi`安装包安装的可以和之前的版本共存,如果通过`.msix`安装包安装则可能出现`0x80073CF3`,备份旧版本数据文件夹后卸载旧版本即可继续安装,将旧版本数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据 只有`.msi`安装包安装的可以和之前的版本共存,如果通过`.msix`安装包安装则可能出现`0x80073CF3`,备份旧版本数据文件夹后卸载旧版本即可继续安装,将旧版本数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据
@@ -34,7 +32,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
**目前元数据的编写进度:** **目前元数据的编写进度:**
| 项目V6.2 | 是否完成 | | 项目V6.3 | 是否完成 |
| ----------- | ----------- | | ----------- | ----------- |
| 总体数据 | ✔️ | | 总体数据 | ✔️ |
@@ -52,40 +50,55 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
https://deepwiki.com/DGP-Studio/Snap.Hutao https://deepwiki.com/DGP-Studio/Snap.Hutao
https://deepwiki.com/DGP-Studio/Snap.Hutao.Server https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
**该项目所需的其他仓库,欢迎贡献或者自部署**
- 元数据:[Snap.Metadata](https://github.com/wangdage12/Snap.Metadata)
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
- Web管理后台和官网[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
## 打包测试 ## 打包测试
由于采用了 wix 进行打包程序VS 需要安装 **HeatWave for VS2022**2026兼容。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi 由于采用了 wix 进行打包程序VS 需要安装 **HeatWave for VS2022**2026兼容。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
### 资源 ## 资源和服务器状态
> 注意普通包的资源服务器只能使用ipv6连接也就是说你的电脑必须有ipv6并且建议你手动配置DNS为`223.5.5.5`
> 如果你的设备不支持ipv6请下载末尾带有`ipv4`的压缩包
> 由于数据文件夹中有元数据的仓库和图片缓存,才得以恢复资源文件
> 如果你发现之前版本可以显示的图片不能显示了,请查找旧数据文件夹
> `C:\Users\<用户名>\AppData\Local\Packages\xxxDGPStudio.SnapHutao_xxx\LocalCache\ImageCache`
> 并将`ImageCache`文件夹提供给我,我会尽力恢复资源
[服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts) <a href="https://uptimerobot.com" target="_blank" rel="noopener">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/wangdage12/wangdage12/main/assets/uptimerobot-logo.svg">
<img alt="logo"
src="https://raw.githubusercontent.com/wangdage12/wangdage12/main/assets/uptimerobot-logo-dark.svg" width="300">
</picture>
</a>
我们将使用[UptimeRobot](https://uptimerobot.com)赞助的监控服务作为新的服务器状态页面,它有更多的功能
[新服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
[旧服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
---
**元数据仓库:** **元数据仓库:**
https://github.com/wangdage12/Snap.Metadata https://github.com/wangdage12/Snap.Metadata
镜像: 仓库镜像:
![http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square) ![http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square)
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
--- ---
**临时API** **API**
![http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square) ![http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square)
https://htserver.wdg.cloudns.ch/api/ https://htserver.wdg.cloudns.ch/api/
--- ---
**临时资源站:** **图片资源站:**
https://htserver.wdg.cloudns.ch/ https://htserver.wdg.cloudns.ch/

BIN
bin/unlockfps.exe Normal file

Binary file not shown.

View File

@@ -4,5 +4,8 @@ This file contains the declaration of all the localizable strings.
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US"> <WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." /> <String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
<String Id="MainAppTitle" Value="Snap.Hutao" />
<String Id="DesktopShortcutTitle" Value="Desktop Shortcut" />
<String Id="StartMenuShortcutTitle" Value="Start Menu Shortcut" />
</WixLocalization> </WixLocalization>

View File

@@ -1,21 +1,32 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"> <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<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.3.0"
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68" UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
Language="2052"
Scope="perMachine"> Scope="perMachine">
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." /> <Property Id="ApplicationFolderName" Value="Snap.Hutao" />
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<MediaTemplate EmbedCab="yes" /> <MediaTemplate EmbedCab="yes" />
<Feature Id="ProductFeature" Title="Snap.Hutao" Level="1"> <ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<ComponentGroupRef Id="MainAppComponents" />
<!-- 快捷方式组件 --> <Feature Id="MainApp" Title="!(loc.MainAppTitle)" Level="1">
<ComponentRef Id="ApplicationShortcut" /> <ComponentGroupRef Id="MainAppComponents" />
</Feature>
<Feature Id="DesktopShortcutFeature" Title="!(loc.DesktopShortcutTitle)" Level="1">
<ComponentRef Id="DesktopShortcut" /> <ComponentRef Id="DesktopShortcut" />
</Feature> </Feature>
<Feature Id="StartMenuShortcutFeature" Title="!(loc.StartMenuShortcutTitle)" Level="1">
<ComponentRef Id="ApplicationShortcut" />
</Feature>
</Package> </Package>
<!-- 安装目录 --> <!-- 安装目录 -->

View File

@@ -0,0 +1,11 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="zh-CN">
<String Id="DowngradeError" Value="已安装更新版本的 [ProductName]。" />
<String Id="MainAppTitle" Value="Snap.Hutao" />
<String Id="DesktopShortcutTitle" Value="桌面快捷方式" />
<String Id="StartMenuShortcutTitle" Value="开始菜单快捷方式" />
</WixLocalization>

View File

@@ -4,6 +4,8 @@
<Platform>x64</Platform> <Platform>x64</Platform>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<DefaultCulture>zh-CN</DefaultCulture>
<Cultures>zh-CN;en-US</Cultures>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -19,6 +21,13 @@
<SuppressRootDirectory>true</SuppressRootDirectory> <SuppressRootDirectory>true</SuppressRootDirectory>
</HarvestDirectory> </HarvestDirectory>
<PackageReference Include="WixToolset.Heat" Version="4.0.1" /> <PackageReference Include="WixToolset.Heat" Version="6.0.2" />
<PackageReference Include="WixToolset.UI.wixext" Version="6.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<WixLocalization Include="Package.zh-cn.wxl" />
<WixLocalization Include="Package.en-us.wxl" />
</ItemGroup>
</Project> </Project>

View File

@@ -39,6 +39,7 @@ internal static class DependencyInjection
.AddJsonOptions() .AddJsonOptions()
.AddDatabase() .AddDatabase()
.AddServices() .AddServices()
.AddThirdPartyToolService()
.AddResponseValidation() .AddResponseValidation()
.AddConfiguredHttpClients() .AddConfiguredHttpClients()

View File

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

View File

@@ -33,6 +33,8 @@ internal static class HutaoRuntime
public static WebView2Version WebView2Version { get; } = InitializeWebView2(); public static WebView2Version WebView2Version { get; } = InitializeWebView2();
public static string WebView2UserDataDirectory { get; } = InitializeWebView2UserDataDirectory();
// ⚠️ 延迟初始化以避免循环依赖 // ⚠️ 延迟初始化以避免循环依赖
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated); private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
@@ -144,6 +146,13 @@ internal static class HutaoRuntime
return cacheDir; return cacheDir;
} }
private static string InitializeWebView2UserDataDirectory()
{
string directory = Path.Combine(LocalCacheDirectory, "WebView2");
Directory.CreateDirectory(directory);
return directory;
}
private static bool CheckAppNotificationEnabled() private static bool CheckAppNotificationEnabled()
{ {
try try

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

View File

@@ -126,6 +126,9 @@ internal static class AvatarIds
public static readonly AvatarId Nefer = 10000122; public static readonly AvatarId Nefer = 10000122;
public static readonly AvatarId Durin = 10000123; public static readonly AvatarId Durin = 10000123;
public static readonly AvatarId Jahoda = 10000124; public static readonly AvatarId Jahoda = 10000124;
public static readonly AvatarId Columbina = 10000125;
public static readonly AvatarId Zibai = 10000126;
public static readonly AvatarId Illuga = 10000127;
private static readonly FrozenSet<AvatarId> StandardWishIds = private static readonly FrozenSet<AvatarId> StandardWishIds =
[ [

View File

@@ -22,9 +22,8 @@ internal static class WeaponIds
11401U, 11402U, 11403U, 11405U, 11401U, 11402U, 11403U, 11405U,
12401U, 12402U, 12403U, 12405U, 12401U, 12402U, 12403U, 12405U,
13401U, 13407U, 13401U, 13407U,
14401U, 14402U, 14403U, 14409U, 14401U, 14402U, 14403U, 14409U, 14433U, 14434U,
15401U, 15402U, 15403U, 15405U, 15401U, 15402U, 15403U, 15405U, 15434U
15434U
]; ];
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds = public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
@@ -34,7 +33,8 @@ internal static class WeaponIds
13502U, 13505U, 13502U, 13505U,
14501U, 14502U, 14501U, 14502U,
15501U, 15502U, 15501U, 15502U,
15515U, 15518U 15515U, 11518U,
14522U, 11519U
]; ];
public static bool IsOrangeStandardWish(in WeaponId weaponId) public static bool IsOrangeStandardWish(in WeaponId weaponId)

View File

@@ -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.3.0" />
<Properties> <Properties>
<DisplayName>Snap Hutao</DisplayName> <DisplayName>Snap Hutao</DisplayName>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,14 @@ internal sealed class LaunchExecutionGameProcessStartHandler : AbstractLaunchExe
public override async ValueTask ExecuteAsync(LaunchExecutionContext context) public override async ValueTask ExecuteAsync(LaunchExecutionContext context)
{ {
// 如果启用了IslandFPS解锁则跳过启动游戏进程
// 因为unlockfps.exe会负责启动游戏
if (context.LaunchOptions.IsIslandEnabled.Value)
{
context.Progress.Report(new(SH.ServiceGameLaunchPhaseProcessStarted));
return;
}
try try
{ {
context.Process.Start(); context.Process.Start();

View File

@@ -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;
@@ -20,6 +21,8 @@ internal abstract class AbstractLaunchExecutionInvoker
private bool invoked; private bool invoked;
protected ImmutableArray<ILaunchExecutionHandler> Handlers { get; init; } protected ImmutableArray<ILaunchExecutionHandler> Handlers { get; init; }
protected virtual bool ShouldWaitForProcessExit { get => true; }
protected virtual bool ShouldSpinWaitGameExitAfterInvoke { get => true; }
public static bool Invoking() public static bool Invoking()
{ {
@@ -39,7 +42,7 @@ internal abstract class AbstractLaunchExecutionInvoker
finally finally
{ {
Invokers.TryRemove(this, out _); Invokers.TryRemove(this, out _);
if (!Invoking()) if (!Invoking() && ShouldSpinWaitGameExitAfterInvoke)
{ {
await GameLifeCycle.SpinWaitGameExitAsync(taskContext).ConfigureAwait(false); await GameLifeCycle.SpinWaitGameExitAsync(taskContext).ConfigureAwait(false);
} }
@@ -100,9 +103,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 +124,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 +133,8 @@ internal abstract class AbstractLaunchExecutionInvoker
await handler.ExecuteAsync(executionContext).ConfigureAwait(false); await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
} }
if (process.IsRunning) // 只有在没有启用Island且进程存在时才等待退出
if (ShouldWaitForProcessExit && process is { IsRunning: true })
{ {
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit)); progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
try try
@@ -139,6 +150,12 @@ internal abstract class AbstractLaunchExecutionInvoker
return; return;
} }
} }
else if (ShouldWaitForProcessExit && 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));

View File

@@ -9,6 +9,9 @@ namespace Snap.Hutao.Service.Game.Launching.Invoker;
internal sealed class ConvertOnlyLaunchExecutionInvoker : AbstractLaunchExecutionInvoker internal sealed class ConvertOnlyLaunchExecutionInvoker : AbstractLaunchExecutionInvoker
{ {
protected override bool ShouldWaitForProcessExit { get => false; }
protected override bool ShouldSpinWaitGameExitAfterInvoke { get => false; }
public ConvertOnlyLaunchExecutionInvoker() public ConvertOnlyLaunchExecutionInvoker()
{ {
Handlers = Handlers =

View File

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

View File

@@ -47,6 +47,9 @@ internal sealed partial class HutaoAsAService : IHutaoAsAService
} }
} }
// Filter announcements by Distribution
array = [.. array.Where(a => string.IsNullOrEmpty(a.Distribution) || a.Distribution == "Snap Hutao")]; // 请自行修改发行版名称
foreach (HutaoAnnouncement item in array) foreach (HutaoAnnouncement item in array)
{ {
item.DismissCommand = dismissCommand; item.DismissCommand = dismissCommand;

View File

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

View File

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

View File

@@ -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.3.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" />

View File

@@ -20,6 +20,8 @@ internal partial class ScopedPage : Page
protected ScopedPage() protected ScopedPage()
{ {
// Allow a small set of recent pages to be cached to reduce navigation stutter.
NavigationCacheMode = NavigationCacheMode.Enabled;
// Events/Override Methods order // Events/Override Methods order
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Page Navigation methods: // Page Navigation methods:
@@ -103,6 +105,13 @@ internal partial class ScopedPage : Page
private void OnUnloaded(object sender, RoutedEventArgs e) private void OnUnloaded(object sender, RoutedEventArgs e)
{ {
// When navigation cache is enabled, the page instance is reused.
// Do not tear down DataContext/scope here to avoid invalid state on return.
if (NavigationCacheMode != NavigationCacheMode.Disabled)
{
return;
}
// Cancel all tasks executed by the view model // Cancel all tasks executed by the view model
viewCts.Cancel(); viewCts.Cancel();

View File

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

View File

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

View File

@@ -264,7 +264,10 @@
<shuxv:UserView x:Name="UserView"/> <shuxv:UserView x:Name="UserView"/>
</NavigationView.PaneFooter> </NavigationView.PaneFooter>
<Frame x:Name="ContentFrame" ContentTransitions="{StaticResource NavigationThemeTransitions}"/> <Frame
x:Name="ContentFrame"
CacheSize="5"
ContentTransitions="{StaticResource NavigationThemeTransitions}"/>
</NavigationView> </NavigationView>
</Grid> </Grid>

View File

@@ -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,6 +560,42 @@
Margin="16" Margin="16"
Padding="0" Padding="0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"> cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<StackPanel Spacing="8">
<!-- 第三方注入工具 -->
<Grid Style="{ThemeResource AcrylicGridCardStyle}" Visibility="{Binding ThirdPartyTools.Value, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
<Grid Padding="16,12" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<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 Style="{ThemeResource AcrylicGridCardStyle}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto"/> <RowDefinition Height="auto"/>
@@ -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,9 +742,13 @@
Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}" Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}"
Target="{x:Bind TargetFpsToggleSwitch}"/> Target="{x:Bind TargetFpsToggleSwitch}"/>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<NumberBox <NumberBox
Grid.Row="1" Grid.Row="0"
Grid.Column="1"
Padding="10,8,6,6" Padding="10,8,6,6"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}"
IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}" IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}"
@@ -714,13 +757,15 @@
SpinButtonPlacementMode="Inline" SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}" ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
Value="{Binding LaunchOptions.TargetFps.Value, Mode=TwoWay}"/> 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>

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.UI.Input.LowLevel; using Snap.Hutao.UI.Input.LowLevel;
@@ -337,7 +338,8 @@ internal sealed partial class CompactWebView2Window : Microsoft.UI.Xaml.Window,
{ {
AdditionalBrowserArguments = "--do-not-de-elevate --autoplay-policy=no-user-gesture-required", AdditionalBrowserArguments = "--do-not-de-elevate --autoplay-policy=no-user-gesture-required",
}; };
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, null, options); string userDataFolder = HutaoRuntime.WebView2UserDataDirectory;
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, userDataFolder, options);
await WebView.EnsureCoreWebView2Async(environment); await WebView.EnsureCoreWebView2Async(environment);
} }
catch (SEHException ex) catch (SEHException ex)

View File

@@ -5,6 +5,7 @@ using Microsoft.UI;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.UI.Windowing; using Snap.Hutao.UI.Windowing;
using Snap.Hutao.UI.Windowing.Abstraction; using Snap.Hutao.UI.Windowing.Abstraction;
@@ -154,7 +155,8 @@ internal sealed partial class WebView2Window : Microsoft.UI.Xaml.Window,
{ {
AdditionalBrowserArguments = "--do-not-de-elevate", AdditionalBrowserArguments = "--do-not-de-elevate",
}; };
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, null, options); string userDataFolder = HutaoRuntime.WebView2UserDataDirectory;
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, userDataFolder, options);
await WebView.EnsureCoreWebView2Async(environment); await WebView.EnsureCoreWebView2Async(environment);
} }
catch (SEHException) catch (SEHException)

View File

@@ -1,4 +1,5 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
@@ -16,11 +17,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 +57,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,9 +129,51 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
await HandleGamePathEntryChangeAsync().ConfigureAwait(false); await HandleGamePathEntryChangeAsync().ConfigureAwait(false);
Shared.ResumeLaunchExecutionAsync(this).SafeForget(); Shared.ResumeLaunchExecutionAsync(this).SafeForget();
// 初始化第三方工具列表(不阻塞页面加载)
_ = InitializeThirdPartyToolsInBackgroundAsync(token);
return true; return true;
} }
private async Task InitializeThirdPartyToolsInBackgroundAsync(CancellationToken token)
{
try
{
// Yield to let navigation/UI finish first.
await Task.Yield();
if (token.IsCancellationRequested || IsViewUnloaded.Value)
{
return;
}
ImmutableArray<ToolInfo> tools = await InitializeThirdPartyToolsAsync(token).ConfigureAwait(false);
if (token.IsCancellationRequested || IsViewUnloaded.Value)
{
return;
}
await taskContext.SwitchToMainThreadAsync();
if (!token.IsCancellationRequested && !IsViewUnloaded.Value)
{
thirdPartyToolsField.Value = tools;
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
SentrySdk.CaptureException(ex);
}
}
[Command("IdentifyMonitorsCommand")] [Command("IdentifyMonitorsCommand")]
private static async Task IdentifyMonitorsAsync() private static async Task IdentifyMonitorsAsync()
{ {
@@ -302,4 +350,47 @@ 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(CancellationToken token)
{
try
{
SentrySdk.AddBreadcrumb("Starting to initialize third party tools", category: "ThirdPartyTool");
IThirdPartyToolService thirdPartyToolService = serviceProvider.GetRequiredService<IThirdPartyToolService>();
SentrySdk.AddBreadcrumb("Got IThirdPartyToolService instance", category: "ThirdPartyTool");
// Note: service API is not cancellable; we only honor cancellation before/after the call.
token.ThrowIfCancellationRequested();
ImmutableArray<ToolInfo> tools = await thirdPartyToolService.GetToolsAsync().ConfigureAwait(false);
token.ThrowIfCancellationRequested();
SentrySdk.AddBreadcrumb($"Got {tools.Length} tools from service", category: "ThirdPartyTool");
return tools;
}
catch (OperationCanceledException)
{
return ImmutableArray<ToolInfo>.Empty;
}
catch (Exception ex)
{
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
SentrySdk.CaptureException(ex);
return ImmutableArray<ToolInfo>.Empty;
}
}
} }

View File

@@ -16,4 +16,6 @@ internal class UploadAnnouncement
public string Link { get; set; } = default!; public string Link { get; set; } = default!;
public string? MaxPresentVersion { get; set; } public string? MaxPresentVersion { get; set; }
public string? Distribution { get; set; }
} }

View File

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

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