23 Commits

Author SHA1 Message Date
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
wangdage12
068eb65fef Add Discord server link to README
Added Discord server link for community engagement.
2026-01-01 12:57:42 +08:00
fanbook-wangdage
09a8cded2f Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2025-12-26 18:36:50 +08:00
fanbook-wangdage
c38fdf30d0 修复以管理员权限重启问题 2025-12-26 18:33:20 +08:00
wangdage12
bc1ff03d0a Update README with metadata progress and synchronization
Removed outdated metadata progress details and added synchronization note.
2025-12-22 21:05:40 +08:00
wangdage12
b288860c3b Update README.md 2025-12-19 23:13:19 +08:00
wangdage12
1e40a6e576 Revise status indicators and update links
Updated status indicators and URLs in the README.
2025-12-19 22:59:02 +08:00
wangdage12
d342b37dc0 Update HarvestDirectory path in wixproj file 2025-12-19 21:28:55 +08:00
fanbook-wangdage
179177a77c Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2025-12-19 21:20:14 +08:00
fanbook-wangdage
6c68a55d81 解决元数据导致的问题 2025-12-19 21:19:47 +08:00
wangdage12
7bd61c8035 Merge pull request #14 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6 in /.github/workflows
2025-12-17 20:20:52 +08:00
wangdage12
c19b71e2c4 Merge pull request #15 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/cache-5
Bump actions/cache from 4 to 5 in /.github/workflows
2025-12-17 20:20:38 +08:00
wangdage12
45b7383fc1 Merge pull request #16 from wangdage12/dependabot/github_actions/dot-github/workflows/dessant/lock-threads-6
Bump dessant/lock-threads from 5 to 6 in /.github/workflows
2025-12-17 20:20:23 +08:00
dependabot[bot]
c83a2f3e9d Bump dessant/lock-threads from 5 to 6 in /.github/workflows
Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 5 to 6.
- [Release notes](https://github.com/dessant/lock-threads/releases)
- [Changelog](https://github.com/dessant/lock-threads/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dessant/lock-threads/compare/v5...v6)

---
updated-dependencies:
- dependency-name: dessant/lock-threads
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 02:30:01 +00:00
dependabot[bot]
2bab0baf69 Bump actions/cache from 4 to 5 in /.github/workflows
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 02:29:57 +00:00
dependabot[bot]
2726e74731 Bump actions/upload-artifact from 5 to 6 in /.github/workflows
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 02:29:52 +00:00
35 changed files with 1353 additions and 145 deletions

View File

@@ -95,7 +95,7 @@ jobs:
- name: Cache NuGet packages - name: Cache NuGet packages
if: ${{ needs.select-runner.outputs.runner == 'windows-latest' }} if: ${{ needs.select-runner.outputs.runner == 'windows-latest' }}
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: ~/.nuget/packages path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }} key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }}
@@ -113,7 +113,7 @@ jobs:
- name: Upload signed msix - name: Upload signed msix
if: success() && github.event_name != 'pull_request' if: success() && github.event_name != 'pull_request'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }} name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix

View File

@@ -59,7 +59,7 @@ jobs:
- name: Cache NuGet packages - name: Cache NuGet packages
if: ${{ steps.merge.outputs.continue == 'true' }} if: ${{ steps.merge.outputs.continue == 'true' }}
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: ~/.nuget/packages path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }} key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }}
@@ -77,7 +77,7 @@ jobs:
- name: Upload signed msix - name: Upload signed msix
if: ${{ success() && steps.merge.outputs.continue == 'true' }} if: ${{ success() && steps.merge.outputs.continue == 'true' }}
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }} name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix

View File

@@ -17,7 +17,7 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v6
with: with:
issue-inactive-days: '30' issue-inactive-days: '30'
issue-comment: 'This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topic.' issue-comment: 'This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topic.'

View File

@@ -31,7 +31,7 @@ jobs:
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
- name: Upload MSI Artifact - name: Upload MSI Artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: Snap.Hutao-MSI name: Snap.Hutao-MSI
path: | path: |

View File

@@ -6,6 +6,8 @@
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用 该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
有条件的话可以加入discord服务器https://discord.gg/ucH3mgeWpQ
**English** **English**
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players. Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
@@ -25,28 +27,16 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx 项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx
> [!WARNING] > [!WARNING]
> 要使该项目可以长期运行,我们需要以下资源 > 要使该项目可以长期运行,我们需要以下资源
> 1. `src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs`中的新签名值 > 1. 元数据的编写
> 2. 元数据的编写 > 2. 图片资源
> 3. 图片资源
已同步原作者的元数据
V6.2的元数据已在编写中
测试仓库位置http://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata.Test
**目前元数据的编写进度:** **目前元数据的编写进度:**
| 项目V6.2 | 是否完成 | | 项目V6.2 | 是否完成 |
| ----------- | ----------- | | ----------- | ----------- |
| 新角色的基本数据 | ✔️ | | 总体数据 | ✔️ |
| 新版本角色/怪物基础数值 | ❔ |
| 新角色的详细资料、名片等 | ❌ |
| 新武器 | ✔️ |
| 新材料 | ❇️ |
| 新怪物 | ❇️ |
| 新圣遗物 | / |
| 新卡池 | ❇️ |
| 新成就 | ✔️ |
| 深境螺旋 | 💠 |
| 幻想真境剧诗 | 💠 |
| 幽境危战 | ✔️ |
✔️:已完成 ✔️:已完成
❌:未编写 ❌:未编写
@@ -81,30 +71,21 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
https://github.com/wangdage12/Snap.Metadata https://github.com/wangdage12/Snap.Metadata
镜像: 镜像:
![http://serverjp.wdg.cloudns.ch:3001/api/badge/6/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/6/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://server.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
![http://serverjp.wdg.cloudns.ch:3001/api/badge/7/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/7/status?style=flat-square)
http://serverjp.wdg.cloudns.ch:3000/wdg1122/Snap.Metadata
--- ---
**临时API** **临时API**
![http://serverjp.wdg.cloudns.ch:3001/api/badge/8/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/8/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://server.wdg.cloudns.ch:5222/ https://htserver.wdg.cloudns.ch/api/
![http://serverjp.wdg.cloudns.ch:3001/api/badge/9/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/9/status?style=flat-square)
http://serverjp.wdg.cloudns.ch:5222/
--- ---
**临时资源站:** **临时资源站:**
http://server.wdg.cloudns.ch:8007/
http://serverjp.wdg.cloudns.ch:8001/ https://htserver.wdg.cloudns.ch/

BIN
bin/unlockfps.exe Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
<Package <Package
Name="Snap.Hutao" Name="Snap.Hutao"
Manufacturer="Millennium Science Technology R-D Inst" Manufacturer="Millennium Science Technology R-D Inst"
Version="1.0.0.0" Version="1.18.2.0"
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68" UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
Scope="perMachine"> Scope="perMachine">

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

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

@@ -193,6 +193,32 @@ internal sealed class ProcessFactory
public static void StartUsingShellExecuteRunAs(string fileName) public static void StartUsingShellExecuteRunAs(string fileName)
{ {
// 尝试从app包中启动
try
{
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
{
FileName = fileName,
UseShellExecute = true,
Verb = "runas",
});
}catch
{
// 如果失败且filename含有Snap.Hutao.Unpackaged就直接用Snap.Hutao.exe重启
if (fileName.Contains("Snap.Hutao.Unpackaged"))
{
string currentDirectory = Directory.GetCurrentDirectory();
string unpackagedPath = Path.Combine(currentDirectory, "Snap.Hutao.exe");
if (File.Exists(unpackagedPath))
{
fileName = unpackagedPath;
}
// 否则抛出异常
else
{
throw;
}
// 重新尝试启动
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
{ {
FileName = fileName, FileName = fileName,
@@ -201,3 +227,5 @@ internal sealed class ProcessFactory
}); });
} }
} }
}
}

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.1.0" /> Version="1.18.1.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

@@ -24,8 +24,10 @@ internal static class AvatarViewBuilderExtension
{ {
if (detailedCharacter.Costumes is [{ Id: { } id }, ..]) if (detailedCharacter.Costumes is [{ Id: { } id }, ..])
{ {
MetadataCostume costume = avatar.Costumes.Single(c => c.Id == id); MetadataCostume? costume = avatar.Costumes.SingleOrDefault(c => c.Id == id);
if (costume != null)
{
ArgumentNullException.ThrowIfNull(costume.FrontIcon); ArgumentNullException.ThrowIfNull(costume.FrontIcon);
ArgumentNullException.ThrowIfNull(costume.SideIcon); ArgumentNullException.ThrowIfNull(costume.SideIcon);
@@ -34,6 +36,13 @@ internal static class AvatarViewBuilderExtension
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon); builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
} }
else else
{
// Costume not found in metadata, fallback to default avatar icon
builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
}
else
{ {
builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon); builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon); builder.View.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);

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;
@@ -100,9 +101,16 @@ internal abstract class AbstractLaunchExecutionInvoker
fileSystemReference.Exchange(beforeContext.FileSystem); fileSystemReference.Exchange(beforeContext.FileSystem);
using (IProcess? process = CreateProcess(beforeContext)) // unlockfps.exe会负责启动游戏
IProcess? process = null;
if (!beforeContext.LaunchOptions.IsIslandEnabled.Value)
{ {
if (process is null) process = CreateProcess(beforeContext);
}
using (process)
{
if (process is null && !beforeContext.LaunchOptions.IsIslandEnabled.Value)
{ {
return; return;
} }
@@ -114,7 +122,7 @@ internal abstract class AbstractLaunchExecutionInvoker
TaskContext = taskContext, TaskContext = taskContext,
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(), Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
LaunchOptions = context.LaunchOptions, LaunchOptions = context.LaunchOptions,
Process = process, Process = process ?? new NullProcess(),
IsOversea = targetScheme.IsOversea, IsOversea = targetScheme.IsOversea,
}; };
@@ -123,7 +131,8 @@ internal abstract class AbstractLaunchExecutionInvoker
await handler.ExecuteAsync(executionContext).ConfigureAwait(false); await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
} }
if (process.IsRunning) // 只有在没有启用Island且进程存在时才等待退出
if (process is { IsRunning: true })
{ {
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit)); progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
try try
@@ -139,6 +148,12 @@ internal abstract class AbstractLaunchExecutionInvoker
return; return;
} }
} }
else if (beforeContext.LaunchOptions.IsIslandEnabled.Value)
{
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
await taskContext.SwitchToBackgroundAsync();
await Task.Delay(30000).ConfigureAwait(false);
}
} }
progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited)); progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited));

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

@@ -10,6 +10,9 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<UseWPF>False</UseWPF> <UseWPF>False</UseWPF>
<!-- 配置版本号 -->
<Version>1.18.2.0</Version>
<UseWindowsForms>False</UseWindowsForms> <UseWindowsForms>False</UseWindowsForms>
<ImplicitUsings>False</ImplicitUsings> <ImplicitUsings>False</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -68,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" />
@@ -288,7 +299,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.3.14"> <PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.3.15">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

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

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

@@ -1,34 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
/// <summary>
/// Salt constants for data signing
/// Values are obtained from https://github.com/UIGF-org/Hoyolab.Salt
/// This file should normally be generated by Snap.Hutao.SourceGeneration.Automation.SaltConstantGenerator
/// But is provided manually when the generator fails to fetch values from the network.
///
/// IMPORTANT: For local builds, you must manually obtain salt values from:
/// https://github.com/UIGF-org/Hoyolab.Salt
/// </summary>
internal static class SaltConstants
{
// Version numbers - Update these according to the current miHoYo app versions
public const string CNVersion = "2.95.1";
public const string OSVersion = "2.54.0";
// Salt keys for Chinese (CN) server
// These are placeholder values - MUST be replaced with actual values from UIGF-org/Hoyolab.Salt
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
// Salt keys for Overseas (OS) server
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
// Note: The actual salt values are security-sensitive and should not be committed
// to public repositories. For local builds, obtain them from the UIGF organization
// and replace the placeholders above.
}

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