94 Commits

Author SHA1 Message Date
fanbook-wangdage
e9ed7928d6 Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2026-02-15 11:28:29 +08:00
fanbook-wangdage
4d2943d1c9 新增yae注入获取成就功能、修改yae逻辑
修复msix没有打包解锁器的问题
修复注入时米游社账号登录不起作用的问题
2026-02-15 11:28:17 +08:00
wangdage12
74e9427451 Update README with website and version highlights
Added official website link and highlighted version features.
2026-02-09 20:14:52 +08:00
fanbook-wangdage
cb6d728c35 提升版本号、解决CI报错 2026-02-07 13:17:31 +08:00
fanbook-wangdage
f87b80cc9e Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2026-02-07 13:08:35 +08:00
fanbook-wangdage
4b313b134e 新增msi安装界面,修复WebView2权限问题,修复切换服务器时会显示等待进程退出的问题,为页面添加缓存来提示频繁切换页面时的性能 2026-02-07 13:07:51 +08:00
wangdage12
0c775a5d3d Revise README for injection features and dependencies
Updated README to clarify injection functionality and added links to required repositories.
2026-01-29 11:54:32 +08:00
wangdage12
00cd5a8c07 Revise README for installation and server status
Updated installation instructions and server status information.
2026-01-27 14:00:53 +08:00
wangdage12
d93ae2bb83 Update README.md 2026-01-20 11:40:34 +08:00
fanbook-wangdage
2f148488f4 修复工具异步加载问题、添加武器和角色id、提示版本号 2026-01-16 12:33:26 +08:00
fanbook-wangdage
df92894307 支持公告中的发行版字段 2026-01-16 11:39:28 +08:00
fanbook-wangdage
5fad9ad855 提升版本号 2026-01-13 16:49:20 +08:00
fanbook-wangdage
1ed2f4f29e 支持注入时传命令行参数 2026-01-13 16:33:17 +08:00
fanbook-wangdage
db6df72791 添加using 2026-01-13 15:30:51 +08:00
fanbook-wangdage
bd9f188ac1 添加第三方工具功能 2026-01-13 15:17:20 +08:00
fanbook-wangdage
56c36a01ae Merge branches 'main' and 'main' of https://github.com/wangdage12/Snap.Hutao 2026-01-08 13:58:14 +08:00
fanbook-wangdage
da6f248509 支持调用genshin-fps-unlock项目来调整帧率、修复武器id问题 2026-01-08 13:57:17 +08:00
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
fanbook-wangdage
6d08f669e7 更新服务器公钥 2025-12-07 16:35:15 +08:00
fanbook-wangdage
84b9b97059 更新服务器域名、更新Sentry配置 2025-12-05 21:06:59 +08:00
wangdage12
0b846f11b7 Merge pull request #6 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5 in /.github/workflows
2025-12-03 22:52:23 +08:00
wangdage12
c9adc06210 Merge pull request #7 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/setup-dotnet-5
Bump actions/setup-dotnet from 4 to 5 in /.github/workflows
2025-12-03 22:51:59 +08:00
wangdage12
6c9f50b055 先删除ISSUE模板,之后有必要时再配置 2025-12-03 19:40:24 +08:00
wangdage12
6c515caa88 Update README 2025-12-03 16:54:06 +08:00
dependabot[bot]
b834ae5425 Bump actions/setup-dotnet from 4 to 5 in /.github/workflows
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 15:00:14 +00:00
wangdage12
49ae21e02c Merge pull request #8 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/checkout-6
Bump actions/checkout from 4 to 6 in /.github/workflows
2025-12-02 22:59:09 +08:00
wangdage12
88f81c5582 Update status of new monsters and achievements 2025-12-02 22:56:48 +08:00
wangdage12
274b2766ce Update MSI path in workflow configuration 2025-12-02 22:14:13 +08:00
dependabot[bot]
b9130979c1 Bump actions/upload-artifact from 4 to 5 in /.github/workflows
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 14:01:28 +00:00
wangdage12
5019987c08 Add Release configuration to MSI installer build 2025-12-02 22:00:31 +08:00
wangdage12
adaf0972ce Update TargetFramework and HarvestDirectory paths 2025-12-02 21:59:30 +08:00
wangdage12
b9169ad41a Fix indentation in MSI build step 2025-12-02 20:35:58 +08:00
wangdage12
00af738520 Add new weapon IDs to WeaponIds.cs 2025-12-02 20:05:44 +08:00
wangdage12
3538665bc6 Add files via upload 2025-12-02 18:21:25 +08:00
wangdage12
c1a1871284 Update README.md 2025-12-02 16:05:37 +08:00
wangdage12
01f965d260 Update completion status in README.md 2025-12-01 18:43:56 +08:00
wangdage12
cadd35a93e Revise README for clarity and updates
Updated README to reflect changes in functionality and metadata progress.
2025-12-01 16:22:52 +08:00
dependabot[bot]
0de6d4b71c Bump actions/checkout from 4 to 6 in /.github/workflows
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:55:54 +00:00
wangdage12
a83f119e8a 添加服务器状态 2025-11-27 22:07:07 +08:00
wangdage12
2278c83dd2 Merge pull request #2 from hoshiizumiya/patch-1
Revise README for injection feature and installation updates
2025-11-27 19:12:53 +08:00
wangdage12
867f8b6558 Fix build command in MSI workflow 2025-11-26 20:36:58 +08:00
wangdage12
7e6a6a509c Simplify build command in MSI workflow 2025-11-26 20:20:21 +08:00
wangdage12
7aaa61dcf3 Fix YAML syntax for dotnet publish command 2025-11-26 19:50:20 +08:00
wangdage12
1fe5b4969e Fix path for restoring NuGet packages 2025-11-26 19:46:39 +08:00
wangdage12
e52ed5470e Update NuGet restore command to specify solution file 2025-11-26 19:41:31 +08:00
wangdage12
4434d76e35 Update Wix Toolset version in workflow 2025-11-26 19:36:30 +08:00
wangdage12
4a9c3229a0 Update Wix Toolset version in CI workflow 2025-11-26 19:33:37 +08:00
wangdage12
1090dfa7c6 Fix case sensitivity in Wix Toolset installation command 2025-11-26 19:28:52 +08:00
wangdage12
a4eb3d6f8a Add GitHub Actions workflow for MSI build 2025-11-26 19:25:43 +08:00
wangdage12
d9c43844b7 Merge pull request #1 from hoshiizumiya/main
支持非打包模式直接启动并更改优化设置存储逻辑采用 wix 打包 msi 安装包支持脱离沙箱使用,为注入功能开发奠定基础
2025-11-26 19:24:03 +08:00
wangdage12
2cc2cc80ac 修复KeyPath问题 2025-11-26 19:18:01 +08:00
wangdage12
cc926e4352 指定TargetFramework值 2025-11-26 19:17:01 +08:00
wangdage12
fa4b975f46 Merge branch 'main' into main 2025-11-24 22:03:57 +08:00
wangdage12
299cdee329 Merge pull request #4 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/checkout-6
Bump actions/checkout from 5 to 6 in /.github/workflows
2025-11-24 21:24:48 +08:00
wangdage12
f49f40b5fd Merge pull request #3 from hoshiizumiya/base
完善并启用主题切换功能
2025-11-24 21:22:09 +08:00
dependabot[bot]
ff21a91cc6 Bump actions/checkout from 5 to 6 in /.github/workflows
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:33:17 +00:00
hoshiizumiya
cc47e050d1 完善并启用主题切换功能
补充部分相关本地化
2025-11-24 00:08:52 +08:00
hoshiizumiya
c2d0156a9d Update README.md 2025-11-23 22:10:08 +08:00
hoshiizumiya
eac314f7d9 Update README with packaging and resource details
Added packaging test instructions and resource notes.
2025-11-23 21:50:21 +08:00
hoshiizumiya
f1132b79f0 Revise README for injection feature and installation updates
Updated README to reflect changes in injection functionality and installation method.
2025-11-23 21:39:08 +08:00
hoshiizumiya
63c4792e00 添加注意 2025-11-23 21:29:19 +08:00
hoshiizumiya
5f196253b3 Enhance failure count handling in RepositoryAffinity
Add lower bounds protection for failure counts to prevent negative values and integer underflow.
2025-11-23 21:24:10 +08:00
hoshiizumiya
a7bb931ea5 Update copyright notice in PackageIdentityDiagnostics.cs 2025-11-23 20:47:40 +08:00
hoshiizumiya
d318dcbfd0 Update copyright information in PackageIdentityAdapter.cs 2025-11-23 20:46:54 +08:00
hoshiizumiya
e045413ac1 Update HarvestDirectory path for project configuration 2025-11-23 20:17:47 +08:00
hoshiizumiya
1039623cbf 完善安装
添加桌面图标,修正注册表安装目标
2025-11-23 20:10:02 +08:00
hoshiizumiya
3f50507490 Delete src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs
删除 wix 示例配置代码
2025-11-23 19:37:46 +08:00
hoshiizumiya
e55d6ea1c5 支持非打包模式直接启动并更改优化设置存储逻辑采用wix 打包 msi 安装包支持脱离沙箱使用,为注入功能开发奠定基础
新增 PackageIdentityAdapter 适配打包和非打包模式,支持通过文件系统路径替代 Windows.Storage.ApplicationData。重构 LocalSetting,抽象为 ISettingStorage 接口,支持 JSON 文件存储非打包模式下的设置。改进异常处理和日志记录,增加调试信息和启动诊断日志。隐藏 AppX 打包配置,更新依赖项版本,支持普通管理员模式下的游戏启动和 DLL 注入。补全 SaltConstants 文件,存储数据签名所需的盐值。

已知问题:
注入功能暂不不支持,缺少服务器提供的当前版本注入信息!
没有启用默认管理员权限打开
2025-11-23 16:58:05 +08:00
wangdage12
e96c9f1f49 Revise README for installation and resource details
Updated installation instructions and resource server notes.
2025-11-23 14:58:04 +08:00
wangdage12
f3d47aeffd 修改图片链接 2025-11-23 14:36:15 +08:00
wangdage12
7ee92db156 Update README with metadata repository information
Added metadata repository and mirror links to README.
2025-11-22 16:09:19 +08:00
wangdage12
7c25951950 Revise README for project resources and documentation
Updated the README to clarify project requirements and documentation links.
2025-11-21 21:53:55 +08:00
wangdage12
2ad17f60bf Update README with GetImage.py usage instructions
Added instructions for using GetImage.py to restore image resources from logs.
2025-11-21 15:16:02 +08:00
wangdage12
4fdea542b0 添加从请求日志中恢复旧图片资源的工具
添加从请求日志中恢复旧图片资源的工具
2025-11-21 14:59:30 +08:00
wangdage12
4d5e5342f2 Add README for development tools 2025-11-21 14:56:46 +08:00
wangdage12
fde960b7cc Update README with compilation instructions
Added a note about using Visual Studio 2026 for compilation.
2025-11-21 14:44:56 +08:00
wangdage12
cfc7ba6274 Delete .github/FUNDING.yml 2025-11-21 14:41:21 +08:00
wangdage12
c463f1809c Update README.md 2025-11-21 14:38:50 +08:00
wangdage12
3d352c1c12 Update README.md 2025-11-20 21:37:30 +08:00
wangdage12
fbf3c0c695 Update README.md 2025-11-20 21:15:25 +08:00
93 changed files with 3706 additions and 1187 deletions

13
.github/FUNDING.yml vendored
View File

@@ -1,13 +0,0 @@
# These are supported funding model platforms
github: [DGP-Studio]
patreon: # Replace with a single Patreon username
open_collective: snaphutao
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://afdian.com/a/DismissedLight

View File

@@ -1,93 +0,0 @@
name: 问题反馈
description: 通过这个议题向开发团队反馈你发现的程序中的问题
title: "[Bug]: 在这里填写一个合适的标题"
type: "Bug"
labels: ["priority:none"]
body:
- type: markdown
attributes:
value: |
> **请在上方以一句话简短地概括你的问题作为标题**
> 请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
- type: input
id: winver
attributes:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
validations:
required: true
- type: input
id: shver
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的反馈中心界面中可以找到
placeholder: 1.9.9.0
validations:
required: true
- type: input
id: deviceid
attributes:
label: 设备 ID
description: |
> 在胡桃工具箱的反馈中心界面,你可以找到并复制你的设备 ID
> 如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
> 如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe),它将显示你的设备 ID
validations:
required: true
- type: dropdown
id: user-set-category
attributes:
label: 问题分类
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options:
- 安装和环境
- 游戏启动器
- 祈愿记录
- 成就管理
- 我的角色
- 实时便笺
- 养成计算
- 深境螺旋/胡桃数据库
- Wiki
- 米游社账号面板
- 每日签到奖励
- 胡桃通行证/胡桃云
- 用户界面
- 文件缓存
- 公告
- 其它
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
如果你无法找到该日志,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: 你期望发生的行为?
description: 详细的描述你期望发生的行为,突出与目前(可能不正确的)行为的不同
validations:
required: false
- type: checkboxes
id: checklist-final
attributes:
label: 最后一步
description: 回顾你的回答
options:
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题
required: true

View File

@@ -1,26 +0,0 @@
name: 功能请求
description: 通过这个议题来向开发团队分享你的想法
title: "[Feat]: 在这里填写一个合适的标题"
type: "Feature"
labels: ["needs-triage", "priority:none"]
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单。
- type: textarea
id: back
attributes:
label: 背景与动机
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的议题
validations:
required: true
- type: textarea
id: req
attributes:
label: 想要实现或优化的功能
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
validations:
required: true

View File

@@ -1,84 +0,0 @@
name: 网络问题
description: 通过这个议题来反馈网络问题
title: "[Network]: 在这里填写一个合适的标题"
type: "Bug"
labels: ["area-Network"]
assignees:
- Lightczx
- Masterain98
body:
- type: markdown
attributes:
value: |
**请先在上方为工单设置一个合适的标题**
**请按下方的要求填写完整的问题表单,以便我们更快的定位问题。**
- type: textarea
id: network-diagnosis-report
attributes:
label: 提交你的网络诊断报告
description: |
停下!
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告并加密压缩,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
validations:
required: true
- type: input
id: user-geo-location
attributes:
label: 你的地理位置
description: |
中国用户请精确到省级行政区
海外用户请精确到国家
placeholder: 北京
validations:
required: true
- type: dropdown
id: user-isp
attributes:
label: 你的运营商
description: 海外用户请选其它
options:
- 中国电信
- 中国联通
- 中国移动
- 中国广电
- 其它
validations:
required: true
- type: dropdown
id: user-issue-category
attributes:
label: 你的问题
description: 选择一个问题类别
options:
- 完全无法连接服务器
- 连接速度慢
- 获取到了不正确的页面或数据
- 客户端图片下载错误
- 客户端图片预下载错误
- 其它
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 你的问题(补充)
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
validations:
required: false
- type: checkboxes
id: checklist-final
attributes:
label: 最后一步
description: 检查你提交的议题
options:
- label: 我已经在该议题中上传了包含网络诊断报告的加密压缩包
required: true

View File

@@ -1,93 +0,0 @@
name: BUG Report [English Form]
description: Tell us what issue you get
title: "[ENG][Bug]: Place your Issue Title Here"
type: "Bug"
labels: ["priority:none"]
body:
- type: markdown
attributes:
value: |
> **Please use one sentence to briefly describe your issue as title above**
> Please follow the instruction below to fill the form, so we can locate the issue quickly
- type: input
id: winver
attributes:
label: Windows Version
description: |
Use `Win+R` and input `winver`, Windows build version is usually at the second line
placeholder: e.g. 22000.556
validations:
required: true
- type: input
id: shver
attributes:
label: Snap Hutao Version
description: You can find the version in application's title bar
placeholder: e.g. 1.9.9.0
validations:
required: true
- type: input
id: deviceid
attributes:
label: Device ID
description: |
> In Snap Hutao's Feedback Center, you can find and copy your device ID
> If your issue is about program crash, please fill this so we can dump the log and locate the source easier
> If your program cannot startup, please download and run [Diagnostic Tooling](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe), it will shows your device ID.
validations:
required: true
- type: dropdown
id: user-set-category
attributes:
label: Issue Category
description: Please select the most associated category of your issue
options:
- Installation and Environment
- Game Launcher
- Wish Export
- Achievement
- My Character
- Realtime Note
- Develop Plan
- Spiral Abyss
- Wiki
- MiHoYo Account Panel
- Daily Checkin Reward
- Hutao Passport/Hutao Cloud
- User Interface
- File Cache
- Announcement
- Other
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What Happened?
description: |
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
If you cannot find it, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: What is expected?
description: Describe expected outcome, highlight the difference with current outcome
validations:
required: false
- type: checkboxes
id: checklist-final
attributes:
label: Last Step
description: Review your Form
options:
- label: I believe the description above is detail enough to allow developers to reproduce the issue
required: true

View File

@@ -1,26 +0,0 @@
name: Feature Request [English Form]
description: Tell us about your thought
title: "[Feat]: Place your title here"
type: "Feature"
labels: ["needs-triage", "priority:none"]
body:
- type: markdown
attributes:
value: |
Please fill the form below
- type: textarea
id: back
attributes:
label: Background & Motivation
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them.
validations:
required: true
- type: textarea
id: req
attributes:
label: Detail of the Feature
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
validations:
required: true

View File

@@ -1,79 +0,0 @@
name: Network Issue [English Form]
description: Submit this issue form when network issue affect your client experience
title: "[Network]: Place your title here"
type: "Bug"
labels: ["area-Network"]
assignees:
- Lightczx
- Masterain98
body:
- type: markdown
attributes:
value: |
**Please use one sentence to briefly describe your issue as title above**
**Please follow the instruction below to fill the form, so we can locate the issue quickly**
- type: textarea
id: network-diagnosis-report
attributes:
label: Submit Your Network Diagnosis Report
description: |
STOP HERE!
**Please run our network diagnosis tool before filling this form**
**The diagnosis tool will generate a report and add it into a password-protected archive. Drag the `.zip` archive to the box below so it can be uploaded.**
- Use the following link to download the Network Diagnosis Tool:
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
validations:
required: true
- type: input
id: user-geo-location
attributes:
label: Your Geographical Location
description: |
Description accurate to country
placeholder: USA
validations:
required: true
- type: input
id: user-isp
attributes:
label: Your ISP Name
description: |
Name of your Internet service provider
placeholder: AT&T
validations:
required: true
- type: dropdown
id: user-issue-category
attributes:
label: Issue Category
description: Select an issue category
options:
- Cannot connect to server completely
- Slow spped
- Fetched wrong page or data
- Image download error in the client
- Image set pre-download error (client welcome wizard process)
- Other
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: Your Issue (cont.)
description: If you selected `Other` in previous dropdown, please explain your issue in detail here.
validations:
required: false
- type: checkboxes
id: checklist-final
attributes:
label: One Last Step
description: Check your issue form
options:
- label: I confirm I have attached the network diagnosis report archive in the issue
required: true

View File

@@ -1,31 +0,0 @@
name: Publish Process
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
title: "[Publish]: Version 1.9.98"
labels: ["Publish"]
assignees:
- Lightczx
body:
- type: textarea
id: main-body
attributes:
label: Publish Process
value: |
## 创建版本
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
- [ ] 发布 RC 版本Optional
- [ ] 合并入主分支
- [ ] 整理更新内容,等待翻译
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
- [ ] 更新日志
- [ ] 功能文档更新
- type: checkboxes
id: checklist-final
attributes:
label: Final Check
description: Understand what you are doing
options:
- label: I understand that I will get banned from repository if I don't have permission to use this template
required: true

View File

@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Snap Hutao 官方文档 / Snap Hutao Document
url: https://hut.ao
about: 请在提出问题前阅读文档 / Read the document before submit the issue
- name: 常见问题 / FAQ
url: https://hut.ao/advanced/FAQ.html
about: 常见的用户提出的问题 / Common questions asked by users
- name: 常见程序异常 / Common Program Exceptions
url: https://hut.ao/advanced/exceptions.html
about: 用户通常能自行解决这些问题 / Users may solve these problems by themselves

View File

@@ -1,13 +0,0 @@
name: 内部任务
description: 此Issue模板仅用于创建内部任务非 DGP Studio 成员请勿使用
title: "[Task]: 在这里填写一个合适的标题"
type: "Task"
labels: ["priority:none"]
body:
- type: textarea
id: content
attributes:
label: 背景与动机
description: 添加相关的说明
validations:
required: true

View File

@@ -73,7 +73,7 @@ jobs:
runs-on: ${{ needs.select-runner.outputs.runner }} runs-on: ${{ needs.select-runner.outputs.runner }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup .NET (self-hosted) - name: Setup .NET (self-hosted)
if: ${{ needs.select-runner.outputs.runner == 'sjc1' }} if: ${{ needs.select-runner.outputs.runner == 'sjc1' }}
@@ -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

@@ -21,7 +21,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
ref: develop ref: develop
fetch-depth: 0 fetch-depth: 0
@@ -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.'

38
.github/workflows/msi-build.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Build MSI Installer
on:
push:
branches: [ main ]
pull_request:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout source
uses: actions/checkout@v6
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Install Wix Toolset 4
run: dotnet tool install --global wix --version 4.0.1
- name: Restore NuGet packages
run: dotnet restore src/Snap.Hutao/Snap.Hutao.slnx
- name: Build WinUI 3 project (self-contained)
run: dotnet build src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj -c Release
- name: Build MSI installer
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
- name: Upload MSI Artifact
uses: actions/upload-artifact@v6
with:
name: Snap.Hutao-MSI
path: |
src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US/*.msi

7
.gitignore vendored
View File

@@ -1,4 +1,4 @@
desktop.ini desktop.ini
*.csproj.user *.csproj.user
*.DotSettings.user *.DotSettings.user
@@ -24,4 +24,7 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
tools/ tools/
src/Snap.Hutao/Snap.Hutao/AppPackages src/Snap.Hutao/Snap.Hutao/AppPackages
/src/Snap.Hutao/Snap.Hutao.Installer/obj
/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US
/src/Snap.Hutao/Snap.Hutao.Installer/bin

192
README.md
View File

@@ -1,54 +1,21 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/976e057c-f01e-486b-9fa0-04744ae96f99" alt="Snap Hutao Banner" width="600"/>
</p>
<h1 align="center">Snap Hutao</h1>
<p align="center">
🎮 开源的原神工具箱,专为 Windows 平台设计,改善桌面端玩家的游戏体验
<br/>
🎮 An open-source Genshin Impact toolkit for Windows, designed to improve the desktop gaming experience
</p>
<div align="center">
<table>
<tr>
<td align="center" style="padding:0 10px;">
<b>Latest CI/CD Build</sub>
</td>
<td align="center" style="padding:0 10px;">
<b>Latest Release</sub>
</td>
<td align="center" style="padding:0 10px;">
<b>Downloads</sub>
</td>
</tr>
<tr>
<td align="center" style="padding:0 10px;">
<a href="https://ci.appveyor.com/project/DGP-Studio/snap-hutao">
<img src="https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true" alt="Build Status"/>
</a>
</td>
<td align="center" style="padding:0 10px;">
<a href="https://github.com/DGP-Studio/Snap.Hutao/releases/latest">
<img src="https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat" alt="Release"/>
</a>
</td>
<td align="center" style="padding:0 10px;">
<img src="https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat" alt="Downloads"/>
</td>
</tr>
</table>
</div>
---
## 📖 简介 / Introduction ## 📖 简介 / Introduction
**中文** **中文**
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。 胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
自带的注入功能只有FPS调整只保证FPS调整长期可用你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
官网https://htserver.wdg.cloudns.ch/
**该版本的特点:**
- 尽量保留原版功能,少重写功能,稳定性强
- 只集成没有争议的安全的注入功能
- 大部分注入功能以第三方工具形式提供,点击即用
- 永久免费的云抽卡日志
有条件的话可以加入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.
@@ -56,89 +23,92 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
## 🚀 安装 / Installation ## 🚀 安装 / Installation
**中文** 目前 Sanp.Hutao.Rev 更新了打包方式,并采用了标准现代的 msi 安装,方便程序获取管理员权限和更多的功能设置,不再需要原 Depolyment
你可以按照 [快速开始](https://hut.ao/zh/quick-start.html) 文档中提供的流程安装并设置 Snap Hutao。
**English** 只有`.msi`安装包安装的可以和之前的版本共存,如果通过`.msix`安装包安装则可能出现`0x80073CF3`,备份旧版本数据文件夹后卸载旧版本即可继续安装,将旧版本数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
有时候我们在对某些功能有重大更改时发布测试版可在官网的下载可加入discord服务器报告功能使用情况和获取测试通知
--- ---
## 🌍 本地化翻译 / Localization ## 开发
项目启动位置已升级为 VS2026 的 slnx 格式 Snap.Hutao\src\Snap.Hutao\Snap.Hutao.slnx
> [!WARNING]
> 要使该项目可以长期运行,我们需要以下资源
> 1. 元数据的编写
> 2. 图片资源
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。 已同步原作者的元数据
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project. **目前元数据的编写进度:**
| Language | Status | | 项目V6.3 | 是否完成 |
|----------|--------| | ----------- | ----------- |
| zh-TW | [![zh-TW](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.zh-TW&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | | 总体数据 | ✔️ |
| en | [![en](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.en&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| fr | [![fr](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.fr&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | ✔️:已完成
| id | [![id](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.id&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | ❌:未编写
| ja | [![ja](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.ja&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | ❇️:编写中
| ko | [![ko](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.ko&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | ❔:数据暂时无法得到
| pt-PT | [![pt-PT](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.pt-PT&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | / :似乎不需要变动
| ru | [![ru](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.ru&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) | 💠:低优先级,以后编写
| vi | [![vi](https://img.shields.io/badge/dynamic/json?color=blue&label=vi&style=flat&logo=crowdin&query=%24.vi&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
**若需编译项目,请使用[Visual Studio 2026](https://visualstudio.microsoft.com/zh-hans/)**
调试选项请选择unpackaged不打包
**原开发文档现在还可使用其中的AI功能很好用以下是开发文档链接**
https://deepwiki.com/DGP-Studio/Snap.Hutao
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
## 资源和服务器状态
<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)
--- ---
## 🛠️ 贡献 / Contribute **元数据仓库:**
https://github.com/wangdage12/Snap.Metadata
- [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html) 仓库镜像:
- [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs) ![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)
- [通过 DeepWiKi 了解项目结构 / Understand Project Structure with DeepWiKi](https://deepwiki.com/DGP-Studio/Snap.Hutao)
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/DGP-Studio/Snap.Hutao) http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
--- ---
## 🙏 特别感谢 / Special Thanks **API**
- [HolographicHat](https://github.com/HolographicHat) ![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)
- [UIGF organization](https://uigf.org)
**特定的原神项目 / Specific Genshin-related Projects** https://htserver.wdg.cloudns.ch/api/
- [Scighost/Starward](https://github.com/Scighost/Starward)
--- ---
## ⚙️ 使用的技术栈 / Tech Stack **图片资源站:**
- [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet) https://htserver.wdg.cloudns.ch/
- [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
- [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
- [dotnet/efcore](https://github.com/dotnet/efcore)
- [dotnet/runtime](https://github.com/dotnet/runtime)
- [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
- [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
- [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
- [quartznet/quartznet](https://github.com/quartznet/quartznet)
---
## ❤️ 赞助商 / Sponsorship
Snap Hutao is currently using sponsored software from the following service providers.
<img src="./res/assets/readmeSponsors.svg" alt="Readme Sponsors" />
- 🏠 [Netlify](https://www.netlify.com/) provides document and home page hosting service for Snap Hutao
- 🌍 [Crowdin](https://crowdin.com/) provides its SaaS platform to help Snap Hutao's localization
- 🗄️ [Navicat](https://navicat.com/) provides Snap Hutao with advanced database management tools
- 🔒 Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
- 🔑 [1Password](https://1password.com/) provides Snap Hutao development team with their amazing password management software
- 🐳 [DigitalOcean](https://www.digitalocean.com) provides reliable cloud database and container service for Snap Hutao database backup
- 📊 [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
- ☁️ [Cloudflare](https://www.cloudflare.com/) sponsors Snap Hutao with their Business Plan, ensuring secure, fast, and reliable worldwide connection to our infrastructure
- 🔐 [Termius](https://termius.com) provides a secure, reliable, and collaborative SSH client
---
## 📈 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
[![](https://opengraph.snapgenshin.cn/gitcode?repo=DGP-Studio/Snap.Hutao)](https://github.com/DGP-Studio/Snap.Hutao)

BIN
bin/unlockfps.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
</Wix>

View File

@@ -0,0 +1,11 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<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>

View File

@@ -0,0 +1,90 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package
Name="Snap.Hutao"
Manufacturer="Millennium Science Technology R-D Inst"
Version="1.18.4.0"
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
Language="2052"
Scope="perMachine">
<Property Id="ApplicationFolderName" Value="Snap.Hutao" />
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<MediaTemplate EmbedCab="yes" />
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<Feature Id="MainApp" Title="!(loc.MainAppTitle)" Level="1">
<ComponentGroupRef Id="MainAppComponents" />
</Feature>
<Feature Id="DesktopShortcutFeature" Title="!(loc.DesktopShortcutTitle)" Level="1">
<ComponentRef Id="DesktopShortcut" />
</Feature>
<Feature Id="StartMenuShortcutFeature" Title="!(loc.StartMenuShortcutTitle)" Level="1">
<ComponentRef Id="ApplicationShortcut" />
</Feature>
</Package>
<!-- 安装目录 -->
<Fragment>
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="Snap.Hutao" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="Snap Hutao" />
</StandardDirectory>
<StandardDirectory Id="DesktopFolder" />
</Fragment>
<!-- 桌面快捷方式 -->
<Fragment>
<Component Id="DesktopShortcut" Directory="DesktopFolder" Guid="*">
<Shortcut
Id="DesktopShortcut_Normal"
Name="Snap Hutao"
Description="Snap Hutao Client"
Target="[INSTALLFOLDER]Snap.Hutao.exe"
WorkingDirectory="INSTALLFOLDER" />
<!-- KeyPath 必须是 HKCU因为快捷方式安装到用户目录 -->
<RegistryValue
Root="HKCU"
Key="Software\Snap.Hutao"
Name="DesktopShortcutInstalled"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</Fragment>
<!-- 开始菜单快捷方式 -->
<Fragment>
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="*">
<Shortcut
Id="ApplicationStartMenuShortcut"
Name="Snap Hutao"
Description="Snap Hutao Client"
Target="[INSTALLFOLDER]Snap.Hutao.exe"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall" />
<!-- KeyPath 依然必须改为 HKCU -->
<RegistryValue
Root="HKCU"
Key="Software\Snap.Hutao"
Name="StartMenuShortcutInstalled"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</Fragment>
</Wix>

View File

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

View File

@@ -0,0 +1,33 @@
<Project Sdk="WixToolset.Sdk/6.0.2">
<PropertyGroup>
<SuppressIces>ICE03;ICE60</SuppressIces>
<Platform>x64</Platform>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<Configuration>Release</Configuration>
<DefaultCulture>zh-CN</DefaultCulture>
<Cultures>zh-CN;en-US</Cultures>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="..\Snap.Hutao\bin\Release\net10.0-windows10.0.26100.0\win-x64">
<ComponentGroupName>MainAppComponents</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressCom>true</SuppressCom>
<SuppressRegistry>true</SuppressRegistry>
<SuppressRootDirectory>true</SuppressRootDirectory>
</HarvestDirectory>
<PackageReference Include="WixToolset.Heat" Version="6.0.2" />
<PackageReference Include="WixToolset.UI.wixext" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<WixLocalization Include="Package.zh-cn.wxl" />
<WixLocalization Include="Package.en-us.wxl" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,8 @@
# Snap.Hutao.SourceGeneration # Snap.Hutao.SourceGeneration
> 生成器包的备份目前还可以从nuget上获取所以暂时不需要使用该目录
> https://www.nuget.org/packages/Snap.Hutao.SourceGeneration/1.3.14
Source Code Generator for Snap.Hutao Source Code Generator for Snap.Hutao
# Development Guideline # Development Guideline
@@ -7,4 +11,4 @@ Use https://roslynquoter.azurewebsites.net/ to get SyntaxTree
1. Every `IncrementalValue(s)Provider<T>`'s step result should be an `IEquatable<T>` to make it really becomes incremental. 1. Every `IncrementalValue(s)Provider<T>`'s step result should be an `IEquatable<T>` to make it really becomes incremental.
2. So the intermediate models should be a `record (class/struct)` if possible 2. So the intermediate models should be a `record (class/struct)` if possible
3. Intermediate array/enumerable should be a `ImmutableArray<T>` if possible, the pipeline use IA internally and has special check for it. 3. Intermediate array/enumerable should be a `ImmutableArray<T>` if possible, the pipeline use IA internally and has special check for it.

View File

@@ -1,24 +1,24 @@
<Solution> <Solution>
<Configurations> <Configurations>
<Platform Name="Any CPU" /> <Platform Name="Any CPU" />
<Platform Name="ARM64" />
<Platform Name="x64" /> <Platform Name="x64" />
<Platform Name="x86" />
</Configurations> </Configurations>
<Folder Name="/Solution Items/"> <Folder Name="/Solution Items/">
<File Path=".editorconfig" /> <File Path=".editorconfig" />
<File Path=".vsconfig" /> <File Path=".vsconfig" />
</Folder> </Folder>
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj" /> <Project Path="Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" Id="91a04cd0-28cc-4562-92e1-202bc163edd7">
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
<!-- For Rider -->
<Configuration Solution="Debug|Any CPU" Project="Debug|x64|Deploy" />
<Configuration Solution="Debug|x64" Project="Debug|x64|Deploy" />
<Configuration Solution="Release|Any CPU" Project="Release|x64|Deploy" />
<Configuration Solution="Release|x64" Project="Release|x64|Deploy" />
<!-- For Visual Studio -->
<Platform Solution="*|Any CPU" Project="x64" /> <Platform Solution="*|Any CPU" Project="x64" />
<Platform Solution="*|arm64" Project="arm64" /> </Project>
<Platform Solution="*|x64" Project="x64" /> <Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj">
<Platform Solution="*|x86" Project="x86" /> <Build Solution="*|ARM64" Project="false" />
<Deploy /> <Build Solution="*|x86" Project="false" />
</Project>
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
<Platform Project="x64" />
<Deploy Solution="*|Any CPU" />
<Deploy Solution="*|x64" />
</Project> </Project>
</Solution> </Solution>

View File

@@ -13,6 +13,7 @@ using Snap.Hutao.Service;
using Snap.Hutao.UI.Xaml; using Snap.Hutao.UI.Xaml;
using Snap.Hutao.UI.Xaml.Control.Theme; using Snap.Hutao.UI.Xaml.Control.Theme;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -64,6 +65,11 @@ public sealed partial class App : Application
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override void OnLaunched(LaunchActivatedEventArgs args)
{ {
// ⚠️ 添加启动诊断
#if DEBUG
Core.ApplicationModel.PackageIdentityDiagnostics.LogDiagnostics();
#endif
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT(); DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
try try
@@ -71,17 +77,40 @@ public sealed partial class App : Application
// Important: You must call AppNotificationManager::Default().Register // Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs. // before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked; AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
AppNotificationManager.Default.Register();
try
{
AppNotificationManager.Default.Register();
}
catch
{
// In unpackaged mode, this might fail - continue anyway
}
// E_INVALIDARG E_OUTOFMEMORY // E_INVALIDARG E_OUTOFMEMORY
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); AppActivationArguments? activatedEventArgs = null;
PrivateNamedPipeClient? namedPipeClient = null;
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs)) try
{ {
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao")); activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
XamlApplicationLifetime.ActivationAndInitializationCompleted = true; namedPipeClient = serviceProvider.GetRequiredService<PrivateNamedPipeClient>();
Exit(); }
return; catch
{
// In unpackaged mode, AppInstance might not work
// Create a default activation argument for launch
}
if (activatedEventArgs is not null && namedPipeClient is not null)
{
if (namedPipeClient.TryRedirectActivationTo(activatedEventArgs))
{
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
Exit();
return;
}
} }
logger.LogInformation($"{ConsoleBanner}"); logger.LogInformation($"{ConsoleBanner}");
@@ -90,10 +119,30 @@ public sealed partial class App : Application
// Manually invoke // Manually invoke
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application")); SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
activation.ActivateAndInitialize(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
HutaoActivationArguments hutaoArgs = activatedEventArgs is not null
? HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)
: HutaoActivationArguments.CreateDefaultLaunchArguments();
activation.ActivateAndInitialize(hutaoArgs);
} }
catch (Exception ex) catch (Exception ex)
{ {
// ⚠️ 添加更详细的异常日志
try
{
string errorPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Hutao",
"startup_error.txt");
Directory.CreateDirectory(Path.GetDirectoryName(errorPath)!);
File.WriteAllText(errorPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Error:\n{ex}");
}
catch
{
// Ignore
}
SentrySdk.CaptureException(ex); SentrySdk.CaptureException(ex);
SentrySdk.Flush(); SentrySdk.Flush();

View File

@@ -31,8 +31,15 @@ public static partial class Bootstrap
[STAThread] [STAThread]
private static void Main(string[] args) private static void Main(string[] args)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
#endif
if (Mutex.TryOpenExisting(LockName, out _)) if (Mutex.TryOpenExisting(LockName, out _))
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Another instance is running");
#endif
return; return;
} }
@@ -42,9 +49,16 @@ public static partial class Bootstrap
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow)); mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity); mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
Debug.Assert(created); Debug.Assert(created);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Mutex created");
#endif
} }
catch (WaitHandleCannotBeOpenedException) catch (WaitHandleCannotBeOpenedException)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] WaitHandleCannotBeOpenedException");
#endif
return; return;
} }
@@ -54,34 +68,70 @@ public static partial class Bootstrap
{ {
if (!OSPlatformSupported()) if (!OSPlatformSupported())
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] OS not supported");
#endif
return; return;
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Setting environment variables");
#endif
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000"); Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128"); Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false); AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing COM wrappers");
#endif
ComWrappersSupport.InitializeComWrappers(); ComWrappersSupport.InitializeComWrappers();
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing DI container");
#endif
// By adding the using statement, we can dispose the injected services when closing // By adding the using statement, we can dispose the injected services when closing
using (ServiceProvider serviceProvider = DependencyInjection.Initialize()) using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
{ {
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread"; Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Calling Application.Start()");
#endif
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging // If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
// You can delete bin and obj folder and then rebuild. // You can delete bin and obj folder and then rebuild.
// In a Desktop app this runs a message pump internally, // In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down. // and does not return until the application shuts down.
Application.Start(AppInitializationCallback); Application.Start(AppInitializationCallback);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Application.Start() returned");
#endif
XamlApplicationLifetime.Exited = true; XamlApplicationLifetime.Exited = true;
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Flushing Sentry");
#endif
SentrySdk.Flush(); SentrySdk.Flush();
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
#endif
} }
private static void InitializeApp(ApplicationInitializationCallbackParams param) private static void InitializeApp(ApplicationInitializationCallbackParams param)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Callback invoked");
#endif
Gen2GcCallback.Register(() => Gen2GcCallback.Register(() =>
{ {
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime")); SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
@@ -90,8 +140,17 @@ public static partial class Bootstrap
IServiceProvider serviceProvider = Ioc.Default; IServiceProvider serviceProvider = Ioc.Default;
_ = serviceProvider.GetRequiredService<ITaskContext>(); #if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
#endif
// ⚠️ 只创建 App
// TaskContext 将在第一次被需要时自动创建(延迟初始化)
_ = serviceProvider.GetRequiredService<App>(); _ = serviceProvider.GetRequiredService<App>();
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
#endif
} }
private static bool OSPlatformSupported() private static bool OSPlatformSupported()

View File

@@ -4,14 +4,13 @@
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core.ApplicationModel; namespace Snap.Hutao.Core.ApplicationModel;
internal static class LimitedAccessFeatures internal static class LimitedAccessFeatures
{ {
private static readonly string PackagePublisherId = Package.Current.Id.PublisherId; private static readonly string PackagePublisherId = PackageIdentityAdapter.PublisherId;
private static readonly string PackageFamilyName = Package.Current.Id.FamilyName; private static readonly string PackageFamilyName = PackageIdentityAdapter.FamilyName;
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary( private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
[ [
@@ -67,8 +66,15 @@ internal static class LimitedAccessFeatures
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717") KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
]); ]);
public static LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId) public static Windows.ApplicationModel.LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
{ {
if (!PackageIdentityAdapter.HasPackageIdentity)
{
// In unpackaged mode, we can't unlock limited access features
// Create a dummy result - actual implementation will handle the failure
return default;
}
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId)); return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
} }

View File

@@ -0,0 +1,109 @@
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Reflection;
namespace Snap.Hutao.Core.ApplicationModel;
/// <summary>
/// Adapter to handle both packaged and unpackaged app scenarios
/// </summary>
internal static class PackageIdentityAdapter
{
private static readonly Lazy<bool> LazyHasPackageIdentity = new(CheckPackageIdentity);
private static readonly Lazy<string> LazyAppDirectory = new(GetAppDirectoryPath);
private static readonly Lazy<Version> LazyAppVersion = new(GetAppVersionInternal);
private static readonly Lazy<string> LazyFamilyName = new(GetFamilyNameInternal);
private static readonly Lazy<string> LazyPublisherId = new(GetPublisherIdInternal);
/// <summary>
/// Check if the app has package identity
/// </summary>
public static bool HasPackageIdentity => LazyHasPackageIdentity.Value;
/// <summary>
/// Get application installation directory
/// </summary>
public static string AppDirectory => LazyAppDirectory.Value;
/// <summary>
/// Get application version
/// </summary>
public static Version AppVersion => LazyAppVersion.Value;
/// <summary>
/// Get package family name (or fallback for unpackaged)
/// </summary>
public static string FamilyName => LazyFamilyName.Value;
/// <summary>
/// Get publisher ID (or fallback for unpackaged)
/// </summary>
public static string PublisherId => LazyPublisherId.Value;
private static bool CheckPackageIdentity()
{
try
{
// Try to access Package.Current - if it throws, we don't have package identity
_ = Windows.ApplicationModel.Package.Current.Id;
return true;
}
catch
{
return false;
}
}
private static string GetAppDirectoryPath()
{
if (HasPackageIdentity)
{
return Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
}
// Unpackaged: use the exe directory
string? exePath = Process.GetCurrentProcess().MainModule?.FileName;
ArgumentException.ThrowIfNullOrEmpty(exePath);
string? directory = Path.GetDirectoryName(exePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
return directory;
}
private static Version GetAppVersionInternal()
{
if (HasPackageIdentity)
{
return Windows.ApplicationModel.Package.Current.Id.Version.ToVersion();
}
// Unpackaged: use assembly version
Assembly assembly = Assembly.GetExecutingAssembly();
Version? version = assembly.GetName().Version;
return version ?? new Version(1, 0, 0, 0);
}
private static string GetFamilyNameInternal()
{
if (HasPackageIdentity)
{
return Windows.ApplicationModel.Package.Current.Id.FamilyName;
}
// Unpackaged: use a deterministic fallback
return "Snap.Hutao.Unpackaged";
}
private static string GetPublisherIdInternal()
{
if (HasPackageIdentity)
{
return Windows.ApplicationModel.Package.Current.Id.PublisherId;
}
// Unpackaged: use a fallback
return "CN=Millennium-Science-Technology-R-D-Inst";
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
using System.IO;
namespace Snap.Hutao.Core.ApplicationModel;
/// <summary>
/// Diagnostic helper for PackageIdentityAdapter
/// </summary>
internal static class PackageIdentityDiagnostics
{
public static void LogDiagnostics()
{
try
{
string logPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Hutao",
"startup_diagnostics.txt");
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
using (StreamWriter writer = File.CreateText(logPath))
{
writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Diagnostics");
writer.WriteLine($"HasPackageIdentity: {PackageIdentityAdapter.HasPackageIdentity}");
writer.WriteLine($"AppVersion: {PackageIdentityAdapter.AppVersion}");
writer.WriteLine($"AppDirectory: {PackageIdentityAdapter.AppDirectory}");
writer.WriteLine($"FamilyName: {PackageIdentityAdapter.FamilyName}");
writer.WriteLine($"PublisherId: {PackageIdentityAdapter.PublisherId}");
writer.WriteLine("---");
}
Debug.WriteLine($"Diagnostics written to: {logPath}");
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to write diagnostics: {ex.Message}");
}
}
}

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

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -17,7 +18,20 @@ internal sealed partial class HutaoDiagnostics : IHutaoDiagnostics
[GeneratedConstructor] [GeneratedConstructor]
public partial HutaoDiagnostics(IServiceProvider serviceProvider); public partial HutaoDiagnostics(IServiceProvider serviceProvider);
public ApplicationDataContainer LocalSettings { get => ApplicationData.Current.LocalSettings; } public ApplicationDataContainer? LocalSettings
{
get
{
if (PackageIdentityAdapter.HasPackageIdentity)
{
return ApplicationData.Current.LocalSettings;
}
// In unpackaged mode, ApplicationDataContainer is not available
// Return null - scripting/diagnostics code should handle this gracefully
return null;
}
}
public async ValueTask<int> ExecuteSqlAsync(string sql) public async ValueTask<int> ExecuteSqlAsync(string sql)
{ {

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")] [SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
public interface IHutaoDiagnostics public interface IHutaoDiagnostics
{ {
ApplicationDataContainer LocalSettings { get; } ApplicationDataContainer? LocalSettings { get; }
ValueTask<int> ExecuteSqlAsync(string sql); ValueTask<int> ExecuteSqlAsync(string sql);

View File

@@ -4,6 +4,7 @@
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Microsoft.Win32; using Microsoft.Win32;
using Microsoft.Windows.AppNotifications; using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Hashing;
@@ -13,31 +14,34 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Windows.ApplicationModel;
using Windows.Storage;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
internal static class HutaoRuntime internal static class HutaoRuntime
{ {
public static Version Version { get; } = Package.Current.Id.Version.ToVersion(); public static Version Version { get; } = PackageIdentityAdapter.AppVersion;
public static string UserAgent { get; } = $"Snap Hutao/{Version}"; public static string UserAgent { get; } = $"Snap Hutao/{Version}";
public static string DataDirectory { get; } = InitializeDataDirectory(); public static string DataDirectory { get; } = InitializeDataDirectory();
public static string LocalCacheDirectory { get; } = ApplicationData.Current.LocalCacheFolder.Path; public static string LocalCacheDirectory { get; } = InitializeLocalCacheDirectory();
public static string FamilyName { get; } = Package.Current.Id.FamilyName; public static string FamilyName { get; } = PackageIdentityAdapter.FamilyName;
public static string DeviceId { get; } = InitializeDeviceId(); public static string DeviceId { get; } = InitializeDeviceId();
public static WebView2Version WebView2Version { get; } = InitializeWebView2(); public static WebView2Version WebView2Version { get; } = InitializeWebView2();
public static bool IsProcessElevated { get; } = LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess; public static string WebView2UserDataDirectory { get; } = InitializeWebView2UserDataDirectory();
// ⚠️ 延迟初始化以避免循环依赖
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
public static bool IsProcessElevated => LazyIsProcessElevated.Value;
// Requires main thread // Requires main thread
public static bool IsAppNotificationEnabled { get; } = AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled; public static bool IsAppNotificationEnabled { get; } = CheckAppNotificationEnabled();
public static string? GetDisplayName() public static string? GetDisplayName()
{ {
@@ -106,32 +110,64 @@ internal static class HutaoRuntime
return string.Intern(directory); return string.Intern(directory);
} }
private static string InitializeDataDirectory() private static bool GetIsProcessElevated()
{ {
// Delete the previous data folder if it exists // ⚠️ 这里调用 LocalSetting 时,确保 DataDirectory 已经初始化完成
try try
{ {
string previousDirectory = LocalSetting.Get(SettingKeys.PreviousDataDirectoryToDelete, string.Empty); return LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
if (!string.IsNullOrEmpty(previousDirectory) && Directory.Exists(previousDirectory))
{
Directory.SetReadOnly(previousDirectory, false);
Directory.Delete(previousDirectory, true);
}
} }
finally catch
{ {
LocalSetting.Set(SettingKeys.PreviousDataDirectoryToDelete, string.Empty); // 如果读取失败,使用默认值
return Environment.IsPrivilegedProcess;
} }
}
// Check if the preferred path is set private static string InitializeLocalCacheDirectory()
string currentDirectory = LocalSetting.Get(SettingKeys.DataDirectory, string.Empty); {
if (PackageIdentityAdapter.HasPackageIdentity)
if (!string.IsNullOrEmpty(currentDirectory))
{ {
Directory.CreateDirectory(currentDirectory); return Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path;
return currentDirectory;
} }
// Unpackaged: use %LOCALAPPDATA%\Snap.Hutao\Cache
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
const string FolderName
#if IS_ALPHA_BUILD
= "HutaoAlpha";
#elif IS_CANARY_BUILD
= "HutaoCanary";
#else
= "Hutao";
#endif
string cacheDir = Path.Combine(localAppData, FolderName, "Cache");
Directory.CreateDirectory(cacheDir);
return cacheDir;
}
private static string InitializeWebView2UserDataDirectory()
{
string directory = Path.Combine(LocalCacheDirectory, "WebView2");
Directory.CreateDirectory(directory);
return directory;
}
private static bool CheckAppNotificationEnabled()
{
try
{
return AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
}
catch
{
// In unpackaged mode, this might fail - return false
return false;
}
}
private static string InitializeDataDirectory()
{
const string FolderName const string FolderName
#if IS_ALPHA_BUILD #if IS_ALPHA_BUILD
= "HutaoAlpha"; = "HutaoAlpha";
@@ -141,30 +177,43 @@ internal static class HutaoRuntime
= "Hutao"; = "Hutao";
#endif #endif
// ⚠️ 不要在这里调用 LocalSetting - 会导致循环依赖
// 先确定默认的数据目录位置
// Check if the old documents path exists // Check if the old documents path exists
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName)); string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
if (Directory.Exists(myDocumentsHutaoDirectory)) if (Directory.Exists(myDocumentsHutaoDirectory))
{ {
LocalSetting.Set(SettingKeys.DataDirectory, myDocumentsHutaoDirectory);
return myDocumentsHutaoDirectory; return myDocumentsHutaoDirectory;
} }
// Prefer LocalApplicationData // Use LocalApplicationData
string localApplicationData = ApplicationData.Current.LocalFolder.Path; string localApplicationData;
string path = Path.GetFullPath(Path.Combine(localApplicationData, FolderName)); if (PackageIdentityAdapter.HasPackageIdentity)
{
localApplicationData = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
}
else
{
// Unpackaged: use %LOCALAPPDATA%
localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
}
string defaultPath = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
// ⚠️ 延迟处理:在第一次使用 LocalSetting 后再检查是否有自定义路径
// 这里返回默认路径,后续通过 LocalSetting 可能会更新
try try
{ {
Directory.CreateDirectory(path); Directory.CreateDirectory(defaultPath);
} }
catch (Exception ex) catch (Exception ex)
{ {
// FileNotFoundException | UnauthorizedAccessException // FileNotFoundException | UnauthorizedAccessException
// We don't have enough permission HutaoException.InvalidOperation($"Failed to create data folder: {defaultPath}", ex);
HutaoException.InvalidOperation($"Failed to create data folder: {path}", ex);
} }
LocalSetting.Set(SettingKeys.DataDirectory, path); return defaultPath;
return path;
} }
private static string InitializeDeviceId() private static string InitializeDeviceId()
@@ -186,4 +235,4 @@ internal static class HutaoRuntime
return new(string.Empty, SH.CoreWebView2HelperVersionUndetected, false); return new(string.Empty, SH.CoreWebView2HelperVersionUndetected, false);
} }
} }
} }

View File

@@ -1,11 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ApplicationModel;
using System.IO; using System.IO;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using Windows.ApplicationModel;
using Windows.Storage;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
@@ -13,7 +12,7 @@ internal static class InstalledLocation
{ {
public static string GetAbsolutePath(string relativePath) public static string GetAbsolutePath(string relativePath)
{ {
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath); return Path.Combine(PackageIdentityAdapter.AppDirectory, relativePath);
} }
public static void CopyFileFromApplicationUri(string url, string path) public static void CopyFileFromApplicationUri(string url, string path)
@@ -23,8 +22,26 @@ internal static class InstalledLocation
static async Task CopyApplicationUriFileCoreAsync(string url, string path) static async Task CopyApplicationUriFileCoreAsync(string url, string path)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(url.ToUri());
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead()) Uri uri = url.ToUri();
Stream outputStream;
if (PackageIdentityAdapter.HasPackageIdentity)
{
// Packaged: use StorageFile
Windows.Storage.StorageFile file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(uri);
outputStream = (await file.OpenReadAsync()).AsStreamForRead();
}
else
{
// Unpackaged: read from file system directly
// Assume ms-appx:/// points to the app directory
string localPath = uri.LocalPath.TrimStart('/');
string fullPath = Path.Combine(PackageIdentityAdapter.AppDirectory, localPath);
outputStream = File.OpenRead(fullPath);
}
using (outputStream)
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {

View File

@@ -74,8 +74,15 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
public void ActivateAndInitialize(HutaoActivationArguments args) public void ActivateAndInitialize(HutaoActivationArguments args)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] ActivateAndInitialize called");
#endif
if (Volatile.Read(ref isActivating) is 1) if (Volatile.Read(ref isActivating) is 1)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Already activating, returning");
#endif
return; return;
} }
@@ -85,21 +92,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
{ {
try try
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Starting activation process");
#endif
using (await activateLock.LockAsync().ConfigureAwait(false)) using (await activateLock.LockAsync().ConfigureAwait(false))
{ {
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0) if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Race condition detected, returning");
#endif
return; return;
} }
#if DEBUG
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleActivationAsync");
#endif
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false); await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
#if DEBUG
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleInitializationAsync");
#endif
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false); await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
#if DEBUG
Debug.WriteLine("[AppActivation] Initialization completed successfully");
#endif
} }
} }
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[AppActivation] Exception during activation: {ex}");
#endif
throw;
}
finally finally
{ {
XamlApplicationLifetime.ActivationAndInitializationCompleted = true; XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
Interlocked.Exchange(ref isActivating, 0); Interlocked.Exchange(ref isActivating, 0);
#if DEBUG
Debug.WriteLine("[AppActivation] ActivationAndInitializationCompleted set to true");
#endif
} }
} }
} }
@@ -313,16 +351,36 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
private async ValueTask<Window?> WaitWindowAsync<TWindow>() private async ValueTask<Window?> WaitWindowAsync<TWindow>()
where TWindow : Window where TWindow : Window
{ {
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Waiting for window type: {typeof(TWindow).Name}");
#endif
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Switched to main thread");
#endif
if (currentXamlWindowReference.Window is not { } window) if (currentXamlWindowReference.Window is not { } window)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Creating new window instance");
#endif
try try
{ {
window = serviceProvider.GetRequiredService<TWindow>(); window = serviceProvider.GetRequiredService<TWindow>();
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Window created successfully: {window.GetType().Name}");
#endif
} }
catch (COMException) catch (COMException ex)
{ {
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] COMException: {ex}");
#endif
if (XamlApplicationLifetime.Exiting) if (XamlApplicationLifetime.Exiting)
{ {
return default; return default;
@@ -330,11 +388,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
throw; throw;
} }
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Exception creating window: {ex}");
#endif
throw;
}
currentXamlWindowReference.Window = window; currentXamlWindowReference.Window = window;
} }
else
{
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Using existing window: {window.GetType().Name}");
#endif
}
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Calling window.SwitchTo()");
#endif
window.SwitchTo(); window.SwitchTo();
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Window activated");
#endif
window.AppWindow?.MoveInZOrderAtTop(); window.AppWindow?.MoveInZOrderAtTop();
return window; return window;
} }

View File

@@ -66,4 +66,14 @@ internal sealed class HutaoActivationArguments
return result; return result;
} }
public static HutaoActivationArguments CreateDefaultLaunchArguments()
{
return new HutaoActivationArguments
{
IsRedirectTo = false,
Kind = HutaoActivationKind.Launch,
LaunchActivatedArguments = string.Empty
};
}
} }

View File

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

View File

@@ -20,9 +20,9 @@ internal static class LoggerFactoryExtension
#if DEBUG || IS_ALPHA_BUILD || IS_CANARY_BUILD #if DEBUG || IS_ALPHA_BUILD || IS_CANARY_BUILD
// Alpha and Canary produces noisy events // Alpha and Canary produces noisy events
options.Dsn = "https://ec3799184191c344ca06c592cb97a464@sentry.snapgenshin.com/4"; options.Dsn = "https://2d3047ff2d451986bc7ef395d1f1fe63@o4507525750521856.ingest.us.sentry.io/4510413123682304";
#else #else
options.Dsn = "https://1a1151ce5ac4e7f1536edf085bd483ec@sentry.snapgenshin.com/2"; options.Dsn = "https://2d3047ff2d451986bc7ef395d1f1fe63@o4507525750521856.ingest.us.sentry.io/4510413123682304";
#endif #endif
#if DEBUG #if DEBUG
@@ -36,8 +36,8 @@ internal static class LoggerFactoryExtension
options.Environment = GetBuildEnvironment(); options.Environment = GetBuildEnvironment();
// Suppress logs to generate events and breadcrumbs // Suppress logs to generate events and breadcrumbs
options.MinimumBreadcrumbLevel = LogLevel.None; options.MinimumBreadcrumbLevel = LogLevel.Information;
options.MinimumEventLevel = LogLevel.None; options.MinimumEventLevel = LogLevel.Error;
options.ProfilesSampleRate = 1.0D; options.ProfilesSampleRate = 1.0D;
options.TracesSampleRate = 1.0D; options.TracesSampleRate = 1.0D;

View File

@@ -1,12 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Process; using Snap.Hutao.Factory.Process;
using Snap.Hutao.Win32; using Snap.Hutao.Win32;
using Snap.Hutao.Win32.Foundation; using Snap.Hutao.Win32.Foundation;
using System.Collections.Concurrent;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Text.Json;
using Windows.Storage; using Windows.Storage;
namespace Snap.Hutao.Core.Setting; namespace Snap.Hutao.Core.Setting;
@@ -36,40 +40,20 @@ internal static class LocalSetting
typeof(ApplicationDataCompositeValue) typeof(ApplicationDataCompositeValue)
]; ];
private static readonly ApplicationDataContainer Container = ApplicationData.Current.LocalSettings; private static readonly Lazy<ISettingStorage> LazyStorage = new(CreateStorage);
private static ISettingStorage Storage => LazyStorage.Value;
public static T Get<T>(string key, T defaultValue = default!) public static T Get<T>(string key, T defaultValue = default!)
{ {
Debug.Assert(SupportedTypes.Contains(typeof(T))); Debug.Assert(SupportedTypes.Contains(typeof(T)));
if (Container.Values.TryGetValue(key, out object? value)) return Storage.Get(key, defaultValue);
{
// unbox the value
return value is null ? defaultValue : (T)value;
}
Set(key, defaultValue);
return defaultValue;
} }
public static void Set<T>(string key, T value) public static void Set<T>(string key, T value)
{ {
Debug.Assert(SupportedTypes.Contains(typeof(T))); Debug.Assert(SupportedTypes.Contains(typeof(T)));
Storage.Set(key, value);
try
{
Container.Values[key] = value;
}
catch (Exception ex)
{
// 状态管理器无法写入设置
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
{
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
ProcessFactory.KillCurrent();
}
throw;
}
} }
public static void SetIf<T>(bool condition, string key, T value) public static void SetIf<T>(bool condition, string key, T value)
@@ -103,4 +87,299 @@ internal static class LocalSetting
Set(key, newValue); Set(key, newValue);
return oldValue; return oldValue;
} }
private static ISettingStorage CreateStorage()
{
if (PackageIdentityAdapter.HasPackageIdentity)
{
return new PackagedSettingStorage();
}
return new UnpackagedSettingStorage();
}
private interface ISettingStorage
{
T Get<T>(string key, T defaultValue);
void Set<T>(string key, T value);
}
private sealed class PackagedSettingStorage : ISettingStorage
{
private readonly ApplicationDataContainer container = ApplicationData.Current.LocalSettings;
public T Get<T>(string key, T defaultValue)
{
if (container.Values.TryGetValue(key, out object? value))
{
// unbox the value
return value is null ? defaultValue : (T)value;
}
Set(key, defaultValue);
return defaultValue;
}
public void Set<T>(string key, T value)
{
try
{
container.Values[key] = value;
}
catch (Exception ex)
{
// 状态管理器无法写入设置
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
{
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
ProcessFactory.KillCurrent();
}
throw;
}
}
}
private sealed class UnpackagedSettingStorage : ISettingStorage
{
private readonly string settingsFilePath;
private readonly ConcurrentDictionary<string, object?> cache = new();
private readonly object fileLock = new();
private readonly JsonSerializerOptions jsonOptions = new()
{
WriteIndented = true,
Converters =
{
new ApplicationDataCompositeValueJsonConverter(),
}
};
public UnpackagedSettingStorage()
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
const string FolderName
#if IS_ALPHA_BUILD
= "HutaoAlpha";
#elif IS_CANARY_BUILD
= "HutaoCanary";
#else
= "Hutao";
#endif
string settingsDir = Path.Combine(localAppData, FolderName, "Settings");
Directory.CreateDirectory(settingsDir);
settingsFilePath = Path.Combine(settingsDir, "LocalSettings.json");
LoadFromFile();
}
public T Get<T>(string key, T defaultValue)
{
if (cache.TryGetValue(key, out object? value))
{
if (value is null)
{
return defaultValue;
}
// Handle JSON deserialization for complex types
if (value is JsonElement jsonElement)
{
try
{
// ⚠️ 特殊处理JSON 数字类型转换
Type targetType = typeof(T);
if (jsonElement.ValueKind == JsonValueKind.Number)
{
if (targetType == typeof(int))
{
return (T)(object)jsonElement.GetInt32();
}
if (targetType == typeof(long))
{
return (T)(object)jsonElement.GetInt64();
}
if (targetType == typeof(short))
{
return (T)(object)jsonElement.GetInt16();
}
if (targetType == typeof(byte))
{
return (T)(object)jsonElement.GetByte();
}
if (targetType == typeof(uint))
{
return (T)(object)jsonElement.GetUInt32();
}
if (targetType == typeof(ulong))
{
return (T)(object)jsonElement.GetUInt64();
}
if (targetType == typeof(ushort))
{
return (T)(object)jsonElement.GetUInt16();
}
if (targetType == typeof(float))
{
return (T)(object)jsonElement.GetSingle();
}
if (targetType == typeof(double))
{
return (T)(object)jsonElement.GetDouble();
}
}
// 其他类型使用标准反序列化
return jsonElement.Deserialize<T>(jsonOptions) ?? defaultValue;
}
catch
{
return defaultValue;
}
}
// ⚠️ 如果是直接从 cache 读取的值,也可能需要类型转换
// 例如double -> int
if (value is double doubleValue)
{
Type targetType = typeof(T);
if (targetType == typeof(int))
{
return (T)(object)(int)doubleValue;
}
if (targetType == typeof(long))
{
return (T)(object)(long)doubleValue;
}
if (targetType == typeof(short))
{
return (T)(object)(short)doubleValue;
}
if (targetType == typeof(byte))
{
return (T)(object)(byte)doubleValue;
}
}
return (T)value;
}
Set(key, defaultValue);
return defaultValue;
}
public void Set<T>(string key, T value)
{
cache[key] = value;
SaveToFile();
}
private void LoadFromFile()
{
lock (fileLock)
{
if (!File.Exists(settingsFilePath))
{
return;
}
try
{
string json = File.ReadAllText(settingsFilePath);
Dictionary<string, JsonElement>? data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json, jsonOptions);
if (data is not null)
{
foreach ((string key, JsonElement value) in data)
{
cache[key] = value;
}
}
}
catch
{
// If file is corrupted, start fresh
}
}
}
private void SaveToFile()
{
lock (fileLock)
{
try
{
// Convert cache to serializable dictionary
Dictionary<string, object?> serializableData = new(cache);
string json = JsonSerializer.Serialize(serializableData, jsonOptions);
File.WriteAllText(settingsFilePath, json);
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to save settings: {ex.Message}");
}
}
}
}
// Converter for ApplicationDataCompositeValue
private sealed class ApplicationDataCompositeValueJsonConverter : System.Text.Json.Serialization.JsonConverter<ApplicationDataCompositeValue>
{
public override ApplicationDataCompositeValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
return null;
}
ApplicationDataCompositeValue composite = new();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return composite;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
continue;
}
string? key = reader.GetString();
reader.Read();
if (key is not null)
{
composite[key] = reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.TryGetInt64(out long l) ? l : reader.GetDouble(),
JsonTokenType.True => true,
JsonTokenType.False => false,
_ => null
};
}
}
return composite;
}
public override void Write(Utf8JsonWriter writer, ApplicationDataCompositeValue value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach ((string key, object? val) in value)
{
writer.WritePropertyName(key);
if (val is null)
{
writer.WriteNullValue();
}
else
{
JsonSerializer.Serialize(writer, val, val.GetType(), options);
}
}
writer.WriteEndObject();
}
}
} }

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

@@ -147,6 +147,27 @@ internal sealed class ProcessFactory
{ {
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory(); string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe"); string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
// Check if FullTrust executable exists - if not, fallback to normal admin mode
if (!File.Exists(fullTrustFilePath))
{
string errorMessage = $"""
Island 功能需要的 FullTrust 进程文件不存在,将使用普通管理员模式启动游戏。
预期路径:{fullTrustFilePath}
原因ContentDelivery 仓库尚未下载或初始化失败(常见于非打包模式首次运行)
Island 功能将不可用,但游戏可以正常启动。
等待仓库下载完成后可重新尝试使用 Island 功能。
""";
// Capture as breadcrumb instead of exception
SentrySdk.AddBreadcrumb(errorMessage, category: "process.fulltrust", level: Sentry.BreadcrumbLevel.Warning);
// Fallback to normal admin mode - Island features will not work but game can launch
return CreateUsingShellExecuteRunAs(arguments, fileName, workingDirectory);
}
StartUsingShellExecuteRunAs(fullTrustFilePath); StartUsingShellExecuteRunAs(fullTrustFilePath);
FullTrustProcessStartInfoRequest request = new() FullTrustProcessStartInfoRequest request = new()
@@ -172,11 +193,39 @@ internal sealed class ProcessFactory
public static void StartUsingShellExecuteRunAs(string fileName) public static void StartUsingShellExecuteRunAs(string fileName)
{ {
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo // 尝试从app包中启动
try
{ {
FileName = fileName, global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
UseShellExecute = true, {
Verb = "runas", FileName = fileName,
}); UseShellExecute = true,
Verb = "runas",
});
}catch
{
// 如果失败且filename含有Snap.Hutao.Unpackaged就直接用Snap.Hutao.exe重启
if (fileName.Contains("Snap.Hutao.Unpackaged"))
{
string currentDirectory = Directory.GetCurrentDirectory();
string unpackagedPath = Path.Combine(currentDirectory, "Snap.Hutao.exe");
if (File.Exists(unpackagedPath))
{
fileName = unpackagedPath;
}
// 否则抛出异常
else
{
throw;
}
// 重新尝试启动
global::System.Diagnostics.Process.Start(new global::System.Diagnostics.ProcessStartInfo
{
FileName = fileName,
UseShellExecute = true,
Verb = "runas",
});
}
}
} }
} }

View File

@@ -1,268 +1,273 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Collections.Immutable; using System.Collections.Immutable;
namespace Snap.Hutao.Model.Metadata.Avatar; namespace Snap.Hutao.Model.Metadata.Avatar;
internal static class AvatarIds internal static class AvatarIds
{ {
// 此处的变量名称以 UI_AvatarIcon 为准 // 此处的变量名称以 UI_AvatarIcon 为准
public static readonly AvatarId Ayaka = 10000002; public static readonly AvatarId Ayaka = 10000002;
public static readonly AvatarId Qin = 10000003; public static readonly AvatarId Qin = 10000003;
public static readonly AvatarId PlayerBoy = 10000005; public static readonly AvatarId PlayerBoy = 10000005;
public static readonly AvatarId Lisa = 10000006; public static readonly AvatarId Lisa = 10000006;
public static readonly AvatarId PlayerGirl = 10000007; public static readonly AvatarId PlayerGirl = 10000007;
public static readonly AvatarId Barbara = 10000014; public static readonly AvatarId Barbara = 10000014;
public static readonly AvatarId Kaeya = 10000015; public static readonly AvatarId Kaeya = 10000015;
public static readonly AvatarId Diluc = 10000016; public static readonly AvatarId Diluc = 10000016;
public static readonly AvatarId Razor = 10000020; public static readonly AvatarId Razor = 10000020;
public static readonly AvatarId Ambor = 10000021; public static readonly AvatarId Ambor = 10000021;
public static readonly AvatarId Venti = 10000022; public static readonly AvatarId Venti = 10000022;
public static readonly AvatarId Xiangling = 10000023; public static readonly AvatarId Xiangling = 10000023;
public static readonly AvatarId Beidou = 10000024; public static readonly AvatarId Beidou = 10000024;
public static readonly AvatarId Xingqiu = 10000025; public static readonly AvatarId Xingqiu = 10000025;
public static readonly AvatarId Xiao = 10000026; public static readonly AvatarId Xiao = 10000026;
public static readonly AvatarId Ningguang = 10000027; public static readonly AvatarId Ningguang = 10000027;
public static readonly AvatarId Klee = 10000029; public static readonly AvatarId Klee = 10000029;
public static readonly AvatarId Zhongli = 10000030; public static readonly AvatarId Zhongli = 10000030;
public static readonly AvatarId Fischl = 10000031; public static readonly AvatarId Fischl = 10000031;
public static readonly AvatarId Bennett = 10000032; public static readonly AvatarId Bennett = 10000032;
public static readonly AvatarId Tartaglia = 10000033; public static readonly AvatarId Tartaglia = 10000033;
public static readonly AvatarId Noel = 10000034; public static readonly AvatarId Noel = 10000034;
public static readonly AvatarId Qiqi = 10000035; public static readonly AvatarId Qiqi = 10000035;
public static readonly AvatarId Chongyun = 10000036; public static readonly AvatarId Chongyun = 10000036;
public static readonly AvatarId Ganyu = 10000037; public static readonly AvatarId Ganyu = 10000037;
public static readonly AvatarId Albedo = 10000038; public static readonly AvatarId Albedo = 10000038;
public static readonly AvatarId Diona = 10000039; public static readonly AvatarId Diona = 10000039;
public static readonly AvatarId Mona = 10000041; public static readonly AvatarId Mona = 10000041;
public static readonly AvatarId Keqing = 10000042; public static readonly AvatarId Keqing = 10000042;
public static readonly AvatarId Sucrose = 10000043; public static readonly AvatarId Sucrose = 10000043;
public static readonly AvatarId Xinyan = 10000044; public static readonly AvatarId Xinyan = 10000044;
public static readonly AvatarId Rosaria = 10000045; public static readonly AvatarId Rosaria = 10000045;
public static readonly AvatarId Hutao = 10000046; public static readonly AvatarId Hutao = 10000046;
public static readonly AvatarId Kazuha = 10000047; public static readonly AvatarId Kazuha = 10000047;
public static readonly AvatarId Feiyan = 10000048; public static readonly AvatarId Feiyan = 10000048;
public static readonly AvatarId Yoimiya = 10000049; public static readonly AvatarId Yoimiya = 10000049;
public static readonly AvatarId Tohma = 10000050; public static readonly AvatarId Tohma = 10000050;
public static readonly AvatarId Eula = 10000051; public static readonly AvatarId Eula = 10000051;
public static readonly AvatarId Shougun = 10000052; public static readonly AvatarId Shougun = 10000052;
public static readonly AvatarId Sayu = 10000053; public static readonly AvatarId Sayu = 10000053;
public static readonly AvatarId Kokomi = 10000054; public static readonly AvatarId Kokomi = 10000054;
public static readonly AvatarId Gorou = 10000055; public static readonly AvatarId Gorou = 10000055;
public static readonly AvatarId Sara = 10000056; public static readonly AvatarId Sara = 10000056;
public static readonly AvatarId Itto = 10000057; public static readonly AvatarId Itto = 10000057;
public static readonly AvatarId Yae = 10000058; public static readonly AvatarId Yae = 10000058;
public static readonly AvatarId Heizo = 10000059; public static readonly AvatarId Heizo = 10000059;
public static readonly AvatarId Yelan = 10000060; public static readonly AvatarId Yelan = 10000060;
public static readonly AvatarId Kirara = 10000061; public static readonly AvatarId Kirara = 10000061;
public static readonly AvatarId Aloy = 10000062; public static readonly AvatarId Aloy = 10000062;
public static readonly AvatarId Shenhe = 10000063; public static readonly AvatarId Shenhe = 10000063;
public static readonly AvatarId Yunjin = 10000064; public static readonly AvatarId Yunjin = 10000064;
public static readonly AvatarId Shinobu = 10000065; public static readonly AvatarId Shinobu = 10000065;
public static readonly AvatarId Ayato = 10000066; public static readonly AvatarId Ayato = 10000066;
public static readonly AvatarId Collei = 10000067; public static readonly AvatarId Collei = 10000067;
public static readonly AvatarId Dori = 10000068; public static readonly AvatarId Dori = 10000068;
public static readonly AvatarId Tighnari = 10000069; public static readonly AvatarId Tighnari = 10000069;
public static readonly AvatarId Nilou = 10000070; public static readonly AvatarId Nilou = 10000070;
public static readonly AvatarId Cyno = 10000071; public static readonly AvatarId Cyno = 10000071;
public static readonly AvatarId Candace = 10000072; public static readonly AvatarId Candace = 10000072;
public static readonly AvatarId Nahida = 10000073; public static readonly AvatarId Nahida = 10000073;
public static readonly AvatarId Layla = 10000074; public static readonly AvatarId Layla = 10000074;
public static readonly AvatarId Wanderer = 10000075; public static readonly AvatarId Wanderer = 10000075;
public static readonly AvatarId Faruzan = 10000076; public static readonly AvatarId Faruzan = 10000076;
public static readonly AvatarId Yaoyao = 10000077; public static readonly AvatarId Yaoyao = 10000077;
public static readonly AvatarId Alhaitham = 10000078; public static readonly AvatarId Alhaitham = 10000078;
public static readonly AvatarId Dehya = 10000079; public static readonly AvatarId Dehya = 10000079;
public static readonly AvatarId Mika = 10000080; public static readonly AvatarId Mika = 10000080;
public static readonly AvatarId Kaveh = 10000081; public static readonly AvatarId Kaveh = 10000081;
public static readonly AvatarId Baizhuer = 10000082; public static readonly AvatarId Baizhuer = 10000082;
public static readonly AvatarId Lynette = 10000083; public static readonly AvatarId Lynette = 10000083;
public static readonly AvatarId Lyney = 10000084; public static readonly AvatarId Lyney = 10000084;
public static readonly AvatarId Freminet = 10000085; public static readonly AvatarId Freminet = 10000085;
public static readonly AvatarId Wriothesley = 10000086; public static readonly AvatarId Wriothesley = 10000086;
public static readonly AvatarId Neuvillette = 10000087; public static readonly AvatarId Neuvillette = 10000087;
public static readonly AvatarId Charlotte = 10000088; public static readonly AvatarId Charlotte = 10000088;
public static readonly AvatarId Furina = 10000089; public static readonly AvatarId Furina = 10000089;
public static readonly AvatarId Chevreuse = 10000090; public static readonly AvatarId Chevreuse = 10000090;
public static readonly AvatarId Navia = 10000091; public static readonly AvatarId Navia = 10000091;
public static readonly AvatarId Gaming = 10000092; public static readonly AvatarId Gaming = 10000092;
public static readonly AvatarId Xianyun = 10000093; public static readonly AvatarId Xianyun = 10000093;
public static readonly AvatarId Chiori = 10000094; public static readonly AvatarId Chiori = 10000094;
public static readonly AvatarId Sigewinne = 10000095; public static readonly AvatarId Sigewinne = 10000095;
public static readonly AvatarId Arlecchino = 10000096; public static readonly AvatarId Arlecchino = 10000096;
public static readonly AvatarId Sethos = 10000097; public static readonly AvatarId Sethos = 10000097;
public static readonly AvatarId Clorinde = 10000098; public static readonly AvatarId Clorinde = 10000098;
public static readonly AvatarId Emilie = 10000099; public static readonly AvatarId Emilie = 10000099;
public static readonly AvatarId Kachina = 10000100; public static readonly AvatarId Kachina = 10000100;
public static readonly AvatarId Kinich = 10000101; public static readonly AvatarId Kinich = 10000101;
public static readonly AvatarId Mualani = 10000102; public static readonly AvatarId Mualani = 10000102;
public static readonly AvatarId Xilonen = 10000103; public static readonly AvatarId Xilonen = 10000103;
public static readonly AvatarId Chasca = 10000104; public static readonly AvatarId Chasca = 10000104;
public static readonly AvatarId Olorun = 10000105; public static readonly AvatarId Olorun = 10000105;
public static readonly AvatarId Mavuika = 10000106; public static readonly AvatarId Mavuika = 10000106;
public static readonly AvatarId Citlali = 10000107; public static readonly AvatarId Citlali = 10000107;
public static readonly AvatarId Lanyan = 10000108; public static readonly AvatarId Lanyan = 10000108;
public static readonly AvatarId Mizuki = 10000109; public static readonly AvatarId Mizuki = 10000109;
public static readonly AvatarId Iansan = 10000110; public static readonly AvatarId Iansan = 10000110;
public static readonly AvatarId Varesa = 10000111; public static readonly AvatarId Varesa = 10000111;
public static readonly AvatarId Escoffier = 10000112; public static readonly AvatarId Escoffier = 10000112;
public static readonly AvatarId Ifa = 10000113; public static readonly AvatarId Ifa = 10000113;
public static readonly AvatarId Skirk = 10000114; public static readonly AvatarId Skirk = 10000114;
public static readonly AvatarId Dahlia = 10000115; public static readonly AvatarId Dahlia = 10000115;
public static readonly AvatarId Ineffa = 10000116; public static readonly AvatarId Ineffa = 10000116;
public static readonly AvatarId MannequinBoy = 10000117; public static readonly AvatarId MannequinBoy = 10000117;
public static readonly AvatarId MannequinGirl = 10000118; public static readonly AvatarId MannequinGirl = 10000118;
public static readonly AvatarId Lauma = 10000119; public static readonly AvatarId Lauma = 10000119;
public static readonly AvatarId Flins = 10000120; public static readonly AvatarId Flins = 10000120;
public static readonly AvatarId Aino = 10000121; public static readonly AvatarId Aino = 10000121;
public static readonly AvatarId Nefer = 10000122; public static readonly AvatarId Nefer = 10000122;
public static readonly AvatarId Durin = 10000123;
private static readonly FrozenSet<AvatarId> StandardWishIds = public static readonly AvatarId Jahoda = 10000124;
[ public static readonly AvatarId Columbina = 10000125;
Qin, public static readonly AvatarId Zibai = 10000126;
Diluc, public static readonly AvatarId Illuga = 10000127;
Mona,
Keqing, private static readonly FrozenSet<AvatarId> StandardWishIds =
Qiqi, [
Tighnari, Qin,
Dehya, Diluc,
Mizuki, Mona,
Lisa, Keqing,
Ambor, Qiqi,
Kaeya, Tighnari,
Aloy, Dehya,
]; Mizuki,
Lisa,
private static readonly FrozenSet<AvatarId> GnosisAvatars = Ambor,
[ Kaeya,
Venti, Aloy,
Zhongli, ];
Shougun,
Nahida, private static readonly FrozenSet<AvatarId> GnosisAvatars =
Mavuika, [
]; Venti,
Zhongli,
private static readonly FrozenSet<AvatarId> MasterAvatars = Shougun,
[ Nahida,
PlayerBoy, Mavuika,
PlayerGirl, ];
MannequinBoy,
MannequinGirl, private static readonly FrozenSet<AvatarId> MasterAvatars =
]; [
PlayerBoy,
public static bool IsStandardWish(AvatarId avatarId) PlayerGirl,
{ MannequinBoy,
return StandardWishIds.Contains(avatarId); MannequinGirl,
} ];
public static bool UsesGnosis(AvatarId avatarId) public static bool IsStandardWish(AvatarId avatarId)
{ {
return GnosisAvatars.Contains(avatarId); return StandardWishIds.Contains(avatarId);
} }
public static bool IsPlayer(AvatarId avatarId) public static bool UsesGnosis(AvatarId avatarId)
{ {
return MasterAvatars.Contains(avatarId); return GnosisAvatars.Contains(avatarId);
} }
public static ImmutableDictionary<AvatarId, Avatar> WithPlayers(ImmutableDictionary<AvatarId, Avatar> idAvatarMap) public static bool IsPlayer(AvatarId avatarId)
{ {
ImmutableDictionary<AvatarId, Avatar>.Builder builder = ImmutableDictionary.CreateBuilder<AvatarId, Avatar>(); return MasterAvatars.Contains(avatarId);
}
builder.AddRange(idAvatarMap);
builder.Add(PlayerBoy, new() public static ImmutableDictionary<AvatarId, Avatar> WithPlayers(ImmutableDictionary<AvatarId, Avatar> idAvatarMap)
{ {
Name = SH.ModelMetadataAvatarPlayerName, ImmutableDictionary<AvatarId, Avatar>.Builder builder = ImmutableDictionary.CreateBuilder<AvatarId, Avatar>();
Icon = "UI_AvatarIcon_PlayerBoy",
SideIcon = "UI_AvatarIcon_Side_PlayerBoy", builder.AddRange(idAvatarMap);
Quality = Intrinsic.QualityType.QUALITY_ORANGE, builder.Add(PlayerBoy, new()
Id = PlayerBoy, {
PromoteId = default, Name = SH.ModelMetadataAvatarPlayerName,
Sort = default, Icon = "UI_AvatarIcon_PlayerBoy",
Body = default, SideIcon = "UI_AvatarIcon_Side_PlayerBoy",
Description = default!, Quality = Intrinsic.QualityType.QUALITY_ORANGE,
BeginTime = default, Id = PlayerBoy,
Weapon = default, PromoteId = default,
BaseValue = default!, Sort = default,
GrowCurves = default!, Body = default,
SkillDepot = default!, Description = default!,
FetterInfo = default!, BeginTime = default,
Costumes = default, Weapon = default,
CultivationItems = default, BaseValue = default!,
NameCard = default!, GrowCurves = default!,
}); SkillDepot = default!,
builder.Add(PlayerGirl, new() FetterInfo = default!,
{ Costumes = default,
Name = SH.ModelMetadataAvatarPlayerName, CultivationItems = default,
Icon = "UI_AvatarIcon_PlayerGirl", NameCard = default!,
SideIcon = "UI_AvatarIcon_Side_PlayerGirl", });
Quality = Intrinsic.QualityType.QUALITY_ORANGE, builder.Add(PlayerGirl, new()
Id = PlayerGirl, {
PromoteId = default, Name = SH.ModelMetadataAvatarPlayerName,
Sort = default, Icon = "UI_AvatarIcon_PlayerGirl",
Body = default, SideIcon = "UI_AvatarIcon_Side_PlayerGirl",
Description = default!, Quality = Intrinsic.QualityType.QUALITY_ORANGE,
BeginTime = default, Id = PlayerGirl,
Weapon = default, PromoteId = default,
BaseValue = default!, Sort = default,
GrowCurves = default!, Body = default,
SkillDepot = default!, Description = default!,
FetterInfo = default!, BeginTime = default,
Costumes = default, Weapon = default,
CultivationItems = default, BaseValue = default!,
NameCard = default!, GrowCurves = default!,
}); SkillDepot = default!,
builder.Add(MannequinBoy, new() FetterInfo = default!,
{ Costumes = default,
Name = "奇偶·男性", CultivationItems = default,
Icon = "UI_AvatarIcon_MannequinBoy", NameCard = default!,
SideIcon = "UI_AvatarIcon_Side_MannequinBoy", });
Quality = Intrinsic.QualityType.QUALITY_ORANGE_SP, builder.Add(MannequinBoy, new()
Id = MannequinBoy, {
PromoteId = default, Name = "奇偶·男性",
Sort = default, Icon = "UI_AvatarIcon_MannequinBoy",
Body = default, SideIcon = "UI_AvatarIcon_Side_MannequinBoy",
Description = default!, Quality = Intrinsic.QualityType.QUALITY_ORANGE_SP,
BeginTime = default, Id = MannequinBoy,
Weapon = default, PromoteId = default,
BaseValue = default!, Sort = default,
GrowCurves = default!, Body = default,
SkillDepot = default!, Description = default!,
FetterInfo = default!, BeginTime = default,
Costumes = default, Weapon = default,
CultivationItems = default, BaseValue = default!,
NameCard = default!, GrowCurves = default!,
}); SkillDepot = default!,
builder.Add(MannequinGirl, new() FetterInfo = default!,
{ Costumes = default,
Name = "奇偶·女性", CultivationItems = default,
Icon = "UI_AvatarIcon_MannequinGirl", NameCard = default!,
SideIcon = "UI_AvatarIcon_Side_MannequinGirl", });
Quality = Intrinsic.QualityType.QUALITY_ORANGE_SP, builder.Add(MannequinGirl, new()
Id = MannequinGirl, {
PromoteId = default, Name = "奇偶·女性",
Sort = default, Icon = "UI_AvatarIcon_MannequinGirl",
Body = default, SideIcon = "UI_AvatarIcon_Side_MannequinGirl",
Description = default!, Quality = Intrinsic.QualityType.QUALITY_ORANGE_SP,
BeginTime = default, Id = MannequinGirl,
Weapon = default, PromoteId = default,
BaseValue = default!, Sort = default,
GrowCurves = default!, Body = default,
SkillDepot = default!, Description = default!,
FetterInfo = default!, BeginTime = default,
Costumes = default, Weapon = default,
CultivationItems = default, BaseValue = default!,
NameCard = default!, GrowCurves = default!,
}); SkillDepot = default!,
FetterInfo = default!,
return builder.ToImmutable(); Costumes = default,
} CultivationItems = default,
NameCard = default!,
});
return builder.ToImmutable();
}
} }

View File

@@ -22,8 +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
]; ];
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds = public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
@@ -33,10 +33,12 @@ internal static class WeaponIds
13502U, 13505U, 13502U, 13505U,
14501U, 14502U, 14501U, 14502U,
15501U, 15502U, 15501U, 15502U,
15515U, 11518U,
14522U, 11519U
]; ];
public static bool IsOrangeStandardWish(in WeaponId weaponId) public static bool IsOrangeStandardWish(in WeaponId weaponId)
{ {
return OrangeStandardWishIds.Contains(weaponId); return OrangeStandardWishIds.Contains(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.4.0" />
<Properties> <Properties>
<DisplayName>Snap Hutao</DisplayName> <DisplayName>Snap Hutao</DisplayName>

View File

@@ -1181,6 +1181,9 @@ Space Available: {2}</value>
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve"> <data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
<value>Waiting for game data</value> <value>Waiting for game data</value>
</data> </data>
<data name="UIViewMainTitleBarBackgroundActivityAction" xml:space="preserve">
<value>Background task</value>
</data>
<data name="UIViewPageAvatarPropertyRecommendedAppendProperties" xml:space="preserve"> <data name="UIViewPageAvatarPropertyRecommendedAppendProperties" xml:space="preserve">
<value>Additional Property Recommendation</value> <value>Additional Property Recommendation</value>
</data> </data>
@@ -3989,4 +3992,7 @@ Space Available: {2}</value>
<data name="WindowIdentifyMonitorHeader" xml:space="preserve"> <data name="WindowIdentifyMonitorHeader" xml:space="preserve">
<value>Monitor ID</value> <value>Monitor ID</value>
</data> </data>
<data name="UIViewMainTitleBarInvertTheme" xml:space="preserve">
<value>Invert Theme</value>
</data>
</root> </root>

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>
@@ -4020,4 +4035,7 @@
<data name="WindowIdentifyMonitorHeader" xml:space="preserve"> <data name="WindowIdentifyMonitorHeader" xml:space="preserve">
<value>显示器编号</value> <value>显示器编号</value>
</data> </data>
<data name="UIViewMainTitleBarInvertTheme" xml:space="preserve">
<value>主题切换</value>
</data>
</root> </root>

View File

@@ -24,14 +24,23 @@ 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.SideIcon);
ArgumentNullException.ThrowIfNull(costume.FrontIcon); // Set to costume icon
ArgumentNullException.ThrowIfNull(costume.SideIcon); builder.View.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
// Set to costume icon }
builder.View.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon); else
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon); {
// 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 else
{ {

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,550 @@
// 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 到可写的应用数据目录
await PrepareUnlockerToDataDirectoryAsync().ConfigureAwait(false);
// 从应用数据目录获取 unlocker.exe 路径
unlockerPath = Path.Combine(HutaoRuntime.DataDirectory, UnlockerExecutableName);
if (!File.Exists(unlockerPath))
{
throw HutaoException.InvalidOperation("未找到unlockfps.exe文件请将genshin-fps-unlock-master编译后的unlockfps.exe放置在Snap.Hutao同目录下");
}
// 添加到 Windows Defender 排除项(需要管理员权限)
await AddToDefenderExclusionAsync(unlockerPath).ConfigureAwait(false);
// 获取游戏路径
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 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)
{
if (string.IsNullOrEmpty(gamePath))
{
throw HutaoException.NotSupported("游戏路径未初始化");
}
// 在应用数据目录中创建配置文件
string unlockerConfigPath = Path.Combine(HutaoRuntime.DataDirectory, 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(HutaoRuntime.DataDirectory, 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;
}
// 获取米游社登录Ticket
string? authTicket = default;
bool useAuthTicket = launchOptions.UsingHoyolabAccount.Value
&& context.TryGetOption(LaunchExecutionOptionsKey.LoginAuthTicket, out authTicket)
&& !string.IsNullOrEmpty(authTicket);
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}");
}
// 添加米游社登录参数
if (useAuthTicket)
{
arguments.Append($" login_auth_ticket={authTicket}");
}
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(HutaoRuntime.DataDirectory, 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

@@ -77,21 +77,48 @@ internal sealed class GameIslandInterop : IGameIslandInterop
{ {
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle(); nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea); InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
if (!resume) if (!resume)
{ {
if (context.Process is not FullTrustProcess fullTrustProcess)
{
throw HutaoException.InvalidOperation("Process is not full trust");
}
ArgumentException.ThrowIfNullOrEmpty(islandPath); ArgumentException.ThrowIfNullOrEmpty(islandPath);
if (!File.Exists(islandPath)) if (!File.Exists(islandPath))
{ {
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists); throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
} }
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath)); // Support both FullTrust and normal admin mode
fullTrustProcess.ResumeMainThread(); if (context.Process is FullTrustProcess fullTrustProcess)
{
// Use FullTrust process for injection (suspended process)
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
fullTrustProcess.ResumeMainThread();
}
else
{
// Use native injection for normal admin mode
// The process was already started by CreateUsingShellExecuteRunAs
// Just inject the DLL into the running process
try
{
// Wait a bit for process to initialize
// await Task.Delay(500, token).ConfigureAwait(false);
// ⚠️此处需要更多调查
// Inject using RemoteThread
DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id);
}
catch (Exception ex)
{
// Log the injection failure but don't crash - game can still run
SentrySdk.AddBreadcrumb(
$"Island DLL injection failed: {ex.Message}",
category: "island.injection",
level: Sentry.BreadcrumbLevel.Error);
// Re-throw to let the caller handle it
throw HutaoException.Throw($"Island DLL 注入失败: {ex.Message}", ex);
}
}
} }
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false); await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
@@ -214,4 +241,4 @@ internal sealed class GameIslandInterop : IGameIslandInterop
} }
} }
} }
} }

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

@@ -45,6 +45,7 @@ internal sealed class GameProcessFactory
string gameFilePath = context.FileSystem.GameFilePath; string gameFilePath = context.FileSystem.GameFilePath;
string gameDirectory = context.FileSystem.GameDirectory; string gameDirectory = context.FileSystem.GameDirectory;
// ProcessFactory.CreateUsingFullTrustSuspended will automatically fallback to normal mode if FullTrust.exe is missing
return launchOptions.IsIslandEnabled.Value return launchOptions.IsIslandEnabled.Value
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory) ? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory); : ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);

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

@@ -28,7 +28,20 @@ internal sealed class LaunchExecutionGameProcessStartHandler : AbstractLaunchExe
{ {
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;
} }

View File

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

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Progress; using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Factory.Process;
using Snap.Hutao.Service.Game.FileSystem; using Snap.Hutao.Service.Game.FileSystem;
using Snap.Hutao.Service.Game.Launching.Context; using Snap.Hutao.Service.Game.Launching.Context;
using Snap.Hutao.Service.Game.Launching.Handler; using Snap.Hutao.Service.Game.Launching.Handler;
@@ -20,6 +21,8 @@ internal abstract class AbstractLaunchExecutionInvoker
private bool invoked; private bool invoked;
protected ImmutableArray<ILaunchExecutionHandler> Handlers { get; init; } protected ImmutableArray<ILaunchExecutionHandler> Handlers { get; init; }
protected virtual bool ShouldWaitForProcessExit { get => true; }
protected virtual bool ShouldSpinWaitGameExitAfterInvoke { get => true; }
public static bool Invoking() public static bool Invoking()
{ {
@@ -39,7 +42,7 @@ internal abstract class AbstractLaunchExecutionInvoker
finally finally
{ {
Invokers.TryRemove(this, out _); Invokers.TryRemove(this, out _);
if (!Invoking()) if (!Invoking() && ShouldSpinWaitGameExitAfterInvoke)
{ {
await GameLifeCycle.SpinWaitGameExitAsync(taskContext).ConfigureAwait(false); await GameLifeCycle.SpinWaitGameExitAsync(taskContext).ConfigureAwait(false);
} }
@@ -100,9 +103,16 @@ internal abstract class AbstractLaunchExecutionInvoker
fileSystemReference.Exchange(beforeContext.FileSystem); fileSystemReference.Exchange(beforeContext.FileSystem);
using (IProcess? process = CreateProcess(beforeContext)) // unlockfps.exe会负责启动游戏
IProcess? process = null;
if (!beforeContext.LaunchOptions.IsIslandEnabled.Value)
{ {
if (process is null) process = CreateProcess(beforeContext);
}
using (process)
{
if (process is null && !beforeContext.LaunchOptions.IsIslandEnabled.Value)
{ {
return; return;
} }
@@ -114,7 +124,7 @@ internal abstract class AbstractLaunchExecutionInvoker
TaskContext = taskContext, TaskContext = taskContext,
Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(), Messenger = context.ServiceProvider.GetRequiredService<IMessenger>(),
LaunchOptions = context.LaunchOptions, LaunchOptions = context.LaunchOptions,
Process = process, Process = process ?? new NullProcess(),
IsOversea = targetScheme.IsOversea, IsOversea = targetScheme.IsOversea,
}; };
@@ -123,7 +133,8 @@ internal abstract class AbstractLaunchExecutionInvoker
await handler.ExecuteAsync(executionContext).ConfigureAwait(false); await handler.ExecuteAsync(executionContext).ConfigureAwait(false);
} }
if (process.IsRunning) // 只有在没有启用Island且进程存在时才等待退出
if (ShouldWaitForProcessExit && process is { IsRunning: true })
{ {
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit)); progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
try try
@@ -139,6 +150,12 @@ internal abstract class AbstractLaunchExecutionInvoker
return; return;
} }
} }
else if (ShouldWaitForProcessExit && beforeContext.LaunchOptions.IsIslandEnabled.Value)
{
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
await taskContext.SwitchToBackgroundAsync();
await Task.Delay(30000).ConfigureAwait(false);
}
} }
progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited)); progress.Report(new(SH.ServiceGameLaunchPhaseProcessExited));
@@ -155,4 +172,4 @@ internal abstract class AbstractLaunchExecutionInvoker
} }
} }
} }
} }

View File

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

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

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

View File

@@ -2,17 +2,17 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using Windows.Storage;
namespace Snap.Hutao.Service.Git; namespace Snap.Hutao.Service.Git;
internal static class RepositoryAffinity internal static class RepositoryAffinity
{ {
private static readonly ApplicationDataContainer RepositoryContainer = ApplicationData.Current.LocalSettings.CreateContainer("RepositoryAffinity", ApplicationDataCreateDisposition.Always); private const string RepositoryAffinityPrefix = "RepositoryAffinity::";
private static readonly Lock SyncRoot = new(); private static readonly Lock SyncRoot = new();
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories) public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
@@ -23,9 +23,11 @@ internal static class RepositoryAffinity
for (int i = 0; i < repositories.Length; i++) for (int i = 0; i < repositories.Length; i++)
{ {
GitRepository repository = repositories[i]; GitRepository repository = repositories[i];
ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant());
counts[i] = container.Values[key] is int c ? c : 0; // 对读取值做下限保护,确保排序使用的是非负失败计数
int raw = LocalSetting.Get(key, 0);
counts[i] = Math.Max(0, raw);
} }
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories)); Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
@@ -42,10 +44,14 @@ internal static class RepositoryAffinity
{ {
lock (SyncRoot) lock (SyncRoot)
{ {
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(name, url);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); int currentCount = LocalSetting.Get(key, 0);
object box = container.Values[key];
container.Values[key] = box is int count ? unchecked(count + 1) : 1; // 防止整数上溢:当已到达 int.MaxValue 时不再自增
if (currentCount < int.MaxValue)
{
LocalSetting.Set(key, currentCount + 1);
}
} }
} }
@@ -58,10 +64,20 @@ internal static class RepositoryAffinity
{ {
lock (SyncRoot) lock (SyncRoot)
{ {
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(name, url);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); int currentCount = LocalSetting.Get(key, 0);
object box = container.Values[key];
container.Values[key] = box is int count ? unchecked(count - 1) : 0; // 失败次数不允许小于 0避免出现负数或整型下溢
if (currentCount > 0)
{
LocalSetting.Set(key, currentCount - 1);
}
} }
} }
}
private static string GetSettingKey(string name, string url)
{
string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
}
}

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

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

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

View File

@@ -0,0 +1,6 @@
namespace Snap.Hutao.Service.Yae.Metadata;
internal interface IYaeMetadataService
{
ValueTask<YaeNativeLibConfig?> GetNativeLibConfigAsync(CancellationToken token = default);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
@@ -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.4.0</Version>
<UseWindowsForms>False</UseWindowsForms> <UseWindowsForms>False</UseWindowsForms>
<ImplicitUsings>False</ImplicitUsings> <ImplicitUsings>False</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -47,12 +50,18 @@
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled> <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile> <PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm> <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<!-- 关闭AppX打包 -->
<AppxPackage>false</AppxPackage>
<!-- 不设置打包类型 -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Package.appxmanifest" />
<None Remove="Package.development.appxmanifest" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Manifest Include="$(ApplicationManifest)" /> <Manifest Include="$(ApplicationManifest)" />
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
</ItemGroup> </ItemGroup>
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build"> <Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
@@ -62,6 +71,23 @@
<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>
<!-- 声明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" />
@@ -241,15 +267,15 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" /> <PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" /> <PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
<PackageReference Include="Google.Protobuf" Version="3.33.0" /> <PackageReference Include="Google.Protobuf" Version="3.33.1" />
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" /> <PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
<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="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -265,7 +291,7 @@
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" /> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="QRCoder" Version="1.7.0" /> <PackageReference Include="QRCoder" Version="1.7.0" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" /> <PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
@@ -274,10 +300,6 @@
<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.Deployment.Runtime" Version="2.5.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0"> <PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -286,7 +308,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

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

View File

@@ -1,45 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- DocumentLink --> <!-- DocumentLink -->
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String> <x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String> <x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String> <x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String> <x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/advanced/loopback.html</x:String> <x:String x:Key="DocumentLink_Loopback">https://hut.ao/advanced/loopback.html</x:String>
<x:String x:Key="DocumentLink_Sponsor">https://hut.ao/support-us.html</x:String> <x:String x:Key="DocumentLink_Sponsor">https://hut.ao/support-us.html</x:String>
<!-- Other --> <!-- Other -->
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String> <x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
<x:String x:Key="Sponsor_Afadian">https://hut.ao/zh/support-us.html</x:String> <x:String x:Key="Sponsor_Afadian">https://hut.ao/zh/support-us.html</x:String>
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String> <x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
<!-- AvatarCard --> <!-- AvatarCard -->
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String> <x:String x:Key="UI_AvatarIcon_Costume_Card">https://htserver.wdg.cloudns.ch/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<!-- Bg --> <!-- Bg -->
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String> <x:String x:Key="UI_ItemIcon_None">https://htserver.wdg.cloudns.ch/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<!-- Mark --> <!-- Mark -->
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String> <x:String x:Key="UI_MarkQuest_Events_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String> <x:String x:Key="UI_MarkQuest_Events_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String> <x:String x:Key="UI_MarkQuest_Main_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String> <x:String x:Key="UI_MarkQuest_Main_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String> <x:String x:Key="UI_MarkTower">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<!-- ItemIcon --> <x:String x:Key="UI_ItemIcon_106">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
<x:String x:Key="UI_ItemIcon_106">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String> <x:String x:Key="UI_ItemIcon_204">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String> <x:String x:Key="UI_ItemIcon_210">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String> <x:String x:Key="UI_ItemIcon_220021">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<!-- EmotionIcon -->
<!-- EmotionIcon --> <x:String x:Key="UI_EmotionIcon52">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String> <x:String x:Key="UI_EmotionIcon71">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String> <x:String x:Key="UI_EmotionIcon89">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String> <x:String x:Key="UI_EmotionIcon250">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String> <x:String x:Key="UI_EmotionIcon271">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String> <x:String x:Key="UI_EmotionIcon272">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String> <x:String x:Key="UI_EmotionIcon293">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String> <x:String x:Key="UI_EmotionIcon433">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
<x:String x:Key="UI_EmotionIcon433">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String> <x:String x:Key="UI_EmotionIcon445">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String> <x:String x:Key="UI_EmotionIcon585">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
<x:String x:Key="UI_EmotionIcon585">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String> </ResourceDictionary>
</ResourceDictionary>

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

@@ -79,9 +79,8 @@
<Button <Button
Padding="6" Padding="6"
Command="{Binding InvertAppThemeCommand}" Command="{Binding InvertAppThemeCommand}"
Content="[Dev] Invert Theme" Content="{shuxm:ResourceString Name=UIViewMainTitleBarInvertTheme}"
Style="{ThemeResource SettingButtonStyle}" Style="{ThemeResource SettingButtonStyle}"/>
Visibility="{Binding IsDebug}"/>
<Button <Button
Padding="0" Padding="0"
@@ -265,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>

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"
@@ -553,18 +555,54 @@
</ScrollViewer> </ScrollViewer>
</PivotItem> </PivotItem>
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}" IsEnabled="False"> <PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}">
<Border <Border
Margin="16" Margin="16"
Padding="0" Padding="0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"> cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource AcrylicGridCardStyle}"> <StackPanel Spacing="8">
<Grid.RowDefinitions> <!-- 第三方注入工具 -->
<RowDefinition Height="auto"/> <Grid Style="{ThemeResource AcrylicGridCardStyle}" Visibility="{Binding ThirdPartyTools.Value, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
<RowDefinition Height="auto"/> <Grid Padding="16,12" ColumnSpacing="12">
<RowDefinition Height="auto"/> <Grid.ColumnDefinitions>
<RowDefinition/> <ColumnDefinition Width="auto"/>
</Grid.RowDefinitions> <ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewPageLaunchGameThirdPartyTools}"/>
<ItemsControl
Grid.Column="1"
HorizontalAlignment="Left"
ItemsSource="{Binding ThirdPartyTools.Value, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="shwt:ToolInfo">
<Button
Padding="12,6"
Command="{Binding DataContext.ShowThirdPartyToolDialogCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Content="{Binding Name}"
Style="{ThemeResource AccentButtonStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid <Grid
Grid.Row="0" Grid.Row="0"
Padding="16,8" Padding="16,8"
@@ -641,30 +679,31 @@
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHotSwitchHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHotSwitchHeader}"
IsOn="{Binding LaunchOptions.IsSetFieldOfViewEnabled.Value, Mode=TwoWay}" IsEnabled="False"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/> IsOn="False"
ToolTipService.ToolTip=""/>
<NumberBox <NumberBox
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Padding="10,8,6,6" Padding="10,8,6,6"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}"
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled.Value}" IsEnabled="False"
LargeChange="10" LargeChange="10"
Maximum="100" Maximum="100"
Minimum="0" Minimum="0"
SmallChange="1" SmallChange="1"
SpinButtonPlacementMode="Inline" SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovDescription}" ToolTipService.ToolTip=""
Value="{Binding LaunchOptions.TargetFov.Value, Mode=TwoWay}"/> Value="45"/>
<ToggleSwitch <ToggleSwitch
Grid.Row="0" Grid.Row="0"
Grid.Column="2" Grid.Column="2"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}"
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled}" IsEnabled="False"
IsOn="{Binding LaunchOptions.DisableFog.Value, Mode=TwoWay}" IsOn="False"
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}" OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}" OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogDescription}"/> ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
x:Name="TargetFpsToggleSwitch" x:Name="TargetFpsToggleSwitch"
@@ -703,24 +742,30 @@
Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}" Subtitle="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsTeachingTipSubtitle}"
Target="{x:Bind TargetFpsToggleSwitch}"/> Target="{x:Bind TargetFpsToggleSwitch}"/>
<NumberBox <Grid Grid.Row="1" Grid.Column="1">
Grid.Row="1" <Grid.RowDefinitions>
Grid.Column="1" <RowDefinition Height="Auto"/>
Padding="10,8,6,6" <RowDefinition Height="Auto"/>
Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}" </Grid.RowDefinitions>
IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}" <NumberBox
Maximum="120" Grid.Row="0"
Minimum="1" Padding="10,8,6,6"
SpinButtonPlacementMode="Inline" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFpsHeader}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}" IsEnabled="{Binding LaunchOptions.IsSetTargetFrameRateEnabled.Value}"
Value="{Binding LaunchOptions.TargetFps.Value, Mode=TwoWay}"/> Maximum="120"
Minimum="1"
SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
Value="{Binding LaunchOptions.TargetFps.Value, Mode=TwoWay}"/>
</Grid>
<ToggleSwitch <ToggleSwitch
x:Name="FixLowFovSceneToggleSwitch" x:Name="FixLowFovSceneToggleSwitch"
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
IsEnabled="{Binding LaunchOptions.IsSetFieldOfViewEnabled}" IsEnabled="False"
IsOn="{Binding LaunchOptions.FixLowFovScene.Value, Mode=TwoWay}" IsOn="False"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameFixLowFovSceneDescription}"> ToolTipService.ToolTip="">
<mxi:Interaction.Behaviors> <mxi:Interaction.Behaviors>
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/> <shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
@@ -756,10 +801,11 @@
x:Name="HideQuestBannerToggleSwitch" x:Name="HideQuestBannerToggleSwitch"
Grid.Row="2" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
IsOn="{Binding LaunchOptions.HideQuestBanner.Value, Mode=TwoWay}" IsEnabled="False"
IsOn="False"
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}" OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}" OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"> ToolTipService.ToolTip="">
<mxi:Interaction.Behaviors> <mxi:Interaction.Behaviors>
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/> <shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
@@ -798,34 +844,38 @@
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressHeader}"
IsOn="{Binding LaunchOptions.RemoveOpenTeamProgress.Value, Mode=TwoWay}" IsEnabled="False"
IsOn="False"
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}" OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}" OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameRemoveOpenTeamProgressDescription}"/> ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="2" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameEventCameraMoveHotSwitchHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameEventCameraMoveHotSwitchHeader}"
IsOn="{Binding LaunchOptions.DisableEventCameraMove.Value, Mode=TwoWay}" IsEnabled="False"
IsOn="False"
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}" OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}" OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/> ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Header="{shuxm:ResourceString Name=ViewOverlayDisableShowDamageTextToolTip}" Header="{shuxm:ResourceString Name=ViewOverlayDisableShowDamageTextToolTip}"
IsOn="{Binding LaunchOptions.DisableShowDamageText.Value, Mode=TwoWay}" IsEnabled="False"
IsOn="False"
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}" OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}" OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameHotSwitchDescription}"/> ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
x:Name="RedirectCombineEntryToggleSwitch" x:Name="RedirectCombineEntryToggleSwitch"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
IsOn="{Binding LaunchOptions.RedirectCombineEntry.Value, Mode=TwoWay}" IsEnabled="False"
IsOn="False"
Style="{ThemeResource DefaultToggleSwitchStyle}" Style="{ThemeResource DefaultToggleSwitchStyle}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageLaunchGameIslandRedirectCombineEntryDescription}"> ToolTipService.ToolTip="">
<mxi:Interaction.Behaviors> <mxi:Interaction.Behaviors>
<shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/> <shuxb:ToggleSwitchHeaderHitTestVisibleBehavior/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
@@ -867,39 +917,51 @@
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandUsingTouchScreenHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandUsingTouchScreenHeader}"
IsEnabled="{Binding LaunchOptions.IsGameRunning.Value, Converter={StaticResource BoolNegationConverter}}" IsEnabled="False"
IsOn="{Binding LaunchOptions.UsingTouchScreen.Value, Mode=TwoWay}"/> IsOn="False"
ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="4" Grid.Row="4"
Grid.Column="0" Grid.Column="0"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowOriginalResinHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowOriginalResinHeader}"
IsOn="{Binding LaunchOptions.ResinListItemId000106Allowed.Value, Mode=TwoWay}"/> IsEnabled="False"
IsOn="False"
ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="4" Grid.Row="4"
Grid.Column="1" Grid.Column="1"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowPrimogemHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowPrimogemHeader}"
IsOn="{Binding LaunchOptions.ResinListItemId000201Allowed.Value, Mode=TwoWay}"/> IsEnabled="False"
IsOn="False"
ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="4" Grid.Row="4"
Grid.Column="2" Grid.Column="2"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowFragileResinHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowFragileResinHeader}"
IsOn="{Binding LaunchOptions.ResinListItemId107009Allowed.Value, Mode=TwoWay}"/> IsEnabled="False"
IsOn="False"
ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="5" Grid.Row="5"
Grid.Column="0" Grid.Column="0"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowTransientResinHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowTransientResinHeader}"
IsOn="{Binding LaunchOptions.ResinListItemId107012Allowed.Value, Mode=TwoWay}"/> IsEnabled="False"
IsOn="False"
ToolTipService.ToolTip=""/>
<ToggleSwitch <ToggleSwitch
Grid.Row="5" Grid.Row="5"
Grid.Column="1" Grid.Column="1"
Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowCondensedResinHeader}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandResinListItemAllowCondensedResinHeader}"
IsOn="{Binding LaunchOptions.ResinListItemId220007Allowed.Value, Mode=TwoWay}"/> IsEnabled="False"
IsOn="False"
ToolTipService.ToolTip=""/>
</Grid> </Grid>
</ContentControl> </ContentControl>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</StackPanel>
</Border> </Border>
</PivotItem> </PivotItem>
</Pivot> </Pivot>

View File

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

View File

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

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

@@ -105,7 +105,7 @@ internal static class StaticResource
foreach ((string key, object value) in map) foreach ((string key, object value) in map)
{ {
if ((int)value < (int)LatestResourceVersionMap[key]) if (Convert.ToInt32(value) < Convert.ToInt32(LatestResourceVersionMap[key]))
{ {
return true; return true;
} }
@@ -120,7 +120,7 @@ internal static class StaticResource
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap); ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
foreach ((string key, object value) in LatestResourceVersionMap) foreach ((string key, object value) in LatestResourceVersionMap)
{ {
if (!map.TryGetValue(key, out object current) || (int)value > (int)current) if (!map.TryGetValue(key, out object current) || Convert.ToInt32(value) > Convert.ToInt32(current))
{ {
result.Add(key); result.Add(key);
} }

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker; using Snap.Hutao.Factory.Picker;
@@ -71,8 +72,18 @@ internal sealed class SettingStorageSetDataFolderOperation
try try
{ {
Directory.SetReadOnly(oldFolderPath, false); Directory.SetReadOnly(oldFolderPath, false);
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false); if (PackageIdentityAdapter.HasPackageIdentity)
{
// Packaged: use StorageFolder API
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
}
else
{
// Unpackaged: use standard file I/O
await CopyDirectoryAsync(oldFolderPath, newFolderPath).ConfigureAwait(false);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -84,4 +95,22 @@ internal sealed class SettingStorageSetDataFolderOperation
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath); LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
return true; return true;
} }
private static async ValueTask CopyDirectoryAsync(string sourceDir, string destDir)
{
await Task.Run(() =>
{
// Create all directories
foreach (string dirPath in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dirPath.Replace(sourceDir, destDir));
}
// Copy all files
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
{
File.Copy(filePath, filePath.Replace(sourceDir, destDir), true);
}
}).ConfigureAwait(false);
}
} }

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
@@ -135,7 +136,18 @@ internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
// TODO: prompt user that restart will be non-elevated // TODO: prompt user that restart will be non-elevated
try try
{ {
AppInstance.Restart(string.Empty); if (PackageIdentityAdapter.HasPackageIdentity)
{
AppInstance.Restart(string.Empty);
}
else
{
// Unpackaged: manually restart the process
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
?? throw new InvalidOperationException("Cannot get process path");
System.Diagnostics.Process.Start(exePath);
Snap.Hutao.Factory.Process.ProcessFactory.KillCurrent();
}
} }
catch (COMException ex) catch (COMException ex)
{ {

View File

@@ -6,9 +6,9 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
[Service(ServiceLifetime.Singleton, typeof(IHutaoEndpoints), Key = HutaoEndpointsKind.Release)] [Service(ServiceLifetime.Singleton, typeof(IHutaoEndpoints), Key = HutaoEndpointsKind.Release)]
internal sealed class HutaoEndpointsForRelease : IHutaoEndpoints internal sealed class HutaoEndpointsForRelease : IHutaoEndpoints
{ {
string IHomaRootAccess.Root { get => "http://server.wdg.cloudns.ch:5222"; } string IHomaRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
string IInfrastructureRootAccess.Root { get => "http://server.wdg.cloudns.ch:5222"; } string IInfrastructureRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
string IInfrastructureRawRootAccess.RawRoot { get => "http://server.wdg.cloudns.ch:5222"; } string IInfrastructureRawRootAccess.RawRoot { get => "https://htserver.wdg.cloudns.ch/api"; }
} }

View File

@@ -5,7 +5,7 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
internal static class StaticResourcesEndpoints internal static class StaticResourcesEndpoints
{ {
public static string Root { get => "http://server.wdg.cloudns.ch:8007"; } public static string Root { get => "https://htserver.wdg.cloudns.ch"; }
public static Uri UIIconNone { get; } = StaticRaw("Bg", "UI_Icon_None.png").ToUri(); public static Uri UIIconNone { get; } = StaticRaw("Bg", "UI_Icon_None.png").ToUri();

View File

@@ -1,11 +0,0 @@
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
internal static class SaltConstants
{
public const string CNVersion = "2.95.1";
public const string OSVersion = "2.54.0";
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
}

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

@@ -17,13 +17,13 @@ internal sealed partial class HutaoPassportClient
{ {
private const string PublicKey = """ private const string PublicKey = """
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5W2SEyZSlP2zBI1Sn8Gd MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy4i0acb1/rTjwNt4Wsi/
TwbZoXlUGNKyoVrY8SVYu9GMefdGZCrUQNkCG/Np8pWPmSSEFGd5oeug/oIMtCZQ AgRLztRRGhludiYOWskqS6o0wTy6+DkGNZew/8qy93Tmv/mhYMoBhDJACD7Dpzu6
NOn0drlR+pul/XZ1KQhKmj/arWjN1XNok2qXF7uxhqD0JyNT/Fxy6QvzqIpBsM9S 2cdiPl6MW8wuAE+H86Mh7ghWxnUAvdK6Cp3qbk7MaJF2zI/2yYesGYhcf7HZmZmC
7ajm8/BOGlPG1SInDPaqTdTRTT30AuN+IhWEEFwT3Ctv1SmDupHs2Oan5qM7Y3uw RldyQnH9SP30FRhmbSAqGAjVSObwfa3W9islkbYB2SkcXguK+hONZmtqISoiUK1/
b6K1rbnk5YokiV2FzHajGUymmSKXqtG1USZzwPqImpYb4Z0M/StPFWdsKqexBqMM d+ZEpL01MNWI06iUxin+iT3yk68o6reLOk/Yoqjj12pONIwbu7Up4noLhhhmBdR3
mkXckI5O98GdlszEmQ0Ejv5Fx9fR2rXRwM76S4iZTfabYpiMbb4bM42mHMauupj6 7Xy9csCounngKoBw+7tEmmzJzeqYm/zeHp/Jpy/996dIxAiq6jVvNpWaT9Es6s08
9QIDAQAB cwIDAQAB
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
"""; """;

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

68
tools/GetImages.py Normal file
View File

@@ -0,0 +1,68 @@
import hashlib
import os
import shutil
import re
def sha1_name(filename):
"""对输入文件名进行 SHA1 哈希,返回大写 40 位字符串"""
sha = hashlib.sha1(filename.encode('utf-8')).hexdigest().upper()
return sha
def search_and_copy(original_name, search_dir, output_dir):
sha_name = sha1_name(original_name)
print(f"SHA1 计算结果: {sha_name}")
# 查找匹配文件
matched_path = None
for root, dirs, files in os.walk(search_dir):
for file in files:
if file.upper() == sha_name:
matched_path = os.path.join(root, file)
break
if not matched_path:
print("❌ 未找到匹配文件!")
return
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 复制并重命名
# 文件名是url取最后一部分作为原文件名
original_filename = os.path.basename(original_name)
# 创建url中最后一个文件夹作为输出目录
last_folder = original_name.split('/')[-2]
output_subdir = os.path.join(output_dir, last_folder)
os.makedirs(output_subdir, exist_ok=True)
new_path = os.path.join(output_subdir, original_filename)
shutil.copy(matched_path, new_path)
print(f"✔ 已复制并重命名文件: {new_path}")
def extract_urls_from_log(log_file):
"""
从日志中提取原始文件 URL并打印到终端
"""
base_url = "https://api.snapgenshin.com/static/raw"
url_pattern = re.compile(r'GET\s+/static/raw/([^/]+)/([^ ]+)\s+HTTP')
urls = []
with open(log_file, 'r', encoding='utf-8') as f:
for line in f:
match = url_pattern.search(line)
if match:
category, filename = match.groups()
full_url = f"{base_url}/{category}/{filename}"
urls.append(full_url)
return urls
if __name__ == "__main__":
logfile="1.txt" # 日志文件路径
original_file = extract_urls_from_log(logfile)
search_directory = "C:\\Users\\username\\AppData\\Local\\Packages\\60568DGPStudio.SnapHutao_wbnnev551gwxy1\\LocalCache\\ImageCache" # 搜索目录
output_directory = "C:\\Users\\username\\AppData\\Local\\Packages\\60568DGPStudio.SnapHutao_wbnnev551gwxy1\\LocalCache\\ImageCache\\output" # 输出目录
for url in original_file:
search_and_copy(url, search_directory, output_directory)

19
tools/README.md Normal file
View File

@@ -0,0 +1,19 @@
# 对开发有用的工具
对开发有用的工具,不参与编译
## GetImage.py
由于没有备份的图片资源并且缓存目录中的图片名全部经过SHA1处理过该工具用于通过请求日志在旧的资源文件夹中还原图片资源结构
### 用法
**先获取资源服务器上的404日志如下所示**
<img width="1161" height="57" alt="image" src="https://github.com/user-attachments/assets/54270165-f077-4c79-ae87-da16592e77fe" />
复制404日志粘贴到文本中
**然后修改脚本,确保路径正确**
`GetImage.py:62`开始修改,确保路径正确,搜索路径一定要为旧资源文件夹,`username`是你的用户名,`60568DGPStudio.SnapHutao_wbnnev551gwxy1`改为你实际的旧数据文件夹
运行后将自动搜索缺失的文件,然后会在输出路径中生成原始文件,可以直接上传至服务器来补全图片资源