mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
9 Commits
1.10.2
...
UnsafeCore
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c03ce3486 | ||
|
|
83e187ea9e | ||
|
|
d86232f413 | ||
|
|
4e6691ac51 | ||
|
|
84ad39b192 | ||
|
|
ce50fc41e0 | ||
|
|
1d71048f56 | ||
|
|
08cf823156 | ||
|
|
cca65635a6 |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"cake.tool": {
|
||||
"version": "4.0.0",
|
||||
"commands": [
|
||||
"dotnet-cake"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
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
|
||||
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Feature Request [English Form]
|
||||
description: Tell us about your thought
|
||||
title: "[Feat]: Place your title here"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
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
|
||||
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
@@ -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"
|
||||
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)
|
||||
- [JIHu GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
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
|
||||
37
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
37
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
@@ -1,37 +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
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
***
|
||||
|
||||
- [ ] 主分支合并入 release 分支
|
||||
- [ ] 等待 Release 自动发布
|
||||
- [ ] 检查极狐是否同步完成 Release
|
||||
- [ ] 通知用户
|
||||
- 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
|
||||
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
Normal file
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: 圣遗物评分细则建议
|
||||
description: 为圣遗物评分规则提供你的想法
|
||||
title: "[Artifact Rating] 请在这里填写角色名称"
|
||||
labels: area-AvatarInfo
|
||||
assignees: Lightczx
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单
|
||||
|
||||
- type: textarea
|
||||
id: your-suggested-rule
|
||||
attributes:
|
||||
label: 评分细则
|
||||
description: |
|
||||
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
|
||||
你可以点击预览按钮(preview)来查看表格最终会显示出的内容
|
||||
value: |
|
||||
|项目|评分权重(0-100)|
|
||||
|-----|-----|
|
||||
|角色名称| 旅行者 |
|
||||
|生命值| 10 |
|
||||
|攻击力| 10 |
|
||||
|防御力| 10 |
|
||||
|暴击率| 10 |
|
||||
|暴击伤害| 10 |
|
||||
|元素精通| 10 |
|
||||
|充能效率| 10 |
|
||||
|治疗加成| 10 |
|
||||
|元素伤害| 10 |
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: no-duplicated-dropdown
|
||||
attributes:
|
||||
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
|
||||
description: 如果有,你应该在已有的工单内回复以提出你的建议
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: title-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已设置合适的标题
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: all-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已完整填写表格
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,7 +1,7 @@
|
||||
name: BUG Report [English Form]
|
||||
description: Tell us what issue you get
|
||||
title: "[ENG][Bug]: Place your Issue Title Here"
|
||||
labels: ["BUG", "priority:none"]
|
||||
labels: ["BUG"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -18,29 +18,29 @@ body:
|
||||
options:
|
||||
- label: I have read [FAQ page](https://hut.ao/advanced/FAQ.html) and [Exception page](https://hut.ao/advanced/exceptions.html) in Snap Hutao document, and my issue is not answered
|
||||
required: true
|
||||
|
||||
|
||||
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
|
||||
required: true
|
||||
|
||||
- label: My issue is not a [finished issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90), and it's not a duplicated issue
|
||||
- label: My issue is not a [fixed issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D), and it's not a duplicated issue
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: winver
|
||||
attributes:
|
||||
label: Windows Version
|
||||
description: |
|
||||
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
|
||||
placeholder: e.g. 1.4.15.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -48,10 +48,9 @@ body:
|
||||
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.
|
||||
description: |
|
||||
In Snap Hutao's settings page, 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
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -62,31 +61,30 @@ body:
|
||||
description: Please select the most associated category of your issue
|
||||
options:
|
||||
- Installation and Environment
|
||||
- Game Launcher
|
||||
- Wish Export
|
||||
- Achievement
|
||||
- My Character
|
||||
- Game Launcher
|
||||
- Realtime Note
|
||||
- Develop Plan
|
||||
- Spiral Abyss
|
||||
- Wiki
|
||||
- MiHoYo Account Panel
|
||||
- Daily Checkin Reward
|
||||
- Hutao Passport/Hutao Cloud
|
||||
- User Interface
|
||||
- File Cache
|
||||
- Wish Export
|
||||
- Game Record
|
||||
- Hutao Database
|
||||
- User Interface
|
||||
- Snap Hutao Cloud
|
||||
- Snap Hutao Account
|
||||
- Checkin
|
||||
- Wiki
|
||||
- Announcement
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
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.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
|
||||
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**
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -106,3 +104,4 @@ body:
|
||||
options:
|
||||
- label: I believe the description above is detail enough to allow developers to reproduce the issue
|
||||
required: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 问题反馈
|
||||
description: 通过这个议题向开发团队反馈你发现的程序中的问题
|
||||
description: 告诉我们你的问题
|
||||
title: "[Bug]: 在这里填写一个合适的标题"
|
||||
labels: ["BUG", "priority:none"]
|
||||
labels: ["BUG"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -14,33 +14,33 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: |-
|
||||
请确保你已完整执行检查清单,否则你的议题可能会被忽略
|
||||
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
|
||||
options:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
|
||||
|
||||
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||
required: true
|
||||
|
||||
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
|
||||
- label: 我的问题不是[已修复](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D)的问题也不是一个别人已发布的**重复的**问题
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: winver
|
||||
attributes:
|
||||
label: Windows 版本
|
||||
description: |
|
||||
description: |
|
||||
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
|
||||
placeholder: 例:22000.556
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
- type: input
|
||||
id: shver
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用标题,应用程序的反馈中心界面中可以找到
|
||||
placeholder: 例:1.9.9.0
|
||||
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
|
||||
placeholder: 例:1.4.15.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -48,10 +48,9 @@ body:
|
||||
id: deviceid
|
||||
attributes:
|
||||
label: 设备 ID
|
||||
description: |
|
||||
在胡桃工具箱的反馈中心界面,你可以找到并复制你的设备 ID
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -62,31 +61,30 @@ body:
|
||||
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
|
||||
options:
|
||||
- 安装和环境
|
||||
- 游戏启动器
|
||||
- 祈愿记录
|
||||
- 成就管理
|
||||
- 我的角色
|
||||
- 角色信息面板
|
||||
- 游戏启动器
|
||||
- 实时便笺
|
||||
- 养成计算
|
||||
- 深境螺旋/胡桃数据库
|
||||
- Wiki
|
||||
- 米游社账号面板
|
||||
- 每日签到奖励
|
||||
- 胡桃通行证/胡桃云
|
||||
- 用户界面
|
||||
- 文件缓存
|
||||
- 祈愿记录
|
||||
- 玩家查询
|
||||
- 胡桃数据库
|
||||
- 用户界面
|
||||
- 胡桃云
|
||||
- 胡桃帐号
|
||||
- 签到
|
||||
- Wiki
|
||||
- 公告
|
||||
- 其它
|
||||
validations:
|
||||
required: true
|
||||
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.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
|
||||
description: 详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -106,3 +104,4 @@ body:
|
||||
options:
|
||||
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题
|
||||
required: true
|
||||
|
||||
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Feature Request 功能请求
|
||||
description: Tell us about your thought 告诉我们你的想法
|
||||
title: "[Feat]: Place your title here 在这里填写一个合适的标题"
|
||||
labels: ["功能"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
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. 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 网络问题
|
||||
description: 通过这个议题来反馈网络问题
|
||||
description: 当网络问题影响到你的程序使用时
|
||||
title: "[Network]: 在这里填写一个合适的标题"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
@@ -19,10 +19,10 @@ body:
|
||||
description: |
|
||||
停下!
|
||||
**在填写下面的问题之前请先使用我们的网络诊断工具**
|
||||
**这个工具将会生成一份报告并加密压缩,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [极狐 GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -60,6 +60,7 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
@@ -73,12 +74,5 @@ body:
|
||||
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 检查你提交的议题
|
||||
options:
|
||||
- label: 我已经在该议题中上传了包含网络诊断报告的加密压缩包
|
||||
required: true
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -13,8 +13,4 @@ updates:
|
||||
groups:
|
||||
packages:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github/workflows" # GitHub Workflows
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- "*"
|
||||
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@@ -1,15 +0,0 @@
|
||||
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
|
||||
<!--- Before you create this PR, please fill the following form and checklist -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
|
||||
<!-- e.g. fix #999, resolve #999, close #999 -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] The target PR branch is `develop` branch
|
||||
33
.github/workflows/PublishDistribution.yml
vendored
33
.github/workflows/PublishDistribution.yml
vendored
@@ -12,9 +12,40 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download Assets
|
||||
- name: Download Release
|
||||
timeout-minutes: 5
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
repository: "DGP-Studio/Snap.Hutao"
|
||||
latest: true
|
||||
fileName: "*.msix"
|
||||
out-file-path: ./release-download
|
||||
|
||||
# Upload to Drive
|
||||
- name: Upload Drive
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RCCONF: ${{ secrets.RCCONF }}
|
||||
run: |
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
mkdir -p ~/.config/rclone/
|
||||
cat << EOF > ~/.config/rclone/rclone.conf
|
||||
$RCCONF
|
||||
EOF
|
||||
|
||||
rclone copy ./release-download/* dgpODCN:/releases/
|
||||
|
||||
# Purge Patch System Cache
|
||||
- name: Purge Patch
|
||||
env:
|
||||
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
|
||||
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
|
||||
PURGE_URL: ${{ secrets.PURGE_URL }}
|
||||
run: |
|
||||
curl -X PATCH $PURGE_URL
|
||||
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
|
||||
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
|
||||
|
||||
87
.github/workflows/alpha.yml
vendored
87
.github/workflows/alpha.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Snap Hutao Alpha
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- 'feat/*'
|
||||
paths-ignore:
|
||||
- '.gitattributes'
|
||||
- '.github/**'
|
||||
- '.gitignore'
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '.gitattributes'
|
||||
- '.github/**'
|
||||
- '.gitignore'
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
runner:
|
||||
- self-hosted
|
||||
- windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0
|
||||
|
||||
- name: Cake
|
||||
id: cake
|
||||
shell: pwsh
|
||||
run: dotnet tool restore && dotnet cake
|
||||
env:
|
||||
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
||||
|
||||
- name: Sign Msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
|
||||
signtool.exe sign /debug /v /a /fd SHA256 /f temp.pfx /p ${{ secrets.PW }} ${{ github.workspace }}\src\output\Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
|
||||
- name: Upload signed msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
|
||||
- name: Add summary
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$summary = "
|
||||
> [!WARNING]
|
||||
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
|
||||
> [!TIP]
|
||||
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
|
||||
>
|
||||
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
|
||||
"
|
||||
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
16
.github/workflows/close_stale.yml
vendored
16
.github/workflows/close_stale.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
any-of-labels: 'needs-more-info,需要更多信息'
|
||||
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
|
||||
days-before-stale: 7
|
||||
days-before-close: 3
|
||||
close-issue-reason: not_planned
|
||||
20
.github/workflows/issue_similarity.yml
vendored
20
.github/workflows/issue_similarity.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: Issues Similarity Analysis
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
similarity-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: analysis
|
||||
uses: actions-cool/issues-similarity-analysis@v1
|
||||
with:
|
||||
filter-threshold: 0.5
|
||||
comment-title: '### Probable Similar Topics'
|
||||
title-excludes: '[Publish]:,[Bug]:,[Feat]:,[Network]:,[ENG]'
|
||||
comment-body: '${index}. ${similarity} #${number}'
|
||||
show-footer: false
|
||||
show-mentioned: true
|
||||
since-days: 365
|
||||
26
.github/workflows/lock_closed_issues.yml
vendored
26
.github/workflows/lock_closed_issues.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
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-lock-reason: 'resolved'
|
||||
process-only: 'issues'
|
||||
log-output: false
|
||||
26
.github/workflows/qodana_code_quality.yml
vendored
Normal file
26
.github/workflows/qodana_code_quality.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2023.2
|
||||
with:
|
||||
pr-mode: false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
desktop.ini
|
||||
|
||||
*.csproj.user
|
||||
*.pubxml
|
||||
*.DotSettings.user
|
||||
|
||||
.vs/
|
||||
@@ -9,13 +10,13 @@ src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
|
||||
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Test/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/Properties/PublishProfiles/FolderProfile.pubxml.user
|
||||
src/Snap.Hutao/Snap.Hutao.Test/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
@@ -1,77 +0,0 @@
|
||||
stages:
|
||||
- fetch
|
||||
- release
|
||||
- refresh
|
||||
|
||||
Fetch:
|
||||
stage: fetch
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
tags:
|
||||
- us3
|
||||
script:
|
||||
- apt-get update -qy
|
||||
- apt-get install -y curl jq
|
||||
- RELEASE_INFO=$(curl -sSL "https://api.github.com/repos/$CI_PROJECT_PATH/releases/latest")
|
||||
- ASSET_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | endswith(".msix")) | .browser_download_url')
|
||||
- SHA256SUMS_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url')
|
||||
- curl -LJO "$ASSET_URL"
|
||||
- curl -LJO "$SHA256SUMS_URL"
|
||||
- FILE_NAME=$(basename "$ASSET_URL")
|
||||
- SHA256SUMS_NAME=$(basename "$SHA256SUMS_URL")
|
||||
- echo "File name at script stage is $FILE_NAME"
|
||||
- echo "SHA256SUMS name at script stage is $SHA256SUMS_NAME"
|
||||
- echo "THIS_FILE_NAME=$FILE_NAME" >> next.env
|
||||
- echo "THIS_SHA256SUMS_NAME=$SHA256SUMS_NAME" >> next.env
|
||||
after_script:
|
||||
- echo "Current Job ID is $CI_JOB_ID"
|
||||
- echo "THIS_JOB_ID=$CI_JOB_ID" >> next.env
|
||||
artifacts:
|
||||
paths:
|
||||
- "*.msix"
|
||||
- "SHA256SUMS"
|
||||
expire_in: 180 days
|
||||
reports:
|
||||
dotenv: next.env
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: Fetch
|
||||
artifacts: true
|
||||
variables:
|
||||
TAG: '$CI_COMMIT_TAG'
|
||||
script:
|
||||
- echo "Create Release $TAG"
|
||||
- echo "$THIS_JOB_ID"
|
||||
- echo "$THIS_FILE_NAME"
|
||||
release:
|
||||
name: '$TAG'
|
||||
tag_name: '$TAG'
|
||||
ref: '$TAG'
|
||||
description: 'Release $TAG by CI'
|
||||
assets:
|
||||
links:
|
||||
- name: "$THIS_FILE_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_FILE_NAME?inline=false"
|
||||
link_type: package
|
||||
- name: "$THIS_SHA256SUMS_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
|
||||
link_type: other
|
||||
- name: "artifact_archive"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
|
||||
link_type: other
|
||||
|
||||
Refresh:
|
||||
stage: refresh
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: release
|
||||
script:
|
||||
- apt-get install -y curl
|
||||
- curl -X PATCH "$PURGE_URL"
|
||||
- curl -X POST -o /dev/null "$UPLOAD_OSS_URL"
|
||||
@@ -4,15 +4,13 @@
|
||||
|
||||
### Setup Snap.Hutao Project
|
||||
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
|
||||
- No need to select workloads; Visual Studio will handle it automatically.
|
||||
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
|
||||
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
|
||||
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
|
||||
3. Switch to the`develop` branch using git.
|
||||
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
|
||||
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
|
||||
6. Open the project solution with your Visual Studio, and you are ready to go.
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
|
||||
2. Open Visual Studio Installer to complete Visual Studio installation
|
||||
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
|
||||
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
|
||||
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
|
||||
5. Switch git branch to `develop`
|
||||
6. Open project solution with your Visual Studio and then you are ready to go
|
||||
|
||||
### Start Pull Request
|
||||
|
||||
|
||||
73
README.md
73
README.md
@@ -1,44 +1,18 @@
|
||||

|
||||

|
||||
|
||||
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
|
||||
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. By combining existing official resources with new features designed by the development team, it provides a complete and useful set of tools without the need to rely on mobile devices. Snap Hutao does not take any destructive modification to the game client to ensure the security of the toolkit.
|
||||
|
||||
## 安装 / Installation
|
||||
## 下载使用 / Download
|
||||
|
||||
 [](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) []()
|
||||
|
||||
---
|
||||
|
||||
你可以按照[快速开始](https://hut.ao/zh/quick-start.html)文档中提供的流程安装并设置 Snap Hutao。
|
||||
|
||||
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
|
||||
|
||||
## 本地化翻译 / Localization
|
||||
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
|
||||
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.
|
||||
|
||||
## 社区 / Community
|
||||
|
||||
[](https://discord.gg/CcH5XtDtvR) [](https://qm.qq.com/q/WJKykrY9W)
|
||||
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
|
||||
|
||||
## 贡献 / Contribute
|
||||
|
||||
* [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
|
||||
* [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
|
||||
* [向我们提交 PR / Make Pull Requests](https://github.com/DGP-Studio/Snap.Hutao/pulls)
|
||||
* [在 Crowdin 上进行本地化 / Translate Project on Crowdin](https://translate.hut.ao/)
|
||||
* [为我们更新文档 / Enhance our Document ](https://github.com/DGP-Studio/Snap.Hutao.Docs)
|
||||
|
||||
## 特别感谢 / Special Thanks
|
||||
|
||||
@@ -47,51 +21,30 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
|
||||
|
||||
### 特定的原神项目 / Specific Genshin-related Projects
|
||||
|
||||
* [Scighost/Starward](https://github.com/Scighost/Starward)
|
||||
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
|
||||
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
|
||||
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
|
||||
|
||||
### 使用的技术栈 / Tech Stack
|
||||
|
||||
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
|
||||
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
|
||||
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
|
||||
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
|
||||
* [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)
|
||||
|
||||
### 支撑项目 / Supporter Project
|
||||
|
||||
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
|
||||
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
|
||||
|
||||
## 赞助商 / Sponsorship
|
||||
|
||||
Snap Hutao is currently using sponsored software from the following service providers.
|
||||
|
||||
| [](https://www.netlify.com/) | [](https://crowdin.com/) | [](https://gitlab.cn/) |
|
||||
|:-:|:-:|:-:|
|
||||
|[](https://about.signpath.io)|[](https://1password.com/)|[](https://www.digitalocean.com)|
|
||||
|[](https://www.jetbrains.com/opensource/)|||
|
||||
|
||||
- Netlify provides document and home page hosting service for Snap Hutao
|
||||
|
||||
- Crowdin provides its SaaS platform to help Snap Hutao's localization
|
||||
|
||||
- Jihu GitLab (极狐) provides Git repository and CI/CD SaaS service for Snap Hutao in China
|
||||
|
||||
- Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
||||
|
||||
- 1Password provides Snap Hutao development team with their amazing password management software
|
||||
|
||||
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
|
||||
|
||||
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
|
||||
|
||||
## 开发 / Development
|
||||
|
||||

|
||||
|
||||
[](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
|
||||
[](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >=1.9.0 | :white_check_mark: |
|
||||
| <1.9.0 | :x: |
|
||||
| >=1.6.0 | :white_check_mark: |
|
||||
| <1.6.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
20
appveyor.yml
20
appveyor.yml
@@ -1,20 +0,0 @@
|
||||
version: 1.0.{build}
|
||||
branches:
|
||||
only:
|
||||
- "release"
|
||||
build_cloud: HUTAO-SERVER
|
||||
image: Visual Studio 2022
|
||||
clone_depth: 3
|
||||
clone_folder: D:\appveyor\project\Snap.Hutao.Project
|
||||
install:
|
||||
- pwsh: dotnet tool restore
|
||||
build_script:
|
||||
- pwsh: dotnet cake
|
||||
artifacts:
|
||||
- path: src/output/*.msix
|
||||
type: file
|
||||
deploy:
|
||||
- provider: Webhook
|
||||
url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix
|
||||
authorization:
|
||||
secure: j8srQ5/UYWhI+jlm3Vo3D3QfXoRyQ9hOn3ynJGtwusKui4+uDi4gykdUFYCITZxK+C/fOCAZNJ+YaKSm/OaiXw==
|
||||
191
azure-pipelines.yml
Normal file
191
azure-pipelines.yml
Normal file
@@ -0,0 +1,191 @@
|
||||
# CI process script for Snap.Hutao
|
||||
# Usage:
|
||||
# 1. Append the script in Pipelines
|
||||
# 2. Upload the pfx and cer certificates to Pipelines Library secrets
|
||||
# 3. Permit the pfx usage
|
||||
# 4. Add a `pw` variable in the script variables, which is pfx password
|
||||
# 5. Connect the GitHub in project settings
|
||||
# 6. Run
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
- azure-pipelines.yml
|
||||
- .github/ISSUE_TEMPLATE/*.yml
|
||||
- .github/workflows/*.yml
|
||||
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
|
||||
|
||||
pool:
|
||||
name: Default
|
||||
demands: agent.name -equals Hutao-Server
|
||||
|
||||
variables:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
solution: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao.sln'
|
||||
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
|
||||
buildPlatform: 'x64'
|
||||
buildConfiguration: 'Release'
|
||||
build_date: $[ format('{0:yyyy}.{0:M}.{0:d}', pipeline.startTime) ]
|
||||
|
||||
|
||||
steps:
|
||||
- task: GetRevision@1
|
||||
displayName: get Pipelines revision number
|
||||
inputs:
|
||||
VariableName: 'rev_number'
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Install dotNet
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '7.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
name: 'NuGetToolInstaller'
|
||||
displayName: 'NuGet Installer'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: NuGet restore
|
||||
inputs:
|
||||
command: 'restore'
|
||||
restoreSolution: '$(solution)'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.Config'
|
||||
|
||||
- task: MsixPackaging@1
|
||||
displayName: Build binary package
|
||||
inputs:
|
||||
outputPath: '$(Build.ArtifactStagingDirectory)/'
|
||||
solution: '$(solution)'
|
||||
clean: false
|
||||
generateBundle: false
|
||||
buildConfiguration: 'Release'
|
||||
buildPlatform: 'x64'
|
||||
updateAppVersion: false
|
||||
appPackageDistributionMode: 'SideloadOnly'
|
||||
msbuildLocationMethod: 'location'
|
||||
msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe'
|
||||
|
||||
- task: MagicChunks@2
|
||||
inputs:
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\AppxManifest.xml'
|
||||
fileType: 'Xml'
|
||||
targetPathType: 'source'
|
||||
transformationType: 'json'
|
||||
transformations: |
|
||||
{
|
||||
"Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c",
|
||||
"Package/Identity/@Publisher": "CN=DGP Studio CI",
|
||||
"Package/Identity/@Version": "$(build_date).$(rev_number)",
|
||||
"Package/Properties/DisplayName": "胡桃 Alpha",
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
|
||||
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
|
||||
}
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Create resources folder
|
||||
inputs:
|
||||
script: |
|
||||
mkdir Assets
|
||||
|
||||
mkdir Resource
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64'
|
||||
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Assets Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Assets'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Resource Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Resource'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Build MSIX
|
||||
inputs:
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
displayName: Sign MSIX package
|
||||
inputs:
|
||||
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
certificate: 'DGP_Studio_CI.pfx'
|
||||
passwordVariable: 'pw'
|
||||
condition: succeeded()
|
||||
|
||||
|
||||
#- task: PublishPipelineArtifact@1
|
||||
# displayName: 'Upload Output'
|
||||
# inputs:
|
||||
# targetPath: '$(Build.ArtifactStagingDirectory)/'
|
||||
# artifact: 'Output'
|
||||
# publishLocation: 'pipeline'
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
name: cerFile
|
||||
displayName: Download Root CA
|
||||
inputs:
|
||||
secureFile: 'Snap.Hutao.CI.cer'
|
||||
|
||||
- task: GitHubRelease@1
|
||||
inputs:
|
||||
gitHubConnection: 'github.com_Masterain'
|
||||
repositoryName: 'DGP-Studio/Snap.Hutao'
|
||||
action: 'create'
|
||||
target: '$(Build.SourceVersion)'
|
||||
tagSource: 'userSpecifiedTag'
|
||||
tag: '$(build_date).$(rev_number)'
|
||||
title: '$(build_date).$(rev_number)'
|
||||
releaseNotesSource: 'inline'
|
||||
releaseNotesInline: |
|
||||
## 普通用户请勿下载
|
||||
该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
|
||||
普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
|
||||
assets: |
|
||||
$(Build.ArtifactStagingDirectory)/*
|
||||
$(cerFile.secureFilePath)
|
||||
isPreRelease: true
|
||||
changeLogCompareToRelease: 'lastFullRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload PR CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/PR/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
223
build.cake
223
build.cake
@@ -1,223 +0,0 @@
|
||||
#tool "nuget:?package=nuget.commandline&version=6.9.1"
|
||||
#addin nuget:?package=Cake.Http&version=4.0.0
|
||||
|
||||
var target = Argument("target", "Build");
|
||||
var configuration = Argument("configuration", "Release");
|
||||
|
||||
// Pre-define
|
||||
|
||||
var version = "version";
|
||||
|
||||
var repoDir = "repoDir";
|
||||
var outputPath = "outputPath";
|
||||
|
||||
// Extension
|
||||
|
||||
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
|
||||
{
|
||||
return condition ? builder.Append(text) : builder;
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
string solution
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
|
||||
}
|
||||
string project
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj");
|
||||
}
|
||||
string binPath
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64");
|
||||
}
|
||||
string manifest
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
|
||||
}
|
||||
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
repoDir = GitHubActions.Environment.Workflow.Workspace.FullPath;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
if (GitHubActions.Environment.PullRequest.IsPullRequest)
|
||||
{
|
||||
version = System.DateTime.Now.ToString("yyyy.M.d.0");
|
||||
|
||||
Information("Is Pull Request. Skip version.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||
version = HttpGet(
|
||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||
new HttpSettings
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "Authorization", versionAuth }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
GitHubActions.Commands.SetOutputParameter("version", version);
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
repoDir = AppVeyor.Environment.Build.Folder;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
version = XmlPeek(manifest, "appx:Package/appx:Identity/@Version", new XmlPeekSettings
|
||||
{
|
||||
Namespaces = new Dictionary<string, string> { { "appx", "http://schemas.microsoft.com/appx/manifest/foundation/windows10" } }
|
||||
})[..^2];
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
else // Local
|
||||
{
|
||||
repoDir = System.Environment.CurrentDirectory;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
Task("Build")
|
||||
.IsDependentOn("Build binary package")
|
||||
.IsDependentOn("Copy files")
|
||||
.IsDependentOn("Build MSIX");
|
||||
|
||||
Task("NuGet Restore")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Restoring packages...");
|
||||
|
||||
var nugetConfig = System.IO.Path.Combine(repoDir, "NuGet.Config");
|
||||
DotNetRestore(project, new DotNetRestoreSettings
|
||||
{
|
||||
Verbosity = DotNetVerbosity.Detailed,
|
||||
Interactive = false,
|
||||
ConfigFile = nugetConfig
|
||||
});
|
||||
});
|
||||
|
||||
Task("Generate AppxManifest")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Generating AppxManifest...");
|
||||
|
||||
var content = System.IO.File.ReadAllText(manifest);
|
||||
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
Information("Using CI configuraion");
|
||||
content = content
|
||||
.Replace("Snap Hutao", "Snap Hutao Alpha")
|
||||
.Replace("胡桃", "胡桃 Alpha")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
Information("Using Release configuration");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
Information("Using Local configuration.");
|
||||
content = content
|
||||
.Replace("Snap Hutao", "Snap Hutao Local")
|
||||
.Replace("胡桃", "胡桃 Local")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"E8B6E2B3-D2A0-4435-A81D-2A16AAF405C7\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
|
||||
}
|
||||
|
||||
System.IO.File.WriteAllText(manifest, content);
|
||||
|
||||
Information("Generated.");
|
||||
});
|
||||
|
||||
Task("Build binary package")
|
||||
.IsDependentOn("NuGet Restore")
|
||||
.IsDependentOn("Generate AppxManifest")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Building binary package...");
|
||||
|
||||
var settings = new DotNetBuildSettings
|
||||
{
|
||||
Configuration = configuration
|
||||
};
|
||||
|
||||
settings.MSBuildSettings = new DotNetMSBuildSettings
|
||||
{
|
||||
ArgumentCustomization = args => args.Append("/p:Platform=x64")
|
||||
.Append("/p:UapAppxPackageBuildMode=SideloadOnly")
|
||||
.Append("/p:AppxPackageSigningEnabled=false")
|
||||
.Append("/p:AppxBundle=Never")
|
||||
.Append("/p:AppxPackageOutput=" + outputPath)
|
||||
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
|
||||
};
|
||||
|
||||
DotNetBuild(project, settings);
|
||||
});
|
||||
|
||||
Task("Copy files")
|
||||
.IsDependentOn("Build binary package")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Copying assets...");
|
||||
CopyDirectory(
|
||||
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Assets"),
|
||||
System.IO.Path.Combine(binPath, "Assets")
|
||||
);
|
||||
|
||||
Information("Copying resource...");
|
||||
CopyDirectory(
|
||||
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Resource"),
|
||||
System.IO.Path.Combine(binPath, "Resource")
|
||||
);
|
||||
});
|
||||
|
||||
Task("Build MSIX")
|
||||
.IsDependentOn("Build binary package")
|
||||
.IsDependentOn("Copy files")
|
||||
.Does(() =>
|
||||
{
|
||||
var arguments = "arguments";
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
|
||||
}
|
||||
var p = StartProcess(
|
||||
"makeappx.exe",
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = arguments
|
||||
}
|
||||
);
|
||||
if (p != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Build failed with exit code " + p);
|
||||
}
|
||||
});
|
||||
|
||||
RunTarget(target);
|
||||
33
qodana.yaml
Normal file
33
qodana.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
exclude:
|
||||
- name: Test
|
||||
paths:
|
||||
- Snap.Hutao.Test
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- name: All
|
||||
paths:
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- Snap.Hutao.Test
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-dotnet:2023.2-eap
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -108,7 +108,6 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_diagnostic.SA1629.severity = none
|
||||
dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
@@ -123,6 +122,9 @@ dotnet_diagnostic.SA1623.severity = none
|
||||
# SA1636: File header copyright text should match
|
||||
dotnet_diagnostic.SA1636.severity = none
|
||||
|
||||
# SA1414: Tuple types in signatures should have element names
|
||||
dotnet_diagnostic.SA1414.severity = none
|
||||
|
||||
# SA0001: XML comment analysis disabled
|
||||
dotnet_diagnostic.SA0001.severity = none
|
||||
csharp_style_prefer_parameter_null_checking = true:suggestion
|
||||
@@ -321,8 +323,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
|
||||
csharp_style_prefer_primary_constructors = false:none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedDesktop",
|
||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
||||
"Microsoft.VisualStudio.Workload.Universal"
|
||||
],
|
||||
"extensions": [
|
||||
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class AttributeGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateAllAttributes);
|
||||
}
|
||||
|
||||
public static void GenerateAllAttributes(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string coreAnnotations = """
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class CommandAttribute : Attribute
|
||||
{
|
||||
public CommandAttribute(string name)
|
||||
{
|
||||
}
|
||||
|
||||
public bool AllowConcurrentExecutions { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
public ConstructorGeneratedAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public bool CallBaseConstructor { get; set; }
|
||||
public bool ResolveHttpClient { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class DependencyPropertyAttribute : Attribute
|
||||
{
|
||||
public DependencyPropertyAttribute(string name, Type type)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAttached { get; set; }
|
||||
public Type AttachedType { get; set; } = default;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
[Conditional("DEBUG")]
|
||||
internal sealed class HighQualityAttribute : Attribute
|
||||
{
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.Annotation.Attributes.g.cs", coreAnnotations);
|
||||
|
||||
string coreDependencyInjectionAnnotationHttpClients = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class HttpClientAttribute : Attribute
|
||||
{
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal enum HttpClientConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求配置
|
||||
/// </summary>
|
||||
XRpc,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社登录请求配置
|
||||
/// </summary>
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab app
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class PrimaryHttpMessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
|
||||
public int MaxConnectionsPerServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.UseCookies"/>
|
||||
/// </summary>
|
||||
public bool UseCookies { get; set; }
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.Attributes.g.cs", coreDependencyInjectionAnnotationHttpClients);
|
||||
|
||||
string coreDependencyInjectionAnnotations = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation;
|
||||
|
||||
internal enum InjectAs
|
||||
{
|
||||
Singleton,
|
||||
Transient,
|
||||
Scoped,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class InjectionAttribute : Attribute
|
||||
{
|
||||
public InjectionAttribute(InjectAs injectAs)
|
||||
{
|
||||
}
|
||||
|
||||
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedMethods, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2<IMethodSymbol>.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateCommandImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedMethods(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is MethodDeclarationSyntax methodDeclarationSyntax
|
||||
&& methodDeclarationSyntax.Parent is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& methodDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2<IMethodSymbol> CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out IMethodSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2<IMethodSymbol> context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateCommandImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementation(SourceProductionContext production, GeneratorSyntaxContext2<IMethodSymbol> context2)
|
||||
{
|
||||
INamedTypeSymbol classSymbol = context2.Symbol.ContainingType;
|
||||
|
||||
AttributeData commandInfo = context2.SingleAttribute(AttributeName);
|
||||
string commandName = (string)commandInfo.ConstructorArguments[0].Value!;
|
||||
|
||||
string commandType = context2.Symbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task")
|
||||
? "AsyncRelayCommand"
|
||||
: "RelayCommand";
|
||||
|
||||
string genericParameter = context2.Symbol.Parameters.ElementAtOrDefault(0) is IParameterSymbol parameter
|
||||
? $"<{parameter.Type.ToDisplayString(SymbolDisplayFormats.FullyQualifiedNonNullableFormat)}>"
|
||||
: string.Empty;
|
||||
|
||||
string concurrentExecution = commandInfo.HasNamedArgumentWith<bool>("AllowConcurrentExecutions", value => value)
|
||||
? ", AsyncRelayCommandOptions.AllowConcurrentExecutions"
|
||||
: string.Empty;
|
||||
|
||||
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
string code = $$"""
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace {{classSymbol.ContainingNamespace}};
|
||||
|
||||
partial class {{className}}
|
||||
{
|
||||
private ICommand _{{commandName}};
|
||||
|
||||
public ICommand {{commandName}}
|
||||
{
|
||||
get => _{{commandName}} ??= new {{commandType}}{{genericParameter}}({{context2.Symbol.Name}}{{concurrentExecution}});
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
|
||||
private const string CompilerGenerated = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
|
||||
|
||||
//private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, ConstructorGeneratedClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(injectionClasses, GenerateConstructorImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 ConstructorGeneratedClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateConstructorImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
|
||||
|
||||
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
|
||||
bool callBaseConstructor = constructorInfo.HasNamedArgumentWith<bool>("CallBaseConstructor", value => value);
|
||||
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
|
||||
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient, callBaseConstructor);
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
|
||||
partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
|
||||
{
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor ? " : base(serviceProvider)" : string.Empty)}}
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.ctor.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context2.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.OfType<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
if (fieldSymbol.Name.AsSpan()[0] is '<')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool shoudSkip = false;
|
||||
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax declarator)
|
||||
{
|
||||
if (declarator.Initializer is not null)
|
||||
{
|
||||
// Skip field with initializer
|
||||
builder.Append(" // Skip field with initializer: ").AppendLine(fieldSymbol.Name);
|
||||
shoudSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shoudSkip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
|
||||
{
|
||||
switch (fieldSymbol.Type.ToDisplayString())
|
||||
{
|
||||
case "System.IServiceProvider":
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = serviceProvider;");
|
||||
break;
|
||||
|
||||
case "System.Net.Http.HttpClient":
|
||||
if (options.ResolveHttpClient)
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = httpClient;");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(nameof(")
|
||||
.Append(context2.Symbol.Name)
|
||||
.AppendLine("));");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<")
|
||||
.Append(fieldSymbol.Type)
|
||||
.AppendLine(">();");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
{
|
||||
if (interfaceSymbol.Name == "IRecipient")
|
||||
{
|
||||
builder
|
||||
.Append(" CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Register<")
|
||||
.Append(interfaceSymbol.TypeArguments[0])
|
||||
.AppendLine(">(serviceProvider.GetRequiredService<CommunityToolkit.Mvvm.Messaging.IMessenger>(), this);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct FieldValueAssignmentOptions
|
||||
{
|
||||
public readonly bool ResolveHttpClient;
|
||||
public readonly bool CallBaseConstructor;
|
||||
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient, bool callBaseConstructor)
|
||||
{
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
CallBaseConstructor = callBaseConstructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateDependencyPropertyImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateDependencyPropertyImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
|
||||
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
|
||||
string register = isAttached ? "RegisterAttached" : "Register";
|
||||
|
||||
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[1].Value!.ToString();
|
||||
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
|
||||
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
|
||||
|
||||
string code;
|
||||
if (isAttached)
|
||||
{
|
||||
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
|
||||
? attachedType.Value!.ToString()
|
||||
: "object";
|
||||
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
|
||||
{
|
||||
return ({{propertyType}})obj?.GetValue({{propertyName}}Property);
|
||||
}
|
||||
|
||||
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
|
||||
{
|
||||
obj.SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public {{propertyType}} {{propertyName}}
|
||||
{
|
||||
get => ({{propertyType}})GetValue({{propertyName}}Property);
|
||||
set => SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetLiteralString(TypedConstant typedConstant)
|
||||
{
|
||||
if (typedConstant.IsNull)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typedConstant.Value is bool boolValue)
|
||||
{
|
||||
return boolValue ? "true" : "false";
|
||||
}
|
||||
|
||||
string result = typedConstant.Value!.ToString();
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HttpClient httpClient = new();
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateSaltContstants);
|
||||
}
|
||||
|
||||
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
Response<SaltLatest> saltInfo = JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
string code = $$"""
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
internal sealed class SaltConstants
|
||||
{
|
||||
public const string CNVersion = "{{saltInfo.Data.CNVersion}}";
|
||||
public const string CNK2 = "{{saltInfo.Data.CNK2}}";
|
||||
public const string CNLK2 = "{{saltInfo.Data.CNLK2}}";
|
||||
|
||||
public const string OSVersion = "{{saltInfo.Data.OSVersion}}";
|
||||
public const string OSK2 = "{{saltInfo.Data.OSK2}}";
|
||||
public const string OSLK2 = "{{saltInfo.Data.OSLK2}}";
|
||||
}
|
||||
""";
|
||||
context.AddSource("SaltConstants.g.cs", code);
|
||||
}
|
||||
|
||||
private sealed class Response<T>
|
||||
{
|
||||
[DataMember(Name = "data")]
|
||||
public T Data { get; set; } = default!;
|
||||
}
|
||||
|
||||
internal sealed class SaltLatest
|
||||
{
|
||||
public string CNVersion { get; set; } = default!;
|
||||
|
||||
public string CNK2 { get; set; } = default!;
|
||||
|
||||
public string CNLK2 { get; set; } = default!;
|
||||
|
||||
public string OSVersion { get; set; } = default!;
|
||||
|
||||
public string OSK2 { get; set; } = default!;
|
||||
|
||||
public string OSLK2 { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
private const string HttpClientConfiguration = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.";
|
||||
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
|
||||
{
|
||||
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
|
||||
{
|
||||
if (injectionData.ConstructorArguments.Length < 2)
|
||||
{
|
||||
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Append(@" services.AddHttpClient<");
|
||||
|
||||
AttributeData httpClientData = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
|
||||
lineBuilder.Append(arguments[0].ToCSharpString().Substring(HttpClientConfiguration.Length)).Append("Configuration)");
|
||||
|
||||
if (context.SingleOrDefaultAttribute(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
|
||||
{
|
||||
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
|
||||
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> property in properties)
|
||||
{
|
||||
lineBuilder.Append(' ');
|
||||
lineBuilder.Append(property.Key);
|
||||
lineBuilder.Append(" = ");
|
||||
lineBuilder.Append(property.Value.ToCSharpString());
|
||||
lineBuilder.Append(',');
|
||||
}
|
||||
|
||||
lineBuilder.Append(" })");
|
||||
}
|
||||
|
||||
lineBuilder.Append(';');
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
|
||||
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddServices(sourceBuilder, context, context2s);
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
lineBuilder.Clear().AppendLine();
|
||||
|
||||
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
|
||||
string injectAsName = arguments[0].ToCSharpString();
|
||||
switch (injectAsName)
|
||||
{
|
||||
case InjectAsSingletonName:
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class ServiceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get => new DiagnosticDescriptor[]
|
||||
{
|
||||
NonSingletonUseServiceProviderDescriptor,
|
||||
SingletonServiceCaptureNonSingletonServiceDescriptor,
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration);
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.HasAttributeLists())
|
||||
{
|
||||
INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
|
||||
if (classSymbol is not null)
|
||||
{
|
||||
foreach (AttributeData attributeData in classSymbol.GetAttributes())
|
||||
{
|
||||
if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName)
|
||||
{
|
||||
string serviceType = attributeData.ConstructorArguments[0].ToCSharpString();
|
||||
if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName)
|
||||
{
|
||||
HandleNonSingletonUseServiceProviderActual(context, classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault();
|
||||
|
||||
if (symbol is not null)
|
||||
{
|
||||
Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
//classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Enum;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
|
||||
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
|
||||
.Where(GeneratorSyntaxContext2.NotNull);
|
||||
|
||||
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is EnumDeclarationSyntax enumDeclarationSyntax
|
||||
&& enumDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, enumSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
internal static class {{context2.Symbol.Name}}Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return Enum.GetName(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
[return:MaybeNull]
|
||||
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.Cast<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
if (localizationKeyInfo != null)
|
||||
{
|
||||
sourceBuilder
|
||||
.Append(" ")
|
||||
.Append(fieldSymbol)
|
||||
.Append(" => \"")
|
||||
.Append(localizationKeyInfo.ConstructorArguments[0].Value)
|
||||
.AppendLine("\",");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("", "RS2008")]
|
||||
@@ -0,0 +1,202 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Identity;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string FileName = "IdentityStructs.json";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(provider, GenerateIdentityStructs);
|
||||
}
|
||||
|
||||
private static bool MatchFileName(AdditionalText text)
|
||||
{
|
||||
return Path.GetFileName(text.Path) == FileName;
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStructs(SourceProductionContext context, ImmutableArray<AdditionalText> texts)
|
||||
{
|
||||
AdditionalText jsonFile = texts.Single();
|
||||
|
||||
string identityJson = jsonFile.GetText(context.CancellationToken)!.ToString();
|
||||
List<IdentityStructMetadata> identities = identityJson.FromJson<List<IdentityStructMetadata>>()!;
|
||||
|
||||
if (identities.Any())
|
||||
{
|
||||
foreach (IdentityStructMetadata identityStruct in identities)
|
||||
{
|
||||
GenerateIdentityStruct(context, identityStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStruct(SourceProductionContext context, IdentityStructMetadata metadata)
|
||||
{
|
||||
string name = metadata.Name;
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().AppendLine($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// {{metadata.Documentation}}
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(IdentityConverter<{{name}}>))]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
|
||||
internal readonly partial struct {{name}}
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly uint Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="{{name}}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public {{name}}(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator uint({{name}} value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator {{name}}(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
if (metadata.Equatable)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEquatable<{{name}}>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is {{name}} other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals({{name}} other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.EqualityOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEqualityOperators<{{name}}, {{name}}, bool>, IEqualityOperators<{{name}}, uint, bool>
|
||||
{
|
||||
public static bool operator ==({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator ==({{name}} left, uint right)
|
||||
{
|
||||
return left.Value == right;
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, {{name}} right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, uint right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.AdditionOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IAdditionOperators<{{name}}, {{name}}, {{name}}>, IAdditionOperators<{{name}}, uint, {{name}}>
|
||||
{
|
||||
public static {{name}} operator +({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value + right.Value;
|
||||
}
|
||||
|
||||
public static {{name}} operator +({{name}} left, uint right)
|
||||
{
|
||||
return left.Value + right;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.IncrementOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IIncrementOperators<{{name}}>
|
||||
{
|
||||
public static unsafe {{name}} operator ++({{name}} value)
|
||||
{
|
||||
++*(uint*)&value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
context.AddSource($"{name}.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private sealed class IdentityStructMetadata
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? Documentation { get; set; }
|
||||
|
||||
public bool Equatable { get; set; }
|
||||
|
||||
public bool EqualityOperators { get; set; }
|
||||
|
||||
public bool AdditionOperators { get; set; }
|
||||
|
||||
public bool IncrementOperators { get; set; }
|
||||
}
|
||||
}
|
||||
439
src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs
Normal file
439
src/Snap.Hutao/Snap.Hutao.SourceGeneration/JsonParser.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
// Really simple JSON parser in ~300 lines
|
||||
// - Attempts to parse JSON files with minimal GC allocation
|
||||
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
|
||||
// - Classes and structs can be parsed too!
|
||||
// class Foo { public int Value; }
|
||||
// "{\"Value\":10}".FromJson<Foo>()
|
||||
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
|
||||
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
|
||||
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
|
||||
// - No JIT Emit support to support AOT compilation on iOS
|
||||
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
|
||||
// - Only public fields and property setters on classes/structs will be written to
|
||||
//
|
||||
// Limitations:
|
||||
// - No JIT Emit support to parse structures quickly
|
||||
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
|
||||
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
|
||||
public static class JsonParser
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<List<string>>? splitArrayPool;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder? stringBuilder;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, FieldInfo>>? fieldInfoCache;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, PropertyInfo>>? propertyInfoCache;
|
||||
|
||||
public static T? FromJson<T>(this string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
stringBuilder ??= new StringBuilder();
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (c == '"')
|
||||
{
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
}
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
// Parse the thing!
|
||||
return (T?)ParseValue(typeof(T), stringBuilder.ToString());
|
||||
}
|
||||
|
||||
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
|
||||
{
|
||||
stringBuilder!.Append(json[startIdx]);
|
||||
for (int i = startIdx + 1; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '\\')
|
||||
{
|
||||
if (appendEscapeCharacter)
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i + 1]);
|
||||
i++;//Skip next character as it is escaped
|
||||
}
|
||||
else if (json[i] == '"')
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
return i;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
}
|
||||
return json.Length - 1;
|
||||
}
|
||||
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : new List<string>();
|
||||
splitArray.Clear();
|
||||
if (json.Length == 2)
|
||||
{
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
int parseDepth = 0;
|
||||
stringBuilder!.Length = 0;
|
||||
for (int i = 1; i < json.Length - 1; i++)
|
||||
{
|
||||
switch (json[i])
|
||||
{
|
||||
case '[':
|
||||
case '{':
|
||||
parseDepth++;
|
||||
break;
|
||||
case ']':
|
||||
case '}':
|
||||
parseDepth--;
|
||||
break;
|
||||
case '"':
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
case ',':
|
||||
case ':':
|
||||
if (parseDepth == 0)
|
||||
{
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
stringBuilder.Length = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
internal static object? ParseValue(Type type, string json)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
if (json.Length <= 2)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder parseStringBuilder = new(json.Length);
|
||||
for (int i = 1; i < json.Length - 1; ++i)
|
||||
{
|
||||
if (json[i] == '\\' && i + 1 < json.Length - 1)
|
||||
{
|
||||
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
|
||||
if (j >= 0)
|
||||
{
|
||||
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
|
||||
{
|
||||
if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c))
|
||||
{
|
||||
parseStringBuilder.Append((char)c);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
parseStringBuilder.Append(json[i]);
|
||||
}
|
||||
return parseStringBuilder.ToString();
|
||||
}
|
||||
if (type.IsPrimitive)
|
||||
{
|
||||
object result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(decimal))
|
||||
{
|
||||
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(DateTime))
|
||||
{
|
||||
DateTime.TryParse(json.Replace("\"", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime result);
|
||||
return result;
|
||||
}
|
||||
if (json == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (json[0] == '"')
|
||||
{
|
||||
json = json.Substring(1, json.Length - 2);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return System.Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type arrayType = type.GetElementType();
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
Array newArray = Array.CreateInstance(arrayType, elems.Count);
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return newArray;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
Type listType = type.GetGenericArguments()[0];
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
IList list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
list.Add(ParseValue(listType, elems[i]));
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return list;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
Type keyType, valueType;
|
||||
{
|
||||
Type[] args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
}
|
||||
|
||||
//Refuse to parse dictionary keys that aren't of type string
|
||||
if (keyType != typeof(string))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//Must be a valid dictionary element
|
||||
if (json[0] != '{' || json[json.Length - 1] != '}')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IDictionary dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
|
||||
object? val = ParseValue(valueType, elems[i + 1]);
|
||||
dictionary[keyValue] = val;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
if (type == typeof(object))
|
||||
{
|
||||
return ParseAnonymousValue(json);
|
||||
}
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
return ParseObject(type, json);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ParseAnonymousValue(string json)
|
||||
{
|
||||
if (json.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> dict = new(elems.Count / 2);
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
if (json[0] == '[' && json[json.Length - 1] == ']')
|
||||
{
|
||||
List<string> items = Split(json);
|
||||
List<object?> finalList = new(items.Count);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
finalList.Add(ParseAnonymousValue(items[i]));
|
||||
}
|
||||
|
||||
return finalList;
|
||||
}
|
||||
if (json[0] == '"' && json[json.Length - 1] == '"')
|
||||
{
|
||||
string str = json.Substring(1, json.Length - 2);
|
||||
return str.Replace("\\", string.Empty);
|
||||
}
|
||||
if (char.IsDigit(json[0]) || json[0] == '-')
|
||||
{
|
||||
if (json.Contains("."))
|
||||
{
|
||||
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
int.TryParse(json, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (json == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (json == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// handles json == "null" as well as invalid JSON
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
|
||||
{
|
||||
Dictionary<string, T> nameToMember = new(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
{
|
||||
T member = members[i];
|
||||
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = member.Name;
|
||||
if (member.IsDefined(typeof(DataMemberAttribute), true))
|
||||
{
|
||||
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
|
||||
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
|
||||
{
|
||||
name = dataMemberAttribute.Name;
|
||||
}
|
||||
}
|
||||
|
||||
nameToMember.Add(name, member);
|
||||
}
|
||||
|
||||
return nameToMember;
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string json)
|
||||
{
|
||||
object instance = FormatterServices.GetUninitializedObject(type);
|
||||
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (!fieldInfoCache!.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
|
||||
{
|
||||
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
fieldInfoCache.Add(type, nameToField);
|
||||
}
|
||||
if (!propertyInfoCache!.TryGetValue(type, out Dictionary<string, PropertyInfo> nameToProperty))
|
||||
{
|
||||
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
propertyInfoCache.Add(type, nameToProperty);
|
||||
}
|
||||
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = elems[i].Substring(1, elems[i].Length - 2);
|
||||
string value = elems[i + 1];
|
||||
|
||||
if (nameToField.TryGetValue(key, out FieldInfo fieldInfo))
|
||||
{
|
||||
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
|
||||
}
|
||||
else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo))
|
||||
{
|
||||
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class AttributeDataExtension
|
||||
{
|
||||
public static bool HasNamedArgumentWith<TValue>(this AttributeData data, string key, Func<TValue, bool> predicate)
|
||||
{
|
||||
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class EnumerableExtension
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
return DistinctByIterator(source, keySelector);
|
||||
}
|
||||
|
||||
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
using IEnumerator<TSource> enumerator = source.GetEnumerator();
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = new();
|
||||
|
||||
do
|
||||
{
|
||||
TSource element = enumerator.Current;
|
||||
if (set.Add(keySelector(element)))
|
||||
{
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
|
||||
{
|
||||
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly INamedTypeSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2 context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public bool HasAttributeWithName(string name)
|
||||
{
|
||||
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData? SingleOrDefaultAttribute(string name)
|
||||
{
|
||||
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public TSyntaxNode Node<TSyntaxNode>()
|
||||
where TSyntaxNode : SyntaxNode
|
||||
{
|
||||
return (TSyntaxNode)Context.Node;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2<TSymbol>
|
||||
where TSymbol : ISymbol
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly TSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, TSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2<TSymbol> context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class GeneratorSyntaxContextExtension
|
||||
{
|
||||
public static bool TryGetDeclaredSymbol<TSymbol>(this GeneratorSyntaxContext context, System.Threading.CancellationToken token, [NotNullWhen(true)] out TSymbol? symbol)
|
||||
where TSymbol : class, ISymbol
|
||||
{
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(context.Node, token) as TSymbol;
|
||||
return symbol != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SymbolDisplayFormats
|
||||
{
|
||||
public static SymbolDisplayFormat FullyQualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
|
||||
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SyntaxExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="MemberDeclarationSyntax"/> has or could potentially have any attribute lists.
|
||||
/// </summary>
|
||||
/// <param name="declaration">The input <see cref="MemberDeclarationSyntax"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="declaration"/> has or potentially has any attribute lists.</returns>
|
||||
public static bool HasAttributeLists<TSyntax>(this TSyntax declaration)
|
||||
where TSyntax : MemberDeclarationSyntax
|
||||
{
|
||||
return declaration.AttributeLists.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class TypeSymbolExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="name"/>.</returns>
|
||||
public static bool IsOrInheritsFrom(this ITypeSymbol typeSymbol, string name)
|
||||
{
|
||||
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
if (currentType.ToDisplayString() == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
312
src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs
Normal file
312
src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 通用分析器
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
|
||||
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
|
||||
{
|
||||
"System.Threading.CancellationToken",
|
||||
"System.Guid"
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DiagnosticDescriptor[]
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
useValueTaskIfPossibleDescriptor,
|
||||
useIsNotNullPatternMatchingDescriptor,
|
||||
useIsNullPatternMatchingDescriptor,
|
||||
useIsPatternRecursiveMatchingDescriptor,
|
||||
useArgumentNullExceptionThrowIfNullDescriptor
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types =
|
||||
{
|
||||
SyntaxKind.ClassDeclaration,
|
||||
SyntaxKind.InterfaceDeclaration,
|
||||
SyntaxKind.StructDeclaration,
|
||||
SyntaxKind.EnumDeclaration,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
|
||||
|
||||
SyntaxKind[] expressions =
|
||||
{
|
||||
SyntaxKind.EqualsExpression,
|
||||
SyntaxKind.NotEqualsExpression,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
|
||||
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
|
||||
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
|
||||
|
||||
// TODO add analyzer for unnecessary IServiceProvider registration
|
||||
// TODO add analyzer for Singlton service use Scoped or Transient services
|
||||
}
|
||||
|
||||
private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
|
||||
|
||||
bool privateExists = false;
|
||||
bool internalExists = false;
|
||||
bool fileExists = false;
|
||||
|
||||
foreach (SyntaxToken token in syntax.Modifiers)
|
||||
{
|
||||
if (token.IsKind(SyntaxKind.PrivateKeyword))
|
||||
{
|
||||
privateExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.InternalKeyword))
|
||||
{
|
||||
internalExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.FileKeyword))
|
||||
{
|
||||
fileExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateExists && !internalExists && !fileExists)
|
||||
{
|
||||
Location location = syntax.Identifier.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location, syntax.Identifier);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
// 跳过重载方法
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ICommand can only use Task or Task<T>
|
||||
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
Location location = methodSyntax.ReturnType.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过方法定义 如 接口
|
||||
if (methodSyntax.Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SyntaxToken token in methodSyntax.Modifiers)
|
||||
{
|
||||
// 跳过异步方法,因为异步方法无法使用 ref/in/out
|
||||
if (token.IsKind(SyntaxKind.AsyncKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过重载方法
|
||||
if (token.IsKind(SyntaxKind.OverrideKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleConstructorParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
|
||||
|
||||
foreach (ParameterSyntax parameter in constructorSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
|
||||
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
else if (syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
|
||||
if (syntax.Pattern is DeclarationPatternSyntax declaration)
|
||||
{
|
||||
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
|
||||
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
|
||||
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
|
||||
{
|
||||
Location location = declaration.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
PostfixUnaryExpressionSyntax syntax = (PostfixUnaryExpressionSyntax)context.Node;
|
||||
|
||||
if (syntax.Operand is LiteralExpressionSyntax literal)
|
||||
{
|
||||
if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (syntax.Operand is DefaultExpressionSyntax expression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location location = syntax.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsBuiltInType(ITypeSymbol symbol)
|
||||
{
|
||||
return symbol.SpecialType switch
|
||||
{
|
||||
SpecialType.System_Boolean => true,
|
||||
SpecialType.System_Char => true,
|
||||
SpecialType.System_SByte => true,
|
||||
SpecialType.System_Byte => true,
|
||||
SpecialType.System_Int16 => true,
|
||||
SpecialType.System_UInt16 => true,
|
||||
SpecialType.System_Int32 => true,
|
||||
SpecialType.System_UInt32 => true,
|
||||
SpecialType.System_Int64 => true,
|
||||
SpecialType.System_UInt64 => true,
|
||||
SpecialType.System_Decimal => true,
|
||||
SpecialType.System_Single => true,
|
||||
SpecialType.System_Double => true,
|
||||
SpecialType.System_IntPtr => true,
|
||||
SpecialType.System_UIntPtr => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
23
src/Snap.Hutao/Snap.Hutao.SourceGeneration/stylecop.json
Normal file
23
src/Snap.Hutao/Snap.Hutao.SourceGeneration/stylecop.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "DGP Studio",
|
||||
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.",
|
||||
"xmlHeader": false,
|
||||
"variables": {
|
||||
"licenseName": "MIT"
|
||||
}
|
||||
},
|
||||
"orderingRules": {
|
||||
"elementOrder": [
|
||||
"kind",
|
||||
"accessibility",
|
||||
"constant",
|
||||
"static",
|
||||
"readonly"
|
||||
],
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class CollectionsMarshalTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
|
||||
{
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key")));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
|
||||
{
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class JsonSerializeTest
|
||||
{
|
||||
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
private const string SmapleObjectJson = """
|
||||
{
|
||||
"A" :1
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleEmptyStringObjectJson = """
|
||||
{
|
||||
"A" : ""
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberKeyDictionaryJson = """
|
||||
{
|
||||
"111" : "12",
|
||||
"222" : "34"
|
||||
}
|
||||
""";
|
||||
|
||||
[TestMethod]
|
||||
public void DelegatePropertyCanSerialize()
|
||||
{
|
||||
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
|
||||
Assert.AreEqual(sample.B, 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ByteArraySerializeAsBase64()
|
||||
{
|
||||
SampleByteArrayPropertyClass sample = new()
|
||||
{
|
||||
Array = [1, 2, 3, 4, 5],
|
||||
};
|
||||
|
||||
string result = JsonSerializer.Serialize(sample);
|
||||
Assert.AreEqual(result, """{"Array":"AQIDBAU="}""");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InterfaceDefaultMethodCanSerializeActualInstanceMember()
|
||||
{
|
||||
ISampleInterface sample = new SampleClassImplementedInterface()
|
||||
{
|
||||
A = 1,
|
||||
B = 2,
|
||||
};
|
||||
|
||||
string result = sample.ToJson();
|
||||
Console.WriteLine(result);
|
||||
Assert.AreEqual(result, """{"A":1,"B":2}""");
|
||||
}
|
||||
|
||||
private sealed class SampleDelegatePropertyClass
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SampleStringReadWriteNumberPropertyClass
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SampleByteArrayPropertyClass
|
||||
{
|
||||
public byte[]? Array { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SampleClassImplementedInterface : ISampleInterface
|
||||
{
|
||||
public int A { get; set; }
|
||||
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
[JsonDerivedType(typeof(SampleClassImplementedInterface))]
|
||||
private interface ISampleInterface
|
||||
{
|
||||
int A { get; set; }
|
||||
|
||||
string ToJson()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class LinqTest
|
||||
{
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public void LinqOrderByWithWrapperStructThrow()
|
||||
{
|
||||
List<MyUInt32> list = [1, 5, 2, 6, 3, 7, 4, 8];
|
||||
string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value));
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
|
||||
private readonly struct MyUInt32
|
||||
{
|
||||
public readonly uint Value;
|
||||
|
||||
public MyUInt32(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator MyUInt32(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class TypeReflectionTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void TypeCodeOfEnumIsUserlyingTypeTypeCode()
|
||||
{
|
||||
Assert.AreEqual(Type.GetTypeCode(typeof(TestEnum)), TypeCode.Int32);
|
||||
}
|
||||
|
||||
private enum TestEnum
|
||||
{
|
||||
A,
|
||||
B,
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class UnsafeAccessorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void UnsafeAccessorCanGetInterfaceProperty()
|
||||
{
|
||||
TestClass test = new();
|
||||
int value = InternalGetInterfaceProperty(test);
|
||||
Assert.AreEqual(3, value);
|
||||
}
|
||||
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
|
||||
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
|
||||
|
||||
internal interface ITestInterface
|
||||
{
|
||||
internal int TestProperty { get; }
|
||||
}
|
||||
|
||||
internal sealed class TestClass : ITestInterface
|
||||
{
|
||||
public int TestProperty { get; } = 3;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DependencyInjectionTest
|
||||
@@ -12,10 +10,7 @@ public sealed class DependencyInjectionTest
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.AddLogging(builder => builder.AddConsole())
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
@@ -46,22 +41,6 @@ public sealed class DependencyInjectionTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoggerWithInterfaceTypeCanBeResolved()
|
||||
{
|
||||
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
|
||||
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeyedServicesCanBeResolvedAsEnumerable()
|
||||
{
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
|
||||
|
||||
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
@@ -107,14 +86,4 @@ public sealed class DependencyInjectionTest
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private interface IKeyedService;
|
||||
|
||||
private sealed class KeyedServiceA : IKeyedService
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class KeyedServiceB : IKeyedService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class GameRegistryContentTest
|
||||
{
|
||||
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void GetRegistryContent()
|
||||
{
|
||||
GetRegistryContentCore(@"Software\miHoYo\原神");
|
||||
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void GetRegistryContentCore(string subkey)
|
||||
{
|
||||
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
|
||||
{
|
||||
RegistryKey? gameKey = key.OpenSubKey(subkey);
|
||||
Assert.IsNotNull(gameKey);
|
||||
|
||||
Dictionary<string, object> data = [];
|
||||
foreach (string valueName in gameKey.GetValueNames())
|
||||
{
|
||||
data[valueName] = gameKey.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
|
||||
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine($"Subkey: {subkey}");
|
||||
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe object GetStringOrObject(byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
string temp = Encoding.UTF8.GetString(span);
|
||||
|
||||
if (temp.AsSpan()[0] is '{' or '[')
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public sealed class GeniusInvokationDecoding
|
||||
{
|
||||
public TestContext? TestContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// https://www.bilibili.com/video/av278125720
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public unsafe void GeniusInvokationShareCodeDecoding()
|
||||
{
|
||||
// 51 bytes obfuscated data
|
||||
byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB");
|
||||
|
||||
// ---------------------------------------------
|
||||
// | Data | Caesar Cipher Key |
|
||||
// |----------|-------------------|
|
||||
// | 50 Bytes | 1 Byte |
|
||||
// ---------------------------------------------
|
||||
// Data:
|
||||
// 00000100 00100001 11000001 11000011 00010100
|
||||
// 00001101 00000001 10000001 00010001 11001001
|
||||
// 01010000 00001101 00001000 10010001 10000001
|
||||
// 11001010 01111001 00001110 00001001 10010001
|
||||
// 00010001 11011010 10011000 00001110 00001010
|
||||
// 10110001 01110001 11110010 10011010 00010000
|
||||
// 00001010 00010001 10010001 11110101 10110110
|
||||
// 00010000 00001100 01000001 10000001 11110110
|
||||
// 10111001 00010000 00001100 01010010 10010001
|
||||
// 00001111 10111010 00010001 00001101 11100001
|
||||
// ---------------------------------------------
|
||||
// Caesar Cipher Key:
|
||||
// 00000001
|
||||
// ---------------------------------------------
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
// Reinterpret as 50 byte actual data and 1 deobfuscate key byte
|
||||
EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr;
|
||||
byte* dataPtr = data->Data;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// | First | Second | Padding |
|
||||
// |-----------|----------|---------|
|
||||
// | 25 Bytes | 25 Bytes | 1 Byte |
|
||||
// ----------------------------------------------------------
|
||||
// We are doing two things here:
|
||||
// 1. Retrieve actual data by subtracting key
|
||||
// 2. Store data into two halves by alternating between them
|
||||
// ----------------------------------------------------------
|
||||
// What we will get after this step:
|
||||
// ----------------------------------------------------------
|
||||
// First:
|
||||
// 00000011 11000000 00010011 00000000 00010000
|
||||
// 01001111 00000111 10000000 01111000 00001000
|
||||
// 00010000 10010111 00001001 01110000 10011001
|
||||
// 00001001 10010000 10110101 00001011 10000000
|
||||
// 10111000 00001011 10010000 10111001 00001100
|
||||
// ----------------------------------------------------------
|
||||
// Second:
|
||||
// 00100000 11000010 00001100 10000000 11001000
|
||||
// 00001100 10010000 11001001 00001101 10010000
|
||||
// 11011001 00001101 10110000 11110001 00001111
|
||||
// 00010000 11110100 00001111 01000000 11110101
|
||||
// 00001111 01010001 00001110 00010000 11100000
|
||||
// ----------------------------------------------------------
|
||||
RearrangeBuffer rearranged = default;
|
||||
byte* pFirst = rearranged.First;
|
||||
byte* pSecond = rearranged.Second;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Determine which half are we going to insert
|
||||
byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond;
|
||||
|
||||
// (actual data = data - key) and store it directly to the target half
|
||||
**ppTarget = unchecked((byte)(dataPtr[i] - data->Key));
|
||||
|
||||
(*ppTarget)++;
|
||||
}
|
||||
|
||||
// Prepare decoded data result storage
|
||||
DecryptedData decoded = default;
|
||||
ushort* pDecoded = decoded.Data;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// | Data |
|
||||
// |----------| x 17 = 51 Bytes
|
||||
// | 3 Bytes |
|
||||
// ----------------------------------------------------------
|
||||
// Grouping each 3 bytes and read out as 2 ushort with
|
||||
// 12 bits each (Big Endian)
|
||||
// ----------------------------------------------------------
|
||||
// 00000011 1100·0000 00010011|
|
||||
// 00000000 0001·0000 01001111|
|
||||
// 00000111 1000·0000 01111000|
|
||||
// 00001000 0001·0000 10010111|
|
||||
// 00001001 0111·0000 10011001|
|
||||
// 00001001 1001·0000 10110101|
|
||||
// 00001011 1000·0000 10111000|
|
||||
// 00001011 1001·0000 10111001|
|
||||
// 00001100 0010·0000 11000010|
|
||||
// 00001100 1000·0000 11001000|
|
||||
// 00001100 1001·0000 11001001|
|
||||
// 00001101 1001·0000 11011001|
|
||||
// 00001101 1011·0000 11110001|
|
||||
// 00001111 0001·0000 11110100|
|
||||
// 00001111 0100·0000 11110101|
|
||||
// 00001111 0101·0001 00001110|
|
||||
// 00010000 1110·0000 -padding|[padding32]
|
||||
// ----------------------------------------------------------
|
||||
// reinterpret as DecodeGroupingHelper for each 3 bytes
|
||||
DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged;
|
||||
for (int i = 0; i < 17; i++)
|
||||
{
|
||||
(ushort first, ushort second) = pGroup->GetData();
|
||||
|
||||
*pDecoded = first;
|
||||
*(pDecoded + 1) = second;
|
||||
|
||||
pDecoded += 2;
|
||||
pGroup++;
|
||||
}
|
||||
|
||||
// Now we get
|
||||
// 60, 19, 1,
|
||||
// 79,120,120,
|
||||
// 129,151,151,
|
||||
// 153,153,181,
|
||||
// 184,184,185,
|
||||
// 185,194,194,
|
||||
// 200,200,201,
|
||||
// 201,217,217,
|
||||
// 219,241,241,
|
||||
// 244,244,245,
|
||||
// 245,270,270,
|
||||
StringBuilder stringBuilder = new();
|
||||
for (int i = 0; i < 33; i++)
|
||||
{
|
||||
stringBuilder
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i])
|
||||
.Append(',');
|
||||
|
||||
if (i % 11 == 10)
|
||||
{
|
||||
stringBuilder.Append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1));
|
||||
|
||||
ushort[] resultArray = new ushort[33];
|
||||
Span<ushort> result = new((ushort*)&decoded, 33);
|
||||
result.CopyTo(resultArray);
|
||||
|
||||
ushort[] testKnownResult =
|
||||
[
|
||||
060,
|
||||
019,
|
||||
001,
|
||||
079,
|
||||
120,
|
||||
120,
|
||||
129,
|
||||
151,
|
||||
151,
|
||||
153,
|
||||
153,
|
||||
181,
|
||||
184,
|
||||
184,
|
||||
185,
|
||||
185,
|
||||
194,
|
||||
194,
|
||||
200,
|
||||
200,
|
||||
201,
|
||||
201,
|
||||
217,
|
||||
217,
|
||||
219,
|
||||
241,
|
||||
241,
|
||||
244,
|
||||
244,
|
||||
245,
|
||||
245,
|
||||
270,
|
||||
270,
|
||||
];
|
||||
|
||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||
}
|
||||
}
|
||||
|
||||
private struct EncryptedDataAndKey
|
||||
{
|
||||
public unsafe fixed byte Data[50];
|
||||
public byte Key;
|
||||
}
|
||||
|
||||
private struct RearrangeBuffer
|
||||
{
|
||||
public unsafe fixed byte First[25];
|
||||
public unsafe fixed byte Second[25];
|
||||
|
||||
// Make it 51 bytes
|
||||
// allow to be group as 17 DecodeGroupingHelper later
|
||||
public byte padding;
|
||||
|
||||
// prevent accidently int32 cast access violation
|
||||
public byte paddingTo32;
|
||||
}
|
||||
|
||||
private struct DecodeGroupingHelper
|
||||
{
|
||||
public unsafe fixed byte Data[3];
|
||||
|
||||
public unsafe (ushort First, ushort Second) GetData()
|
||||
{
|
||||
fixed (byte* ptr = Data)
|
||||
{
|
||||
uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only
|
||||
return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DecryptedData
|
||||
{
|
||||
public unsafe fixed ushort Data[33];
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class SpiralAbyssScheduleIdTest
|
||||
{
|
||||
private static readonly TimeSpan Utc8 = new(8, 0, 0);
|
||||
|
||||
[TestMethod]
|
||||
public void Test()
|
||||
{
|
||||
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
|
||||
|
||||
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
|
||||
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
|
||||
}
|
||||
|
||||
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
// Force time in UTC+08
|
||||
dateTimeOffset = dateTimeOffset.ToOffset(Utc8);
|
||||
|
||||
((int year, int mouth, int day), (int hour, _), _) = dateTimeOffset;
|
||||
|
||||
// 2020-07-01 04:00:00 为第 1 期
|
||||
int periodNum = (((year - 2020) * 12) + (mouth - 6)) * 2;
|
||||
|
||||
// 上半月:1-15 日, 以及 16 日 00:00-04:00
|
||||
if (day < 16 || (day == 16 && hour < 4))
|
||||
{
|
||||
periodNum--;
|
||||
}
|
||||
|
||||
// 上个月:1 日 00:00-04:00
|
||||
if (day is 1 && hour < 4)
|
||||
{
|
||||
periodNum--;
|
||||
}
|
||||
|
||||
return periodNum;
|
||||
}
|
||||
}
|
||||
67
src/Snap.Hutao/Snap.Hutao.Test/JsonSerializeTest.cs
Normal file
67
src/Snap.Hutao/Snap.Hutao.Test/JsonSerializeTest.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class JsonSerializeTest
|
||||
{
|
||||
private const string SmapleObjectJson = """
|
||||
{
|
||||
"A" :1
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleEmptyStringObjectJson = """
|
||||
{
|
||||
"A" : ""
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberKeyDictionaryJson = """
|
||||
{
|
||||
"111" : "12",
|
||||
"222" : "34"
|
||||
}
|
||||
""";
|
||||
|
||||
[TestMethod]
|
||||
public void DelegatePropertyCanSerialize()
|
||||
{
|
||||
Sample sample = JsonSerializer.Deserialize<Sample>(SmapleObjectJson)!;
|
||||
Assert.AreEqual(sample.B, 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
JsonSerializerOptions options = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
private sealed class Sample
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
private sealed class StringNumberSample
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,11 @@ public sealed class ForEachRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
{
|
||||
List<string> strings =
|
||||
#if NET8_0_OR_GREATER
|
||||
["a", "b", "c"];
|
||||
#else
|
||||
new() { "a", "b", "c" };
|
||||
#endif
|
||||
List<string> strings = new()
|
||||
{
|
||||
"a", "b", "c"
|
||||
};
|
||||
|
||||
int count = 0;
|
||||
foreach (ReadOnlySpan<char> chars in strings)
|
||||
{
|
||||
|
||||
@@ -8,13 +8,8 @@ public sealed class RangeRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void RangeTrimLastOne()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
int[] array = [1, 2, 3, 4];
|
||||
int[] test = [1, 2, 3];
|
||||
#else
|
||||
int[] array = { 1, 2, 3, 4 };
|
||||
int[] test = { 1, 2, 3 };
|
||||
#endif
|
||||
int[] result = array[..^1];
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));
|
||||
|
||||
@@ -1,79 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class UnsafeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void UInt32AllSetIsUInt32MaxValue()
|
||||
public unsafe void UInt32AllSetIs()
|
||||
{
|
||||
byte[] bytes =
|
||||
#if NET8_0_OR_GREATER
|
||||
[0xFF, 0xFF, 0xFF, 0xFF];
|
||||
#else
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
#endif
|
||||
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
|
||||
fixed (byte* pBytes = bytes)
|
||||
{
|
||||
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UInt32LayoutIsLittleEndian()
|
||||
{
|
||||
ulong testValue = 0x1234567887654321;
|
||||
ref BuildVersion version = ref Unsafe.As<ulong, BuildVersion>(ref testValue);
|
||||
|
||||
Assert.AreEqual(0x1234, version.Major);
|
||||
Assert.AreEqual(0x5678, version.Minor);
|
||||
Assert.AreEqual(0x8765, version.Patch);
|
||||
Assert.AreEqual(0x4321, version.Build);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void ReadOnlyStructCanBeModifiedInCtor()
|
||||
{
|
||||
TestStruct testStruct = new([4444, 7878, 5656, 1212]);
|
||||
|
||||
Assert.AreEqual(4444, testStruct.Value1);
|
||||
Assert.AreEqual(7878, testStruct.Value2);
|
||||
Assert.AreEqual(5656, testStruct.Value3);
|
||||
Assert.AreEqual(1212, testStruct.Value4);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UnsafeUtf8StringReference()
|
||||
{
|
||||
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8));
|
||||
GC.Collect(GC.MaxGeneration);
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
|
||||
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
|
||||
}
|
||||
|
||||
private readonly struct TestStruct
|
||||
{
|
||||
public readonly int Value1;
|
||||
public readonly int Value2;
|
||||
public readonly int Value3;
|
||||
public readonly int Value4;
|
||||
|
||||
public TestStruct(List<int> list)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As<TestStruct, int>(ref this), 4));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct BuildVersion
|
||||
{
|
||||
public readonly ushort Build;
|
||||
public readonly ushort Patch;
|
||||
public readonly ushort Minor;
|
||||
public readonly ushort Major;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
5
src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json
Normal file
5
src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"useSafeHandles": false
|
||||
}
|
||||
74
src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt
Normal file
74
src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt
Normal file
@@ -0,0 +1,74 @@
|
||||
// COMCTL32
|
||||
DefSubclassProc
|
||||
RemoveWindowSubclass
|
||||
SetWindowSubclass
|
||||
|
||||
// DWMAPI
|
||||
DwmSetWindowAttribute
|
||||
|
||||
// GDI32
|
||||
GetDeviceCaps
|
||||
|
||||
// KERNEL32
|
||||
CloseHandle
|
||||
CreateEventW
|
||||
CreateRemoteThread
|
||||
GetModuleHandleW
|
||||
GetProcAddress
|
||||
K32EnumProcessModules
|
||||
K32GetModuleBaseNameW
|
||||
K32GetModuleInformation
|
||||
ReadProcessMemory
|
||||
SetEvent
|
||||
VirtualAlloc
|
||||
VirtualAllocEx
|
||||
VirtualFree
|
||||
VirtualFreeEx
|
||||
WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
|
||||
// OLE32
|
||||
CoWaitForMultipleObjects
|
||||
|
||||
// USER32
|
||||
AttachThreadInput
|
||||
FindWindowExW
|
||||
GetCursorPos
|
||||
GetDC
|
||||
GetDpiForWindow
|
||||
GetForegroundWindow
|
||||
GetWindowPlacement
|
||||
GetWindowThreadProcessId
|
||||
ReleaseDC
|
||||
RegisterHotKey
|
||||
SendInput
|
||||
SetForegroundWindow
|
||||
UnregisterHotKey
|
||||
|
||||
// COM
|
||||
IPersistFile
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
SHELL_LINK_DATA_FLAGS
|
||||
|
||||
// WinRT
|
||||
IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
INFINITE
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
WM_NCRBUTTONDOWN
|
||||
WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
|
||||
// System.Threading
|
||||
LPTHREAD_START_ROUTINE
|
||||
|
||||
// UI.WindowsAndMessaging
|
||||
MINMAXINFO
|
||||
|
||||
// System.Com
|
||||
CWMO_FLAGS
|
||||
25
src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj
Normal file
25
src/Snap.Hutao/Snap.Hutao.Win32/Snap.Hutao.Win32.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="NativeMethods.json" />
|
||||
<None Remove="NativeMethods.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
src/Snap.Hutao/Snap.Hutao.Win32/StructMarshal.cs
Normal file
36
src/Snap.Hutao/Snap.Hutao.Win32/StructMarshal.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
[assembly: InternalsVisibleTo("Snap.Hutao")]
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
/// <summary>
|
||||
/// 结构体封送
|
||||
/// </summary>
|
||||
internal static class StructMarshal
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
|
||||
/// </summary>
|
||||
/// <returns>新的实例</returns>
|
||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
||||
{
|
||||
return new() { length = SizeOf<WINDOWPLACEMENT>() };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取结构的大小
|
||||
/// </summary>
|
||||
/// <typeparam name="TStruct">结构类型</typeparam>
|
||||
/// <returns>结构的大小</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe uint SizeOf<TStruct>()
|
||||
where TStruct : unmanaged
|
||||
{
|
||||
return unchecked((uint)sizeof(TStruct));
|
||||
}
|
||||
}
|
||||
37
src/Snap.Hutao/Snap.Hutao.Win32/UnsafePInvoke.cs
Normal file
37
src/Snap.Hutao/Snap.Hutao.Win32/UnsafePInvoke.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI.Core;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
internal static class UnsafePInvoke
|
||||
{
|
||||
private enum WINDOW_TYPE : uint
|
||||
{
|
||||
IMMERSIVE_BODY,
|
||||
IMMERSIVE_DOCK,
|
||||
IMMERSIVE_HOSTED,
|
||||
IMMERSIVE_TEST,
|
||||
IMMERSIVE_BODY_ACTIVE,
|
||||
IMMERSIVE_DOCK_ACTIVE,
|
||||
NOT_IMMERSIVE,
|
||||
}
|
||||
|
||||
[DllImport("Windows.UI.dll", CharSet = CharSet.None, EntryPoint = "#1500", ExactSpelling = false, SetLastError = true)]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
private static extern HRESULT PrivateCreateCoreWindow(WINDOW_TYPE WindowType, PWSTR pWindowTitle, int x, int y, uint uWidth, uint uHeight, uint dwAttributes, HWND hOwnerWindow, Guid riid, out nint ppv);
|
||||
|
||||
public static unsafe CoreWindow PrivateCreateCoreWindow(string title, HWND hOwnerWindow)
|
||||
{
|
||||
fixed(char* pTitle = title)
|
||||
{
|
||||
PrivateCreateCoreWindow(WINDOW_TYPE.NOT_IMMERSIVE, pTitle, 0, 0, 400, 400, 0, hOwnerWindow, typeof(ICoreWindow).GUID, out nint thisPtr);
|
||||
return CoreWindow.FromAbi(thisPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/Snap.Hutao/Snap.Hutao.Win32/WinRTCustomMarshaler.cs
Normal file
72
src/Snap.Hutao/Snap.Hutao.Win32/WinRTCustomMarshaler.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Windows.Win32.CsWin32.InteropServices;
|
||||
|
||||
internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
{
|
||||
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
|
||||
|
||||
private readonly string className;
|
||||
private bool lookedForFromAbi;
|
||||
private MethodInfo? fromAbiMethod;
|
||||
|
||||
private WinRTCustomMarshaler(string className)
|
||||
{
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
public static ICustomMarshaler GetInstance(string cookie)
|
||||
{
|
||||
return new WinRTCustomMarshaler(cookie);
|
||||
}
|
||||
|
||||
public void CleanUpManagedData(object ManagedObj)
|
||||
{
|
||||
}
|
||||
|
||||
public void CleanUpNativeData(nint pNativeData)
|
||||
{
|
||||
Marshal.Release(pNativeData);
|
||||
}
|
||||
|
||||
public int GetNativeDataSize()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public nint MarshalManagedToNative(object ManagedObj)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object MarshalNativeToManaged(nint thisPtr)
|
||||
{
|
||||
return className switch
|
||||
{
|
||||
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
|
||||
_ => MarshalNativeToManagedSlow(thisPtr),
|
||||
};
|
||||
}
|
||||
|
||||
private object MarshalNativeToManagedSlow(nint pNativeData)
|
||||
{
|
||||
if (!lookedForFromAbi)
|
||||
{
|
||||
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
|
||||
|
||||
fromAbiMethod = type?.GetMethod("FromAbi");
|
||||
lookedForFromAbi = true;
|
||||
}
|
||||
|
||||
if (fromAbiMethod is not null)
|
||||
{
|
||||
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,14 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.vsconfig = .vsconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -49,6 +52,22 @@ Global
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
@@ -65,16 +84,32 @@ Global
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_SortFileContentOnSave = True
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -4,18 +4,12 @@
|
||||
"add": {
|
||||
"extensionToExtension": {
|
||||
"add": {
|
||||
".json": [
|
||||
".txt"
|
||||
]
|
||||
".json": [ ".txt" ]
|
||||
}
|
||||
},
|
||||
"pathSegment": {
|
||||
"add": {
|
||||
".*": [
|
||||
".cs",
|
||||
".resx",
|
||||
".appxmanifest"
|
||||
]
|
||||
".*": [ ".cs", ".resx" ]
|
||||
}
|
||||
},
|
||||
"fileSuffixToExtension": {
|
||||
@@ -25,24 +19,12 @@
|
||||
},
|
||||
"fileToFile": {
|
||||
"add": {
|
||||
".filenesting.json": [
|
||||
"App.xaml.cs"
|
||||
],
|
||||
"app.manifest": [
|
||||
"App.xaml.cs"
|
||||
],
|
||||
"Package.appxmanifest": [
|
||||
"App.xaml"
|
||||
],
|
||||
"Package.StoreAssociation.xml": [
|
||||
"App.xaml"
|
||||
],
|
||||
".editorconfig": [
|
||||
"Program.cs"
|
||||
],
|
||||
"GlobalUsing.cs": [
|
||||
"Program.cs"
|
||||
]
|
||||
".filenesting.json": [ "App.xaml.cs" ],
|
||||
"app.manifest": [ "App.xaml.cs" ],
|
||||
"Package.appxmanifest": [ "App.xaml" ],
|
||||
"Package.StoreAssociation.xml": [ "App.xaml" ],
|
||||
".editorconfig": [ "Program.cs" ],
|
||||
"GlobalUsing.cs": [ "Program.cs" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,12 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
|
||||
@@ -23,21 +19,16 @@
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SegmentedOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
</Style>
|
||||
<Style
|
||||
|
||||
@@ -6,9 +6,7 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
@@ -22,24 +20,9 @@ namespace Snap.Hutao;
|
||||
[SuppressMessage("", "SH001")]
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private const string ConsoleBanner = $"""
|
||||
----------------------------------------------------------------
|
||||
_____ _ _ _
|
||||
/ ____| | | | | | |
|
||||
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
|
||||
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
|
||||
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|
||||
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
|
||||
| |
|
||||
|_|
|
||||
|
||||
Snap.Hutao is a open source software developed by DGP Studio.
|
||||
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
|
||||
----------------------------------------------------------------
|
||||
""";
|
||||
|
||||
private const string AppInstanceKey = "main";
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IAppActivation activation;
|
||||
private readonly IActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -50,42 +33,41 @@ public sealed partial class App : Application
|
||||
{
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
activation = serviceProvider.GetRequiredService<IAppActivation>();
|
||||
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public new void Exit()
|
||||
{
|
||||
XamlWindowLifetime.ApplicationExiting = true;
|
||||
base.Exit();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
||||
|
||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||
if (firstInstance.IsCurrent)
|
||||
{
|
||||
Exit();
|
||||
return;
|
||||
// manually invoke
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
LogDiagnosticInformation();
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Redirect the activation (and args) to the "main" instance, and exit.
|
||||
firstInstance.RedirectActivationTo(activatedEventArgs);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
|
||||
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// Manually invoke
|
||||
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
activation.PostInitialization();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
// AppInstance.GetCurrent() calls failed
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
@@ -94,8 +76,8 @@ public sealed partial class App : Application
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
logger.LogColorizedInformation(("FamilyName: {Name}", ConsoleColor.Blue), (runtimeOptions.FamilyName, ConsoleColor.Cyan));
|
||||
logger.LogColorizedInformation(("Version: {Version}", ConsoleColor.Blue), (runtimeOptions.Version, ConsoleColor.Cyan));
|
||||
logger.LogColorizedInformation(("LocalCache: {Path}", ConsoleColor.Blue), (runtimeOptions.LocalCache, ConsoleColor.Cyan));
|
||||
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
|
||||
logger.LogInformation("Version: {version}", runtimeOptions.Version);
|
||||
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Animation;
|
||||
|
||||
/// <summary>
|
||||
/// 动画时长
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class AnimationDurations
|
||||
{
|
||||
/// <summary>
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageZoom = TimeSpan.FromSeconds(0.5);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡入
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡出
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Animation;
|
||||
|
||||
internal static class ControlAnimationConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 1
|
||||
/// </summary>
|
||||
public const string One = "1";
|
||||
|
||||
/// <summary>
|
||||
/// 1.1
|
||||
/// </summary>
|
||||
public const string OnePointOne = "1.1";
|
||||
|
||||
/// <summary>
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageZoom = TimeSpan.FromSeconds(0.5);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡入
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageScaleFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡出
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageScaleFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
|
||||
public static readonly TimeSpan ImageOpacityFadeInOut = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomInAnimation()
|
||||
{
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = ControlAnimationConstants.OnePointOne;
|
||||
To = Core.StringLiterals.OnePointOne;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomOutAnimation()
|
||||
{
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = ControlAnimationConstants.One;
|
||||
To = Core.StringLiterals.One;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using System.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
[DependencyProperty("FilterCommand", typeof(ICommand))]
|
||||
[DependencyProperty("FilterCommandParameter", typeof(object))]
|
||||
[DependencyProperty("AvailableTokens", typeof(IReadOnlyDictionary<string, SearchToken>))]
|
||||
internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
|
||||
{
|
||||
public AutoSuggestTokenBox()
|
||||
{
|
||||
DefaultStyleKey = typeof(TokenizingTextBox);
|
||||
TextChanged += OnFilterSuggestionRequested;
|
||||
QuerySubmitted += OnQuerySubmitted;
|
||||
TokenItemAdding += OnTokenItemAdding;
|
||||
TokenItemAdded += OnTokenItemCollectionChanged;
|
||||
TokenItemRemoved += OnTokenItemCollectionChanged;
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
|
||||
{
|
||||
IAppResourceProvider appResourceProvider = this.ServiceProvider().GetRequiredService<IAppResourceProvider>();
|
||||
|
||||
listView.Background = null;
|
||||
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
|
||||
|
||||
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
|
||||
CornerRadius overlayCornerRadius = appResourceProvider.GetResource<CornerRadius>("OverlayCornerRadius");
|
||||
CornerRadiusFilterConverter cornerRadiusFilterConverter = new() { Filter = CornerRadiusFilterKind.Bottom };
|
||||
border.CornerRadius = (CornerRadius)cornerRadiusFilterConverter.Convert(overlayCornerRadius, typeof(CornerRadius), default, default);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Text))
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.Select(kvp => kvp.Value);
|
||||
}
|
||||
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.ThenBy(kvp => kvp.Value.Order)
|
||||
.Select(kvp => kvp.Value)
|
||||
.DefaultIfEmpty(SearchToken.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
{
|
||||
if (args.ChosenSuggestion is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
|
||||
}
|
||||
|
||||
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.TokenText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
|
||||
{
|
||||
args.Item = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTokenItemCollectionChanged(TokenizingTextBox sender, object args)
|
||||
{
|
||||
if (args is SearchToken { Kind: SearchTokenKind.None } token)
|
||||
{
|
||||
((IList)sender.ItemsSource).Remove(token);
|
||||
}
|
||||
|
||||
FilterCommand.TryExecute(FilterCommandParameter);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
internal sealed class SearchToken
|
||||
{
|
||||
public static readonly SearchToken NotFound = new(SearchTokenKind.None, SH.ControlAutoSuggestBoxNotFoundValue, 0);
|
||||
|
||||
public SearchToken(SearchTokenKind kind, string value, int order, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
|
||||
{
|
||||
Value = value;
|
||||
Kind = kind;
|
||||
IconUri = iconUri;
|
||||
SideIconUri = sideIconUri;
|
||||
Quality = quality;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public SearchTokenKind Kind { get; }
|
||||
|
||||
public string Value { get; set; } = default!;
|
||||
|
||||
public Uri? IconUri { get; }
|
||||
|
||||
public Uri? SideIconUri { get; }
|
||||
|
||||
public Color? Quality { get; }
|
||||
|
||||
public int Order { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
internal enum SearchTokenKind
|
||||
{
|
||||
None,
|
||||
ItemQuality,
|
||||
WeaponType,
|
||||
FightProperty,
|
||||
ElementName,
|
||||
AssociationType,
|
||||
BodyType,
|
||||
Avatar,
|
||||
Weapon,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
@@ -36,16 +35,15 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
|
||||
|
||||
private void TryExecuteCommand()
|
||||
{
|
||||
if (AssociatedObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
executed = Command.TryExecute(CommandParameter);
|
||||
if (Command is not null && Command.CanExecute(CommandParameter))
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
executed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user