mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-18 02:42:15 +08:00
Compare commits
11 Commits
1.18.1.0_T
...
e9ed7928d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9ed7928d6 | ||
|
|
4d2943d1c9 | ||
|
|
74e9427451 | ||
|
|
cb6d728c35 | ||
|
|
f87b80cc9e | ||
|
|
4b313b134e | ||
|
|
0c775a5d3d | ||
|
|
00cd5a8c07 | ||
|
|
d93ae2bb83 | ||
|
|
2f148488f4 | ||
|
|
df92894307 |
55
README.md
55
README.md
@@ -4,7 +4,15 @@
|
|||||||
**中文**
|
**中文**
|
||||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
|
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
|
||||||
|
|
||||||
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
|
自带的注入功能只有FPS调整,只保证FPS调整长期可用,你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
|
||||||
|
|
||||||
|
官网:https://htserver.wdg.cloudns.ch/
|
||||||
|
|
||||||
|
**该版本的特点:**
|
||||||
|
- 尽量保留原版功能,少重写功能,稳定性强
|
||||||
|
- 只集成没有争议的安全的注入功能
|
||||||
|
- 大部分注入功能以第三方工具形式提供,点击即用
|
||||||
|
- 永久免费的云抽卡日志
|
||||||
|
|
||||||
有条件的话可以加入discord服务器:https://discord.gg/ucH3mgeWpQ
|
有条件的话可以加入discord服务器:https://discord.gg/ucH3mgeWpQ
|
||||||
|
|
||||||
@@ -15,12 +23,12 @@ 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`,备份旧版本数据文件夹后卸载旧版本即可继续安装,将旧版本数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据
|
||||||
|
|
||||||
|
有时候我们在对某些功能有重大更改时发布测试版,可在官网的下载,可加入discord服务器报告功能使用情况和获取测试通知
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
@@ -34,7 +42,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
|||||||
|
|
||||||
**目前元数据的编写进度:**
|
**目前元数据的编写进度:**
|
||||||
|
|
||||||
| 项目(V6.2) | 是否完成 |
|
| 项目(V6.3) | 是否完成 |
|
||||||
| ----------- | ----------- |
|
| ----------- | ----------- |
|
||||||
| 总体数据 | ✔️ |
|
| 总体数据 | ✔️ |
|
||||||
|
|
||||||
@@ -52,40 +60,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://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**临时API:**
|
**API:**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
https://htserver.wdg.cloudns.ch/api/
|
https://htserver.wdg.cloudns.ch/api/
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**临时资源站:**
|
**图片资源站:**
|
||||||
|
|
||||||
https://htserver.wdg.cloudns.ch/
|
https://htserver.wdg.cloudns.ch/
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.18.1.0"
|
Version="1.18.4.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>
|
||||||
|
|
||||||
<!-- 安装目录 -->
|
<!-- 安装目录 -->
|
||||||
|
|||||||
11
src/Snap.Hutao/Snap.Hutao.Installer/Package.zh-cn.wxl
Normal file
11
src/Snap.Hutao/Snap.Hutao.Installer/Package.zh-cn.wxl
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -226,4 +235,4 @@ internal static class HutaoRuntime
|
|||||||
return new(string.Empty, SH.CoreWebView2HelperVersionUndetected, false);
|
return new(string.Empty, SH.CoreWebView2HelperVersionUndetected, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ internal sealed class YaeNamedPipeServer : IAsyncDisposable
|
|||||||
private readonly TargetNativeConfiguration config;
|
private readonly TargetNativeConfiguration config;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
private readonly IProcess gameProcess;
|
private readonly IProcess gameProcess;
|
||||||
|
private readonly bool supportsResumeMainThread;
|
||||||
|
|
||||||
private readonly NamedPipeServerStream serverStream;
|
private readonly NamedPipeServerStream serverStream;
|
||||||
|
|
||||||
private volatile bool disposed;
|
private volatile bool disposed;
|
||||||
|
|
||||||
public YaeNamedPipeServer(IServiceProvider serviceProvider, IProcess gameProcess, TargetNativeConfiguration config)
|
public YaeNamedPipeServer(IServiceProvider serviceProvider, IProcess gameProcess, TargetNativeConfiguration config, bool supportsResumeMainThread = true)
|
||||||
{
|
{
|
||||||
Verify.Operation(HutaoRuntime.IsProcessElevated, "Snap Hutao must be elevated to use Yae.");
|
Verify.Operation(HutaoRuntime.IsProcessElevated, "Snap Hutao must be elevated to use Yae.");
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ internal sealed class YaeNamedPipeServer : IAsyncDisposable
|
|||||||
|
|
||||||
this.gameProcess = gameProcess;
|
this.gameProcess = gameProcess;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.supportsResumeMainThread = supportsResumeMainThread;
|
||||||
|
|
||||||
// Yae is always running elevated, so we don't need to use ACL method.
|
// Yae is always running elevated, so we don't need to use ACL method.
|
||||||
serverStream = new(PipeName);
|
serverStream = new(PipeName);
|
||||||
@@ -116,7 +118,10 @@ internal sealed class YaeNamedPipeServer : IAsyncDisposable
|
|||||||
|
|
||||||
case YaeCommandKind.RequestResumeThread:
|
case YaeCommandKind.RequestResumeThread:
|
||||||
{
|
{
|
||||||
gameProcess.ResumeMainThread();
|
if (supportsResumeMainThread)
|
||||||
|
{
|
||||||
|
gameProcess.ResumeMainThread();
|
||||||
|
}
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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, 11518U
|
15515U, 11518U,
|
||||||
|
14522U, 11519U
|
||||||
];
|
];
|
||||||
|
|
||||||
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
||||||
|
|||||||
@@ -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.18.1.0" />
|
Version="1.18.4.0" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Snap Hutao</DisplayName>
|
<DisplayName>Snap Hutao</DisplayName>
|
||||||
|
|||||||
@@ -37,25 +37,30 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取unlocker.exe路径,放在Snap.Hutao同一目录下
|
// 准备 unlocker.exe 到可写的应用数据目录
|
||||||
string hutaoDirectory = AppContext.BaseDirectory;
|
await PrepareUnlockerToDataDirectoryAsync().ConfigureAwait(false);
|
||||||
unlockerPath = Path.Combine(hutaoDirectory, UnlockerExecutableName);
|
|
||||||
|
// 从应用数据目录获取 unlocker.exe 路径
|
||||||
|
unlockerPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerExecutableName);
|
||||||
|
|
||||||
if (!File.Exists(unlockerPath))
|
if (!File.Exists(unlockerPath))
|
||||||
{
|
{
|
||||||
throw HutaoException.InvalidOperation("未找到unlockfps.exe文件,请将genshin-fps-unlock-master编译后的unlockfps.exe放置在Snap.Hutao同目录下");
|
throw HutaoException.InvalidOperation("未找到unlockfps.exe文件,请将genshin-fps-unlock-master编译后的unlockfps.exe放置在Snap.Hutao同目录下");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加到 Windows Defender 排除项(需要管理员权限)
|
||||||
|
await AddToDefenderExclusionAsync(unlockerPath).ConfigureAwait(false);
|
||||||
|
|
||||||
// 获取游戏路径
|
// 获取游戏路径
|
||||||
gamePath = context.FileSystem.GameFilePath;
|
gamePath = context.FileSystem.GameFilePath;
|
||||||
|
|
||||||
// 验证游戏路径
|
// 验证游戏路径
|
||||||
SentrySdk.AddBreadcrumb(
|
SentrySdk.AddBreadcrumb(
|
||||||
$"Game path from Snap.Hutao: {gamePath}",
|
$"Game path from Snap.Hutao: {gamePath}",
|
||||||
category: "fps.unlocker",
|
category: "fps.unlocker",
|
||||||
level: Sentry.BreadcrumbLevel.Info);
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
|
||||||
|
|
||||||
if (!File.Exists(gamePath))
|
if (!File.Exists(gamePath))
|
||||||
{
|
{
|
||||||
throw HutaoException.InvalidOperation($"游戏文件不存在: {gamePath}");
|
throw HutaoException.InvalidOperation($"游戏文件不存在: {gamePath}");
|
||||||
@@ -81,6 +86,134 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
await MonitorUnlockerProcessAsync(context, token).ConfigureAwait(false);
|
await MonitorUnlockerProcessAsync(context, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask PrepareUnlockerToDataDirectoryAsync()
|
||||||
|
{
|
||||||
|
// 数据目录中的目标路径
|
||||||
|
string dataDirectoryUnlockerPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerExecutableName);
|
||||||
|
|
||||||
|
// 安装目录中的源路径
|
||||||
|
string installDirectoryUnlockerPath = Path.Combine(AppContext.BaseDirectory, UnlockerExecutableName);
|
||||||
|
|
||||||
|
// 检查是否需要复制
|
||||||
|
bool needsCopy = false;
|
||||||
|
if (!File.Exists(dataDirectoryUnlockerPath))
|
||||||
|
{
|
||||||
|
needsCopy = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 比较文件大小和修改时间,如果不同则更新
|
||||||
|
var sourceInfo = new FileInfo(installDirectoryUnlockerPath);
|
||||||
|
var targetInfo = new FileInfo(dataDirectoryUnlockerPath);
|
||||||
|
|
||||||
|
if (sourceInfo.Length != targetInfo.Length || sourceInfo.LastWriteTime > targetInfo.LastWriteTime)
|
||||||
|
{
|
||||||
|
needsCopy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要复制,执行复制操作
|
||||||
|
if (needsCopy)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
Directory.CreateDirectory(HutaoRuntime.DataDirectory);
|
||||||
|
|
||||||
|
|
||||||
|
File.Copy(installDirectoryUnlockerPath, dataDirectoryUnlockerPath, true);
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"unlockfps.exe 已复制到数据目录: {dataDirectoryUnlockerPath}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation($"复制 unlockfps.exe 到数据目录失败: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask AddToDefenderExclusionAsync(string executablePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 检查是否已经在排除项中
|
||||||
|
ProcessStartInfo checkInfo = new()
|
||||||
|
{
|
||||||
|
FileName = "powershell.exe",
|
||||||
|
Arguments = $"-Command \"(Get-MpPreference).ExclusionPath -split '\"' | Where-Object {{ $_ -eq '{executablePath}' }}\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
};
|
||||||
|
|
||||||
|
using (Process checkProcess = new() { StartInfo = checkInfo })
|
||||||
|
{
|
||||||
|
checkProcess.Start();
|
||||||
|
string output = await checkProcess.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
await checkProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 如果输出包含路径,说明已经在排除项中
|
||||||
|
if (!string.IsNullOrWhiteSpace(output) && output.Trim().Contains(executablePath))
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"unlockfps.exe 已在 Windows Defender 排除项中",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不在排除项中,尝试添加
|
||||||
|
ProcessStartInfo addInfo = new()
|
||||||
|
{
|
||||||
|
FileName = "powershell.exe",
|
||||||
|
Arguments = $"-Command \"Add-MpPreference -ExclusionPath '{executablePath}'\"",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
Verb = "runas", // 请求管理员权限
|
||||||
|
};
|
||||||
|
|
||||||
|
using (Process addProcess = new() { StartInfo = addInfo })
|
||||||
|
{
|
||||||
|
addProcess.Start();
|
||||||
|
string output = await addProcess.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
string error = await addProcess.StandardError.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
await addProcess.WaitForExitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (addProcess.ExitCode == 0)
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"unlockfps.exe 已成功添加到 Windows Defender 排除项",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"无法添加到 Windows Defender 排除项(需要管理员权限): {error ?? output}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"添加 Windows Defender 排除项失败: {ex.Message}",
|
||||||
|
category: "fps.unlocker",
|
||||||
|
level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask CreateUnlockerConfigAsync(BeforeLaunchExecutionContext context)
|
private async ValueTask CreateUnlockerConfigAsync(BeforeLaunchExecutionContext context)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(gamePath))
|
if (string.IsNullOrEmpty(gamePath))
|
||||||
@@ -88,8 +221,8 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
throw HutaoException.NotSupported("游戏路径未初始化");
|
throw HutaoException.NotSupported("游戏路径未初始化");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接在unlocker同目录创建配置文件
|
// 在应用数据目录中创建配置文件
|
||||||
string unlockerConfigPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
string unlockerConfigPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerConfigName);
|
||||||
int targetFps = context.LaunchOptions.TargetFps.Value;
|
int targetFps = context.LaunchOptions.TargetFps.Value;
|
||||||
|
|
||||||
string configContent = $"[Setting]\nPath={gamePath}\nFPS={targetFps}";
|
string configContent = $"[Setting]\nPath={gamePath}\nFPS={targetFps}";
|
||||||
@@ -126,7 +259,7 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
string configPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
string configPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerConfigName);
|
||||||
if (!File.Exists(configPath))
|
if (!File.Exists(configPath))
|
||||||
{
|
{
|
||||||
throw HutaoException.InvalidOperation($"配置文件不存在: {configPath}");
|
throw HutaoException.InvalidOperation($"配置文件不存在: {configPath}");
|
||||||
@@ -214,6 +347,12 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取米游社登录Ticket
|
||||||
|
string? authTicket = default;
|
||||||
|
bool useAuthTicket = launchOptions.UsingHoyolabAccount.Value
|
||||||
|
&& context.TryGetOption(LaunchExecutionOptionsKey.LoginAuthTicket, out authTicket)
|
||||||
|
&& !string.IsNullOrEmpty(authTicket);
|
||||||
|
|
||||||
StringBuilder arguments = new();
|
StringBuilder arguments = new();
|
||||||
|
|
||||||
// 构建与 GameProcessFactory.CreateForDefault 相同的命令行参数
|
// 构建与 GameProcessFactory.CreateForDefault 相同的命令行参数
|
||||||
@@ -249,6 +388,12 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
arguments.Append($" -platform_type {launchOptions.PlatformType.Value:G}");
|
arguments.Append($" -platform_type {launchOptions.PlatformType.Value:G}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加米游社登录参数
|
||||||
|
if (useAuthTicket)
|
||||||
|
{
|
||||||
|
arguments.Append($" login_auth_ticket={authTicket}");
|
||||||
|
}
|
||||||
|
|
||||||
return arguments.ToString();
|
return arguments.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +447,7 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string configPath = Path.Combine(Path.GetDirectoryName(unlockerPath)!, UnlockerConfigName);
|
string configPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerConfigName);
|
||||||
if (File.Exists(configPath))
|
if (File.Exists(configPath))
|
||||||
{
|
{
|
||||||
string[] lines = await File.ReadAllLinesAsync(configPath).ConfigureAwait(false);
|
string[] lines = await File.ReadAllLinesAsync(configPath).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -26,17 +26,22 @@ 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
|
||||||
{
|
{
|
||||||
|
// 对于suspended进程(Yae注入模式、Island模式),需要先Start()创建进程,然后ResumeMainThread()恢复主线程
|
||||||
|
// 对于正常启动的进程(ShellExecute、DiagnosticsProcess),只调用Start()
|
||||||
context.Process.Start();
|
context.Process.Start();
|
||||||
|
|
||||||
|
// 尝试恢复主线程(适用于suspended进程)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.Process.ResumeMainThread();
|
||||||
|
}
|
||||||
|
catch (HutaoException ex) when (ex.Message.Contains("ResumeMainThread is not supported"))
|
||||||
|
{
|
||||||
|
// ResumeMainThread不支持,说明是正常启动的进程(DiagnosticsProcess),忽略此错误
|
||||||
|
}
|
||||||
|
|
||||||
await context.TaskContext.SwitchToMainThreadAsync();
|
await context.TaskContext.SwitchToMainThreadAsync();
|
||||||
GameLifeCycle.IsGameRunningProperty.Value = true;
|
GameLifeCycle.IsGameRunningProperty.Value = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.Diagnostics;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.LifeCycle.InterProcess.Yae;
|
using Snap.Hutao.Core.LifeCycle.InterProcess.Yae;
|
||||||
|
using Snap.Hutao.Factory.Process;
|
||||||
|
using Snap.Hutao.Service.Game;
|
||||||
using Snap.Hutao.Service.Game.Island;
|
using Snap.Hutao.Service.Game.Island;
|
||||||
using Snap.Hutao.Service.Game.Launching.Context;
|
using Snap.Hutao.Service.Game.Launching.Context;
|
||||||
using Snap.Hutao.Service.Yae.Achievement;
|
using Snap.Hutao.Service.Yae.Achievement;
|
||||||
@@ -32,36 +35,57 @@ internal sealed class LaunchExecutionYaeNamedPipeHandler : AbstractLaunchExecuti
|
|||||||
HutaoException.NotSupported(SH.ServiceGameLaunchingHandlerEmbeddedYaeClientNotElevated);
|
HutaoException.NotSupported(SH.ServiceGameLaunchingHandlerEmbeddedYaeClientNotElevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.LaunchOptions.IsIslandEnabled.Value)
|
|
||||||
{
|
|
||||||
context.Process.Kill();
|
|
||||||
HutaoException.NotSupported(SH.ServiceGameLaunchingHandlerEmbeddedYaeIslandNotEnabled);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string dataFolderYaePath = Path.Combine(HutaoRuntime.DataDirectory, "YaeAchievementLib.dll");
|
string dataFolderYaePath = Path.Combine(HutaoRuntime.DataDirectory, "YaeAchievementLib.dll");
|
||||||
InstalledLocation.CopyFileFromApplicationUri("ms-appx:///YaeAchievementLib.dll", dataFolderYaePath);
|
InstalledLocation.CopyFileFromApplicationUri("ms-appx:///YaeAchievementLib.dll", dataFolderYaePath);
|
||||||
|
|
||||||
|
// 直接使用创建的游戏进程
|
||||||
|
int actualProcessId = context.Process.Id;
|
||||||
|
if (actualProcessId == 0)
|
||||||
|
{
|
||||||
|
throw HutaoException.Throw("游戏进程未正确创建");
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DllInjectionUtilities.InjectUsingRemoteThread(dataFolderYaePath, "YaeMain", context.Process.Id);
|
DllInjectionUtilities.InjectUsingRemoteThread(dataFolderYaePath, "YaeMain", actualProcessId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Windows Defender Application Control
|
// Windows Defender Application Control
|
||||||
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION))
|
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION))
|
||||||
{
|
{
|
||||||
context.Process.Kill();
|
|
||||||
throw HutaoException.Throw(SH.ServiceGameLaunchingHandlerEmbeddedYaeErrorSystemIntegrityPolicyViolation);
|
throw HutaoException.Throw(SH.ServiceGameLaunchingHandlerEmbeddedYaeErrorSystemIntegrityPolicyViolation);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw;
|
// Access Denied (0x80070005) - 权限不足,无法在远程进程中分配内存
|
||||||
|
if (ex.HResult == unchecked((int)0x80070005))
|
||||||
|
{
|
||||||
|
throw HutaoException.Throw($"无法在游戏进程中注入 DLL (访问被拒绝)。\n\n" +
|
||||||
|
$"可能的原因:\n" +
|
||||||
|
$"1. 游戏进程的完整性级别高于 Snap Hutao\n" +
|
||||||
|
$"2. Windows Defender 或其他安全软件阻止了注入\n" +
|
||||||
|
$"解决方法:\n" +
|
||||||
|
$"1. 检查 Windows Defender 设置,将 Snap Hutao 添加到排除列表\n" +
|
||||||
|
$"2. 以管理员身份运行 Snap Hutao\n" +
|
||||||
|
$"3. 检查是否有其他安全软件(如 360、火绒等)干扰");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏进程由直接启动,已经是运行状态
|
||||||
|
// InjectUsingWindowsHook2 需要手动恢复主线程,但 DiagnosticsProcess 不支持 ResumeMainThread
|
||||||
|
// 这里不使用 InjectUsingWindowsHook2
|
||||||
|
throw new InvalidOperationException($"无法注入 DLL: {ex.Message}. 请确保没有启用 Windows Defender Application Control 或其他安全限制。", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 获取游戏进程用于命名管道服务器
|
||||||
|
IProcess actualProcess = ProcessFactory.TryGetById(actualProcessId, out IProcess? process)
|
||||||
|
? process
|
||||||
|
: throw HutaoException.Throw($"无法获取进程 ID {actualProcessId}");
|
||||||
|
|
||||||
|
// 已经是运行状态,不需要恢复主线程
|
||||||
#pragma warning disable CA2007
|
#pragma warning disable CA2007
|
||||||
await using (YaeNamedPipeServer server = new(context.ServiceProvider, context.Process, config))
|
await using (YaeNamedPipeServer server = new(context.ServiceProvider, actualProcess, config, supportsResumeMainThread: false))
|
||||||
#pragma warning restore CA2007
|
#pragma warning restore CA2007
|
||||||
{
|
{
|
||||||
receiver.Array = await server.GetDataArrayAsync().ConfigureAwait(false);
|
receiver.Array = await server.GetDataArrayAsync().ConfigureAwait(false);
|
||||||
@@ -69,8 +93,7 @@ internal sealed class LaunchExecutionYaeNamedPipeHandler : AbstractLaunchExecuti
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
context.Process.Kill();
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,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()
|
||||||
{
|
{
|
||||||
@@ -40,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);
|
||||||
}
|
}
|
||||||
@@ -132,7 +134,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只有在没有启用Island且进程存在时才等待退出
|
// 只有在没有启用Island且进程存在时才等待退出
|
||||||
if (process is { IsRunning: true })
|
if (ShouldWaitForProcessExit && process is { IsRunning: true })
|
||||||
{
|
{
|
||||||
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||||
try
|
try
|
||||||
@@ -148,7 +150,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
else if (ShouldWaitForProcessExit && beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||||
{
|
{
|
||||||
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
@@ -170,4 +172,4 @@ internal abstract class AbstractLaunchExecutionInvoker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
@@ -24,4 +27,4 @@ internal sealed class ConvertOnlyLaunchExecutionInvoker : AbstractLaunchExecutio
|
|||||||
// Since this invoker is only for conversion, we do not actually need the process.
|
// Since this invoker is only for conversion, we do not actually need the process.
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.Diagnostics;
|
using Snap.Hutao.Core.Diagnostics;
|
||||||
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.LifeCycle.InterProcess.Yae;
|
using Snap.Hutao.Core.LifeCycle.InterProcess.Yae;
|
||||||
|
using Snap.Hutao.Factory.Progress;
|
||||||
|
using Snap.Hutao.Factory.Process;
|
||||||
|
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;
|
||||||
|
using Snap.Hutao.Service.Game.Package;
|
||||||
using Snap.Hutao.Service.Yae.Achievement;
|
using Snap.Hutao.Service.Yae.Achievement;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Invoker;
|
namespace Snap.Hutao.Service.Game.Launching.Invoker;
|
||||||
@@ -28,4 +33,99 @@ internal sealed class YaeLaunchExecutionInvoker : AbstractLaunchExecutionInvoker
|
|||||||
{
|
{
|
||||||
return GameProcessFactory.CreateForEmbeddedYae(beforeContext);
|
return GameProcessFactory.CreateForEmbeddedYae(beforeContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldWaitForProcessExit { get => false; }
|
||||||
|
|
||||||
|
protected override bool ShouldSpinWaitGameExitAfterInvoke { get => false; }
|
||||||
|
|
||||||
|
public async ValueTask InvokeAsync(LaunchExecutionInvocationContext context)
|
||||||
|
{
|
||||||
|
ITaskContext taskContext = context.ServiceProvider.GetRequiredService<ITaskContext>();
|
||||||
|
|
||||||
|
string lockTrace = $"{GetType().Name}.{nameof(InvokeAsync)}";
|
||||||
|
context.LaunchOptions.TryGetGameFileSystem(lockTrace, out IGameFileSystem? gameFileSystem);
|
||||||
|
ArgumentNullException.ThrowIfNull(gameFileSystem);
|
||||||
|
|
||||||
|
using (GameFileSystemReference fileSystemReference = new(gameFileSystem))
|
||||||
|
{
|
||||||
|
if (context.ViewModel.TargetScheme is not { } targetScheme)
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation(SH.ViewModelLaunchGameSchemeNotSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.ViewModel.CurrentScheme is not { } currentScheme)
|
||||||
|
{
|
||||||
|
throw HutaoException.InvalidOperation(SH.ServiceGameLaunchExecutionCurrentSchemeNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
IProgress<LaunchStatus?> progress = CreateStatusProgress(context.ServiceProvider);
|
||||||
|
|
||||||
|
BeforeLaunchExecutionContext beforeContext = new()
|
||||||
|
{
|
||||||
|
ViewModel = context.ViewModel,
|
||||||
|
Progress = progress,
|
||||||
|
ServiceProvider = context.ServiceProvider,
|
||||||
|
TaskContext = taskContext,
|
||||||
|
FileSystem = fileSystemReference,
|
||||||
|
HoyoPlay = context.ServiceProvider.GetRequiredService<IHoyoPlayService>(),
|
||||||
|
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
|
||||||
|
LaunchOptions = context.LaunchOptions,
|
||||||
|
CurrentScheme = currentScheme,
|
||||||
|
TargetScheme = targetScheme,
|
||||||
|
Identity = context.Identity,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (ILaunchExecutionHandler handler in Handlers)
|
||||||
|
{
|
||||||
|
await handler.BeforeAsync(beforeContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSystemReference.Exchange(beforeContext.FileSystem);
|
||||||
|
|
||||||
|
// Yae注入功能不依赖unlockfps.exe,总是创建游戏进程
|
||||||
|
IProcess? process = CreateProcess(beforeContext);
|
||||||
|
|
||||||
|
using (process)
|
||||||
|
{
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchExecutionContext executionContext = new()
|
||||||
|
{
|
||||||
|
Progress = progress,
|
||||||
|
ServiceProvider = context.ServiceProvider,
|
||||||
|
TaskContext = taskContext,
|
||||||
|
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
|
||||||
|
LaunchOptions = context.LaunchOptions,
|
||||||
|
Process = process,
|
||||||
|
IsOversea = targetScheme.IsOversea,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (ILaunchExecutionHandler handler in Handlers)
|
||||||
|
{
|
||||||
|
await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AfterLaunchExecutionContext afterContext = new()
|
||||||
|
{
|
||||||
|
ServiceProvider = context.ServiceProvider,
|
||||||
|
TaskContext = taskContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (ILaunchExecutionHandler handler in Handlers)
|
||||||
|
{
|
||||||
|
await handler.AfterAsync(afterContext).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IProgress<LaunchStatus?> CreateStatusProgress(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
|
||||||
|
LaunchStatusOptions options = serviceProvider.GetRequiredService<LaunchStatusOptions>();
|
||||||
|
return progressFactory.CreateForMainThread<LaunchStatus?, LaunchStatusOptions>(static (status, options) => options.LaunchStatus = status, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -29,6 +29,26 @@ internal sealed class TargetNativeConfiguration
|
|||||||
|
|
||||||
public required uint Decompress { get; init; }
|
public required uint Decompress { get; init; }
|
||||||
|
|
||||||
|
public static TargetNativeConfiguration Create(uint storeCmdId, uint achievementCmdId, MethodRva methodRva)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
StoreCmdId = storeCmdId,
|
||||||
|
AchievementCmdId = achievementCmdId,
|
||||||
|
|
||||||
|
DoCmd = methodRva.DoCmd,
|
||||||
|
UpdateNormalProperty = methodRva.UpdateNormalProperty,
|
||||||
|
NewString = methodRva.NewString,
|
||||||
|
FindGameObject = methodRva.FindGameObject,
|
||||||
|
EventSystemUpdate = methodRva.EventSystemUpdate,
|
||||||
|
SimulatePointerClick = methodRva.SimulatePointerClick,
|
||||||
|
ToInt32 = methodRva.ToInt32,
|
||||||
|
TcpStatePtr = methodRva.TcpStatePtr,
|
||||||
|
SharedInfoPtr = methodRva.SharedInfoPtr,
|
||||||
|
Decompress = methodRva.Decompress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static TargetNativeConfiguration Create(NativeConfiguration config, bool isOversea)
|
public static TargetNativeConfiguration Create(NativeConfiguration config, bool isOversea)
|
||||||
{
|
{
|
||||||
MethodRva methodRva = isOversea ? config.MethodRva.Oversea : config.MethodRva.Chinese;
|
MethodRva methodRva = isOversea ? config.MethodRva.Oversea : config.MethodRva.Chinese;
|
||||||
@@ -51,4 +71,4 @@ internal sealed class TargetNativeConfiguration
|
|||||||
Decompress = methodRva.Decompress,
|
Decompress = methodRva.Decompress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/Snap.Hutao/Snap.Hutao/Service/Yae/Metadata/Crc32.cs
Normal file
32
src/Snap.Hutao/Snap.Hutao/Service/Yae/Metadata/Crc32.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Snap.Hutao.Service.Yae.Metadata;
|
||||||
|
|
||||||
|
internal static class Crc32
|
||||||
|
{
|
||||||
|
private const uint Polynomial = 0xEDB88320;
|
||||||
|
private static readonly uint[] Crc32Table = new uint[256];
|
||||||
|
|
||||||
|
static Crc32()
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < Crc32Table.Length; i++)
|
||||||
|
{
|
||||||
|
uint value = i;
|
||||||
|
for (int j = 0; j < 8; j++)
|
||||||
|
{
|
||||||
|
value = (value >> 1) ^ ((value & 1) * Polynomial);
|
||||||
|
}
|
||||||
|
|
||||||
|
Crc32Table[i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint Compute(Span<byte> buffer)
|
||||||
|
{
|
||||||
|
uint checksum = 0xFFFFFFFF;
|
||||||
|
foreach (byte b in buffer)
|
||||||
|
{
|
||||||
|
checksum = (checksum >> 8) ^ Crc32Table[(b ^ checksum) & 0xFF];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ~checksum;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Snap.Hutao.Service.Yae.Metadata;
|
||||||
|
|
||||||
|
internal interface IYaeMetadataService
|
||||||
|
{
|
||||||
|
ValueTask<YaeNativeLibConfig?> GetNativeLibConfigAsync(CancellationToken token = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
using Google.Protobuf;
|
||||||
|
using Snap.Hutao.Core.Protobuf;
|
||||||
|
using Snap.Hutao.Service.Yae.Achievement;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Yae.Metadata;
|
||||||
|
|
||||||
|
internal static class YaeMetadataParser
|
||||||
|
{
|
||||||
|
private const uint AchievementInfoNativeConfigTag = 42; // field 5, wire type length-delimited
|
||||||
|
private const uint NativeConfigStoreCmdIdTag = 8; // field 1, varint
|
||||||
|
private const uint NativeConfigAchievementCmdIdTag = 16; // field 2, varint
|
||||||
|
private const uint NativeConfigMethodRvaTag = 82; // field 10, length-delimited
|
||||||
|
|
||||||
|
private const uint MapEntryKeyTag = 8; // field 1, varint
|
||||||
|
private const uint MapEntryValueTag = 18; // field 2, length-delimited
|
||||||
|
|
||||||
|
private const uint MethodRvaDoCmdTag = 8;
|
||||||
|
private const uint MethodRvaUpdateNormalPropTag = 24;
|
||||||
|
private const uint MethodRvaNewStringTag = 32;
|
||||||
|
private const uint MethodRvaFindGameObjectTag = 40;
|
||||||
|
private const uint MethodRvaEventSystemUpdateTag = 48;
|
||||||
|
private const uint MethodRvaSimulatePointerClickTag = 56;
|
||||||
|
private const uint MethodRvaToInt32Tag = 64;
|
||||||
|
private const uint MethodRvaTcpStatePtrTag = 72;
|
||||||
|
private const uint MethodRvaSharedInfoPtrTag = 80;
|
||||||
|
private const uint MethodRvaDecompressTag = 88;
|
||||||
|
|
||||||
|
public static YaeNativeLibConfig? ParseNativeLibConfig(byte[] data)
|
||||||
|
{
|
||||||
|
uint storeCmdId = 0;
|
||||||
|
uint achievementCmdId = 0;
|
||||||
|
Dictionary<uint, MethodRva> methodRva = [];
|
||||||
|
bool hasNativeConfig = false;
|
||||||
|
|
||||||
|
CodedInputStream input = new(data);
|
||||||
|
while (input.TryReadTag(out uint tag))
|
||||||
|
{
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case AchievementInfoNativeConfigTag:
|
||||||
|
hasNativeConfig = true;
|
||||||
|
using (CodedInputStream nativeConfigStream = input.UnsafeReadLengthDelimitedStream())
|
||||||
|
{
|
||||||
|
ParseNativeConfig(nativeConfigStream, ref storeCmdId, ref achievementCmdId, methodRva);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input.SkipLastField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNativeConfig || methodRva.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new YaeNativeLibConfig
|
||||||
|
{
|
||||||
|
StoreCmdId = storeCmdId,
|
||||||
|
AchievementCmdId = achievementCmdId,
|
||||||
|
MethodRva = methodRva,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseNativeConfig(CodedInputStream input, ref uint storeCmdId, ref uint achievementCmdId, Dictionary<uint, MethodRva> methodRva)
|
||||||
|
{
|
||||||
|
while (input.TryReadTag(out uint tag))
|
||||||
|
{
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case NativeConfigStoreCmdIdTag:
|
||||||
|
storeCmdId = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case NativeConfigAchievementCmdIdTag:
|
||||||
|
achievementCmdId = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case NativeConfigMethodRvaTag:
|
||||||
|
using (CodedInputStream entryStream = input.UnsafeReadLengthDelimitedStream())
|
||||||
|
{
|
||||||
|
ParseMethodRvaEntry(entryStream, methodRva);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input.SkipLastField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseMethodRvaEntry(CodedInputStream input, Dictionary<uint, MethodRva> methodRva)
|
||||||
|
{
|
||||||
|
uint key = 0;
|
||||||
|
MethodRva? value = null;
|
||||||
|
|
||||||
|
while (input.TryReadTag(out uint tag))
|
||||||
|
{
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case MapEntryKeyTag:
|
||||||
|
key = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MapEntryValueTag:
|
||||||
|
using (CodedInputStream valueStream = input.UnsafeReadLengthDelimitedStream())
|
||||||
|
{
|
||||||
|
value = ParseMethodRvaConfig(valueStream);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input.SkipLastField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
methodRva[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodRva ParseMethodRvaConfig(CodedInputStream input)
|
||||||
|
{
|
||||||
|
uint doCmd = 0;
|
||||||
|
uint updateNormalProp = 0;
|
||||||
|
uint newString = 0;
|
||||||
|
uint findGameObject = 0;
|
||||||
|
uint eventSystemUpdate = 0;
|
||||||
|
uint simulatePointerClick = 0;
|
||||||
|
uint toInt32 = 0;
|
||||||
|
uint tcpStatePtr = 0;
|
||||||
|
uint sharedInfoPtr = 0;
|
||||||
|
uint decompress = 0;
|
||||||
|
|
||||||
|
while (input.TryReadTag(out uint tag))
|
||||||
|
{
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case MethodRvaDoCmdTag:
|
||||||
|
doCmd = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaUpdateNormalPropTag:
|
||||||
|
updateNormalProp = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaNewStringTag:
|
||||||
|
newString = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaFindGameObjectTag:
|
||||||
|
findGameObject = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaEventSystemUpdateTag:
|
||||||
|
eventSystemUpdate = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaSimulatePointerClickTag:
|
||||||
|
simulatePointerClick = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaToInt32Tag:
|
||||||
|
toInt32 = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaTcpStatePtrTag:
|
||||||
|
tcpStatePtr = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaSharedInfoPtrTag:
|
||||||
|
sharedInfoPtr = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
case MethodRvaDecompressTag:
|
||||||
|
decompress = input.ReadUInt32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
input.SkipLastField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MethodRva
|
||||||
|
{
|
||||||
|
DoCmd = doCmd,
|
||||||
|
UpdateNormalProperty = updateNormalProp,
|
||||||
|
NewString = newString,
|
||||||
|
FindGameObject = findGameObject,
|
||||||
|
EventSystemUpdate = eventSystemUpdate,
|
||||||
|
SimulatePointerClick = simulatePointerClick,
|
||||||
|
ToInt32 = toInt32,
|
||||||
|
TcpStatePtr = tcpStatePtr,
|
||||||
|
SharedInfoPtr = sharedInfoPtr,
|
||||||
|
Decompress = decompress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Yae.Metadata;
|
||||||
|
|
||||||
|
[Service(ServiceLifetime.Singleton, typeof(IYaeMetadataService))]
|
||||||
|
[HttpClient(HttpClientConfiguration.Default)]
|
||||||
|
internal sealed partial class YaeMetadataService : IYaeMetadataService
|
||||||
|
{
|
||||||
|
private const string MetadataUrl = "https://rin.holohat.work/schicksal/metadata";
|
||||||
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(6);
|
||||||
|
private static readonly string? LocalMetadataPath = TryGetLocalMetadataPath();
|
||||||
|
|
||||||
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
|
private readonly IMemoryCache memoryCache;
|
||||||
|
|
||||||
|
[GeneratedConstructor]
|
||||||
|
public partial YaeMetadataService(IServiceProvider serviceProvider);
|
||||||
|
|
||||||
|
public ValueTask<YaeNativeLibConfig?> GetNativeLibConfigAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
Task<YaeNativeLibConfig?> task = memoryCache.GetOrCreateAsync($"{nameof(YaeMetadataService)}.NativeLibConfig", async entry =>
|
||||||
|
{
|
||||||
|
entry.SetSlidingExpiration(CacheDuration);
|
||||||
|
|
||||||
|
byte[] data;
|
||||||
|
if (!string.IsNullOrEmpty(LocalMetadataPath) && File.Exists(LocalMetadataPath))
|
||||||
|
{
|
||||||
|
data = await File.ReadAllBytesAsync(LocalMetadataPath, token).ConfigureAwait(false);
|
||||||
|
if (data.Length > 0)
|
||||||
|
{
|
||||||
|
return YaeMetadataParser.ParseNativeLibConfig(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using HttpClient httpClient = httpClientFactory.CreateClient(nameof(YaeMetadataService));
|
||||||
|
data = await httpClient.GetByteArrayAsync(MetadataUrl, token).ConfigureAwait(false);
|
||||||
|
return YaeMetadataParser.ParseNativeLibConfig(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ValueTask<YaeNativeLibConfig?>(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryGetLocalMetadataPath()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试获取用户下载目录下的metadata文件,本地测试和排查问题时使用
|
||||||
|
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
if (string.IsNullOrEmpty(userProfile))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
string localPath = Path.Combine(userProfile, "Downloads", "metadata");
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Snap.Hutao.Service.Yae.Achievement;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Yae.Metadata;
|
||||||
|
|
||||||
|
internal sealed class YaeNativeLibConfig
|
||||||
|
{
|
||||||
|
public required uint StoreCmdId { get; init; }
|
||||||
|
|
||||||
|
public required uint AchievementCmdId { get; init; }
|
||||||
|
|
||||||
|
public required IReadOnlyDictionary<uint, MethodRva> MethodRva { get; init; }
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Core.LifeCycle.InterProcess.Yae;
|
|||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
using Snap.Hutao.Model.InterChange.Achievement;
|
using Snap.Hutao.Model.InterChange.Achievement;
|
||||||
using Snap.Hutao.Model.InterChange.Inventory;
|
using Snap.Hutao.Model.InterChange.Inventory;
|
||||||
using Snap.Hutao.Service.Feature;
|
|
||||||
using Snap.Hutao.Service.Game;
|
using Snap.Hutao.Service.Game;
|
||||||
using Snap.Hutao.Service.Game.FileSystem;
|
using Snap.Hutao.Service.Game.FileSystem;
|
||||||
using Snap.Hutao.Service.Game.Launching;
|
using Snap.Hutao.Service.Game.Launching;
|
||||||
@@ -15,10 +14,12 @@ using Snap.Hutao.Service.Game.Launching.Invoker;
|
|||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
using Snap.Hutao.Service.User;
|
using Snap.Hutao.Service.User;
|
||||||
using Snap.Hutao.Service.Yae.Achievement;
|
using Snap.Hutao.Service.Yae.Achievement;
|
||||||
|
using Snap.Hutao.Service.Yae.Metadata;
|
||||||
using Snap.Hutao.Service.Yae.PlayerStore;
|
using Snap.Hutao.Service.Yae.PlayerStore;
|
||||||
using Snap.Hutao.ViewModel.Game;
|
using Snap.Hutao.ViewModel.Game;
|
||||||
using Snap.Hutao.ViewModel.User;
|
using Snap.Hutao.ViewModel.User;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Yae;
|
namespace Snap.Hutao.Service.Yae;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
{
|
{
|
||||||
private readonly IContentDialogFactory contentDialogFactory;
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly IFeatureService featureService;
|
private readonly IYaeMetadataService yaeMetadataService;
|
||||||
private readonly IUserService userService;
|
private readonly IUserService userService;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
private readonly IMessenger messenger;
|
private readonly IMessenger messenger;
|
||||||
@@ -57,15 +58,12 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
Identity = GameIdentity.Create(userAndUid, viewModel.GameAccount),
|
Identity = GameIdentity.Create(userAndUid, viewModel.GameAccount),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!TryGetGameVersion(context, out string? version, out bool isOversea))
|
TargetNativeConfiguration? config = await TryGetTargetNativeConfigurationAsync(context).ConfigureAwait(false);
|
||||||
|
if (config is null)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
AchievementFieldId? fieldId = await featureService.GetAchievementFieldIdAsync(version).ConfigureAwait(false);
|
|
||||||
ArgumentNullException.ThrowIfNull(fieldId);
|
|
||||||
|
|
||||||
TargetNativeConfiguration config = TargetNativeConfiguration.Create(fieldId.NativeConfig, isOversea);
|
|
||||||
await new YaeLaunchExecutionInvoker(config, receiver).InvokeAsync(context).ConfigureAwait(false);
|
await new YaeLaunchExecutionInvoker(config, receiver).InvokeAsync(context).ConfigureAwait(false);
|
||||||
|
|
||||||
UIAF? uiaf = default;
|
UIAF? uiaf = default;
|
||||||
@@ -76,7 +74,7 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
if (data.Kind is YaeCommandKind.ResponseAchievement)
|
if (data.Kind is YaeCommandKind.ResponseAchievement)
|
||||||
{
|
{
|
||||||
Debug.Assert(uiaf is null);
|
Debug.Assert(uiaf is null);
|
||||||
uiaf = AchievementParser.Parse(data.Bytes, fieldId);
|
uiaf = AchievementParser.Parse(data.Bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,15 +114,12 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
Identity = GameIdentity.Create(userAndUid, viewModel.GameAccount),
|
Identity = GameIdentity.Create(userAndUid, viewModel.GameAccount),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!TryGetGameVersion(context, out string? version, out bool isOversea))
|
TargetNativeConfiguration? config = await TryGetTargetNativeConfigurationAsync(context).ConfigureAwait(false);
|
||||||
|
if (config is null)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
AchievementFieldId? fieldId = await featureService.GetAchievementFieldIdAsync(version).ConfigureAwait(false);
|
|
||||||
ArgumentNullException.ThrowIfNull(fieldId);
|
|
||||||
|
|
||||||
TargetNativeConfiguration config = TargetNativeConfiguration.Create(fieldId.NativeConfig, isOversea);
|
|
||||||
await new YaeLaunchExecutionInvoker(config, receiver).InvokeAsync(context).ConfigureAwait(false);
|
await new YaeLaunchExecutionInvoker(config, receiver).InvokeAsync(context).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -167,9 +162,9 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetGameVersion(LaunchExecutionInvocationContext context, [NotNullWhen(true)] out string? version, out bool isOversea)
|
private async ValueTask<TargetNativeConfiguration?> TryGetTargetNativeConfigurationAsync(LaunchExecutionInvocationContext context)
|
||||||
{
|
{
|
||||||
const string LockTrace = $"{nameof(YaeService)}.{nameof(TryGetGameVersion)}";
|
const string LockTrace = $"{nameof(YaeService)}.{nameof(TryGetTargetNativeConfigurationAsync)}";
|
||||||
|
|
||||||
if (context.LaunchOptions.TryGetGameFileSystem(LockTrace, out IGameFileSystem? gameFileSystem) is not GameFileSystemErrorKind.None)
|
if (context.LaunchOptions.TryGetGameFileSystem(LockTrace, out IGameFileSystem? gameFileSystem) is not GameFileSystemErrorKind.None)
|
||||||
{
|
{
|
||||||
@@ -178,23 +173,54 @@ internal sealed partial class YaeService : IYaeService
|
|||||||
|
|
||||||
if (gameFileSystem is null)
|
if (gameFileSystem is null)
|
||||||
{
|
{
|
||||||
version = default;
|
return default;
|
||||||
isOversea = false;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (gameFileSystem)
|
using (gameFileSystem)
|
||||||
{
|
{
|
||||||
if (!gameFileSystem.TryGetGameVersion(out version) || string.IsNullOrEmpty(version))
|
if (!TryGetGameExecutableHash(gameFileSystem.GameFilePath, out uint hash))
|
||||||
{
|
{
|
||||||
messenger.Send(InfoBarMessage.Error(SH.ServiceYaeGetGameVersionFailed));
|
messenger.Send(InfoBarMessage.Error(SH.ServiceYaeGetGameVersionFailed));
|
||||||
isOversea = false;
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
YaeNativeLibConfig? nativeConfig = await yaeMetadataService.GetNativeLibConfigAsync().ConfigureAwait(false);
|
||||||
|
if (nativeConfig is null)
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Error(SH.ServiceYaeGetGameVersionFailed));
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nativeConfig.MethodRva.TryGetValue(hash, out MethodRva? methodRva))
|
||||||
|
{
|
||||||
|
messenger.Send(InfoBarMessage.Error(SH.ServiceYaeGetGameVersionFailed));
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TargetNativeConfiguration.Create(nativeConfig.StoreCmdId, nativeConfig.AchievementCmdId, methodRva);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetGameExecutableHash(string gameFilePath, out uint hash)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[0x10000];
|
||||||
|
using FileStream stream = File.OpenRead(gameFilePath);
|
||||||
|
int read = stream.ReadAtLeast(buffer, buffer.Length, throwOnEndOfStream: false);
|
||||||
|
if (read < buffer.Length)
|
||||||
|
{
|
||||||
|
hash = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isOversea = gameFileSystem.IsExecutableOversea;
|
hash = Crc32.Compute(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
hash = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<UseWinUI>true</UseWinUI>
|
<UseWinUI>true</UseWinUI>
|
||||||
<UseWPF>False</UseWPF>
|
<UseWPF>False</UseWPF>
|
||||||
<!-- 配置版本号 -->
|
<!-- 配置版本号 -->
|
||||||
<Version>1.18.1.0</Version>
|
<Version>1.18.4.0</Version>
|
||||||
|
|
||||||
<UseWindowsForms>False</UseWindowsForms>
|
<UseWindowsForms>False</UseWindowsForms>
|
||||||
<ImplicitUsings>False</ImplicitUsings>
|
<ImplicitUsings>False</ImplicitUsings>
|
||||||
@@ -79,6 +79,15 @@
|
|||||||
<Copy SourceFiles="@(UnlockFpsExeSource)" DestinationFolder="$(OutputPath)" ContinueOnError="true" />
|
<Copy SourceFiles="@(UnlockFpsExeSource)" DestinationFolder="$(OutputPath)" ContinueOnError="true" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<!-- 声明unlockfps.exe为项目内容,确保MSIX打包时包含此文件 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="$(MSBuildThisFileDirectory)..\..\..\bin\unlockfps.exe">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Pack>true</Pack>
|
||||||
|
<PackagePath>unlockfps.exe</PackagePath>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Analyzer Files -->
|
<!-- Analyzer Files -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="ApiEndpoints.csv" />
|
<AdditionalFiles Include="ApiEndpoints.csv" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -140,4 +149,4 @@ internal partial class ScopedPage : Page
|
|||||||
|
|
||||||
Unloaded -= OnUnloaded;
|
Unloaded -= OnUnloaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,10 +264,13 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<shuxv:InfoBarView Margin="0,44,0,0" VerticalAlignment="Stretch"/>
|
<shuxv:InfoBarView Margin="0,44,0,0" VerticalAlignment="Stretch"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -430,4 +432,4 @@ internal sealed partial class CompactWebView2Window : Microsoft.UI.Xaml.Window,
|
|||||||
RefreshButton.Command = RefreshCommand;
|
RefreshButton.Command = RefreshCommand;
|
||||||
ProgressRing.Visibility = Visibility.Collapsed;
|
ProgressRing.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -213,4 +215,4 @@ internal sealed partial class WebView2Window : Microsoft.UI.Xaml.Window,
|
|||||||
{
|
{
|
||||||
contentProvider.ActualTheme = sender.ActualTheme;
|
contentProvider.ActualTheme = sender.ActualTheme;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -129,12 +130,39 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeThirdPartyToolsInBackgroundAsync(CancellationToken token)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ImmutableArray<ToolInfo> tools = await InitializeThirdPartyToolsAsync().ConfigureAwait(false);
|
// Yield to let navigation/UI finish first.
|
||||||
SentrySdk.AddBreadcrumb($"Initialized {tools.Length} third party tools", category: "ThirdPartyTool");
|
await Task.Yield();
|
||||||
thirdPartyToolsField.Value = tools;
|
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -142,7 +170,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
|||||||
SentrySdk.CaptureException(ex);
|
SentrySdk.CaptureException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("IdentifyMonitorsCommand")]
|
[Command("IdentifyMonitorsCommand")]
|
||||||
@@ -337,19 +366,26 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<ImmutableArray<ToolInfo>> InitializeThirdPartyToolsAsync()
|
private async ValueTask<ImmutableArray<ToolInfo>> InitializeThirdPartyToolsAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb("Starting to initialize third party tools", category: "ThirdPartyTool");
|
SentrySdk.AddBreadcrumb("Starting to initialize third party tools", category: "ThirdPartyTool");
|
||||||
IThirdPartyToolService thirdPartyToolService = serviceProvider.GetRequiredService<IThirdPartyToolService>();
|
IThirdPartyToolService thirdPartyToolService = serviceProvider.GetRequiredService<IThirdPartyToolService>();
|
||||||
SentrySdk.AddBreadcrumb("Got IThirdPartyToolService instance", category: "ThirdPartyTool");
|
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);
|
ImmutableArray<ToolInfo> tools = await thirdPartyToolService.GetToolsAsync().ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
SentrySdk.AddBreadcrumb($"Got {tools.Length} tools from service", category: "ThirdPartyTool");
|
SentrySdk.AddBreadcrumb($"Got {tools.Length} tools from service", category: "ThirdPartyTool");
|
||||||
|
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return ImmutableArray<ToolInfo>.Empty;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user