This commit is contained in:
fanbook-wangdage
2025-11-20 20:28:27 +08:00
commit 1f268acc29
2530 changed files with 221582 additions and 0 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "5.1.0",
"commands": [
"dotnet-cake"
]
}
}
}

13
.github/FUNDING.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

31
.github/ISSUE_TEMPLATE/MGMT-publish.yml vendored Normal file
View File

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

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

13
.github/ISSUE_TEMPLATE/task.yml vendored Normal file
View File

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

156
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,156 @@
Your task is to “onboard” this repository to the Copilot coding agent by adding this .github/copilot-instructions.md file. It gives the agent the minimum, durable context it needs to understand the project, validate changes, and open mergeable PRs.
These instructions are repository-wide and task-agnostic. If something here contradicts ad-hoc guidance in an issue or PR, prefer the ad-hoc guidance for that task.
<Goals>
- Keep generated PRs mergeable by aligning with repository conventions; build and packaging validation will be performed by GitHub Actions.
- Minimize broken shell or PowerShell steps by pinning prerequisites and the order of operations.
- Reduce unnecessary codebase exploration by pointing to the right files and patterns first.
</Goals>
<Limitations>
- Do not include issue-specific plans or debugging transcripts.
- Keep guidance concise (≈2 pages). Link to existing scripts or configs in-repo rather than inlining them.
- Localization policy: Base language is Chinese (Simplified). English is also maintained by the core team; all other languages are community-contributed via Crowdin. If a PR introduces new UI strings, only add the new strings to the Chinese resource file at src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx. Do not edit any other locale files (including English) in that PR; leave them untranslated.
</Limitations>
---
## High-level overview
- What this is. Snap Hutao (胡桃工具箱) is a C# WinUI 3 desktop application packaged as MSIX. It is an open-source toolkit that enhances the Genshin Impact experience on Windows without modifying the game client.
- Target OS. Runs on Windows 11 (22H2+) and Windows 10 22H2 with the January 2025 update. Agent work should assume a modern, fully updated Windows dev box.
- Key tech. The lastest .NET and Windows App SDK / WinUI 3 (XAML), GitHub Actions CI, analyzers such as StyleCop, and validation utilities such as Microsoft.VisualStudio.Validation (see usage policy below).
- License and scope. MIT. The app integrates official game data and community features; it must not introduce destructive client modifications.
Rule of thumb: Prefer Visual Studio 2022 for packaging or signing workflows. Use the .NET SDK CLI for restore, build, and test where practical.
---
## Repository layout (what to look for first)
- Top level
- .github — issue templates and workflows; this file lives here.
- res — assets and miscellaneous resources.
- src/Snap.Hutao — the solution root for the desktop app (primary code lives here).
- Build and CI configuration in the root (for example build.cake, NuGet.Config, CI YAML).
- Inside src/Snap.Hutao
- Solution: Snap.Hutao.sln (primary).
- Main app project: Snap.Hutao (WinUI 3 XAML and C#).
- Common folders you will work in:
- UI (XAML views, controls) and ViewModel (MVVM presentation).
- Service (application and business services; network, storage, background work).
- Model (domain and data models).
- Core and Extension (infrastructure, helpers, extensions).
- Web (HTTP or API integrations) and Win32 (interop).
- Important config:
- App.xaml and App.xaml.cs (app startup and resources), Package.appxmanifest (MSIX).
- .editorconfig, stylecop.json, GlobalUsing.cs, BannedSymbols.txt (coding standards and guardrails).
Search shortcuts: Package.appxmanifest for capabilities or identity, stylecop.json for style rules, GlobalUsing.cs for shared namespaces, *.xaml plus ViewModel for MVVM entry points.
---
## Branching and PR policy (follow strictly)
- Work on develop for feature and bug-fix branches; target PRs to develop.
- CI builds main, develop, and feat/* branches. Alpha artifacts may be produced by CI for testing.
- Link issues with closing keywords in PR descriptions when appropriate, for example `Fixes #123`.
---
## Prerequisites (Windows only)
1) Windows: Windows 11 22H2+ or Windows 10 22H2 with the latest updates. Enable Developer Mode.
2) Visual Studio 2022 with workloads:
- .NET desktop development
- Desktop development with C++
- Windows application development
3) MSIX tooling: Single-project MSIX Packaging Tools for Visual Studio 2022 if your VS installation does not include it.
4) SDK policy: Always target the latest .NET SDK. Do not downgrade SDK versions or LangVersion.
5) Optional runtime UX: WebView2 Runtime, Segoe Fluent Icons font, MSVC Runtime if required by features.
If the agent runs headless, prefer CLI restore, build, and test for validation and rely on CI to produce MSIX artifacts.
---
## Build, run, and validate (minimal, reliable sequence)
Use this order on a clean clone. Local build is recommended for rapid feedback but not required; GitHub Actions will perform the authoritative validation.
1) Restore
dotnet restore src/Snap.Hutao/Snap.Hutao.sln
2) Build (Debug)
dotnet build src/Snap.Hutao/Snap.Hutao.sln -c Debug
Expect WinUI 3 XAML compilation and analyzer checks. Fix analyzer violations before proposing changes.
3) Run for development
- Open src/Snap.Hutao/Snap.Hutao.sln in Visual Studio 2022.
- Set the main project as startup, select x64, press F5. Packaged debugging will register the app locally.
4) Package when needed
- Use Visual Studio Publish or Package for MSIX.
- CI may also produce Alpha packages for main, develop, or feat/* branches. Local install of CI-signed packages can require installing the provided certificate.
5) Tests (if present)
dotnet test src/Snap.Hutao/Snap.Hutao.sln
Add or update tests for non-UI logic when you change behavior.
Common validation before opening a PR:
- Solution builds cleanly and analyzer warnings are resolved per repository policy.
- App starts in Debug; smoke-test the feature you touched.
- Do not change Package.appxmanifest capabilities unless required by the task and documented in the PR.
---
## Coding style and contribution patterns
- MVVM: Put UI behavior in ViewModels. Use async APIs for I/O and keep the UI thread responsive.
- Consistency first: Mirror existing folder structure, naming, and patterns. Prefer existing services/helpers over new global/singleton patterns.
- Analyzers: Fix StyleCop and other analyzer diagnostics. Follow rules set in .editorconfig / stylecop.json (naming, docs, layout).
- Validation & errors: Use established guard/validation utilities; fail fast on bad inputs; handle network/storage exceptions gracefully.
- XAML: Reuse existing styles/resources; keep bindings simple and observable; avoid UI thread blocking.
- Security & privacy: Dont log secrets or game tokens; follow existing storage conventions.
---
## Localization and strings (enforced)
- Base language is Chinese (Simplified). English is maintained by the core team; other languages are translated by the community via Crowdin.
- When adding new UI strings in a PR, only add them to the Chinese resource file:
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
- Do not create or modify any other locale resource files (including English) in that PR; leave them untranslated. The core team and Crowdin will handle downstream updates.
- Prefer resource bindings over hard-coded strings. Reuse existing keys where possible; introduce new keys only when needed and name them consistently.
---
## Where to implement changes (quick map)
- New UI feature → UI (XAML) plus ViewModel (presentation) plus Service (logic or data) plus Model (types).
- Network or API work → Web (HTTP clients, DTOs) plus relevant Service.
- Interop → Win32 (P/Invoke or wrappers). Follow precedent conservatively to avoid regressions.
- Cross-cutting → Core and Extension for helpers, dependency injection, and reusable infrastructure.
---
## CI interaction (what the agent should expect)
- Pushes to feature branches trigger build checks; some branches produce Alpha artifacts.
- Build and packaging validation is performed by GitHub Actions and is the source of truth.
- If CI fails on analyzers or formatting, align code with the repositorys rules rather than suppressing diagnostics.
- Keep local steps minimal and deterministic; rely on CI for signing and distribution.
---
## Agent operating guidelines
- Trust this file first. Explore or run extra commands only if information is missing or build errors indicate a mismatch.
- Do not downgrade SDKs or language level. Avoid changing packaging or signing settings unless the task explicitly requires it.
- Keep edits scoped. Touch the smallest set of files; update tests or docs alongside code when behavior changes.
- PR hygiene. Use clear commit messages, link issues with keywords, and summarize the validation steps you performed such as build, tests, and manual smoke checks.
---

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/src/Snap.Hutao" # Snap.Hutao.csproj
target-branch: "develop"
schedule:
interval: "weekly"
groups:
packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/.github/workflows" # GitHub Workflows
schedule:
interval: "weekly"

15
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,15 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please check our contribution guide (https://hut.ao/en/development/contribute.html) and fill out 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

View File

@@ -0,0 +1,73 @@
name: PublishDistribution
on:
release:
types: [released]
workflow_dispatch:
jobs:
Publish:
runs-on: ubuntu-latest
steps:
# Purge Patch System Cache
- name: Purge Patch
env:
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
curl -X PATCH $PURGE_URL
- name: Overwrite CN patch mirrors
shell: pwsh
run: |
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/DGP-Studio/Snap.Hutao/releases/latest"
$asset = $latestRelease.assets[0]
$assetUrl = "https://ghproxy.qhy04.cc/" + $asset.browser_download_url
$tagName = $latestRelease.tag_name
Write-Output "Waiting Patch API to update"
while ($true) {
$patchData = Invoke-RestMethod -Uri "https://api.snapgenshin.com/patch/hutao"
$cachedVersion = $patchData.data.version.Substring(0, $patchData.data.version.Length - 2)
if ($cachedVersion -eq $tagName) {
break
}
Start-Sleep -Seconds 3
}
Write-Output "Add GitHub Proxy to Patch API"
$mirrorData = @{
key = "snap-hutao"
url = $assetUrl
mirror_name = "GitHub Proxy"
mirror_type = "direct"
} | ConvertTo-Json
$response1 = Invoke-WebRequest -Uri "https://api.snapgenshin.com/patch/mirror" `
-Method POST `
-Headers @{"API-Token" = "${{ secrets.OVERWRITE_TOKEN }}"} `
-Body $mirrorData `
-ContentType "application/json"
Write-Output $response1.Content
Write-Output "Add R2 to Patch API"
$r2Url = "https://hutao-dist.qhy04.cc/$($asset.name)"
$r2Data = @{
key = "snap-hutao"
url = $r2Url
mirror_name = "Cloudflare R2"
mirror_type = "direct"
} | ConvertTo-Json
$response2 = Invoke-WebRequest -Uri "https://api.snapgenshin.com/patch/mirror" `
-Method POST `
-Headers @{"API-Token" = "${{ secrets.OVERWRITE_TOKEN }}"} `
-Body $r2Data `
-ContentType "application/json"
Write-Output $response2.Content
- uses: benc-uk/workflow-dispatch@v1.2.4
with:
workflow: Build
repo: DGP-Studio/hutao-installer
ref: main
token: "${{ secrets.RUNNER_CHECK_TOKEN }}"
inputs: '{ "only-offline": true }'

139
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,139 @@
name: Snap Hutao Alpha
on:
workflow_dispatch:
push:
branches: [ develop ]
paths-ignore:
- '.gitattributes'
- '.github/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- '**.yml'
pull_request:
branches: [ develop ]
paths-ignore:
- '.gitattributes'
- '.github/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- '**.yml'
- '**.resx'
jobs:
select-runner:
runs-on: ubuntu-latest
outputs:
runner: ${{ steps.select_runner.outputs.runner }}
steps:
- name: Select runner
id: select_runner
shell: bash
env:
ORG: ${{ github.repository_owner }}
REPO: ${{ github.repository }}
TOKEN: ${{ secrets.RUNNER_CHECK_TOKEN }}
LABEL: ${{ vars.RUNNER_LABEL || 'sjc1' }}
run: |
set -o pipefail
FALLBACK="windows-latest"
get_status_by_label () {
local url="$1"
local json
json=$(curl -fsSL \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
"$url" 2>/dev/null || true)
jq -r --arg label "$LABEL" '
(.runners // [])
| map(select(any(.labels[]?; .name==$label)))
| .[0].status // ""' <<<"$json" 2>/dev/null || echo ""
}
# Check org's runner first and then repo's
STATUS="$(get_status_by_label "https://api.github.com/orgs/${ORG}/actions/runners?per_page=100")"
if [ -z "$STATUS" ]; then
STATUS="$(get_status_by_label "https://api.github.com/repos/${REPO}/actions/runners?per_page=100")"
fi
if [ "$STATUS" = "online" ]; then
PICK="$LABEL"
else
PICK="$FALLBACK"
fi
echo "Selected runner: $PICK (label=$LABEL, status=${STATUS:-unknown})"
echo "runner=$PICK" >> "$GITHUB_OUTPUT"
build:
needs: select-runner
runs-on: ${{ needs.select-runner.outputs.runner }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup .NET (self-hosted)
if: ${{ needs.select-runner.outputs.runner == 'sjc1' }}
shell: pwsh
run: |
Write-Host "Running environment setup script…"
Invoke-WebRequest `
-Uri 'https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.DevelopEnvironment.Setup/refs/heads/main/setup.ps1' `
-OutFile 'setup.ps1' `
-UseBasicParsing
Write-Host "Executing setup.ps1"
.\setup.ps1
- name: Setup .NET
if: ${{ needs.select-runner.outputs.runner == 'windows-latest' }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0
- name: Cache NuGet packages
if: ${{ needs.select-runner.outputs.runner == 'windows-latest' }}
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Cake
id: cake
shell: pwsh
run: dotnet tool restore && dotnet cake
env:
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }}
- name: Upload signed msix
if: success() && github.event_name != 'pull_request'
uses: actions/upload-artifact@v5
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` 测试版本,包含已经基本完工的新功能及问题修复
> [!IMPORTANT]
> 请先安装 **[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
- name: Clean up
shell: pwsh
run: |
Write-Host "Cleaning up NuGet cache..."
Remove-Item -Recurse -Force "$env:USERPROFILE\.nuget\packages"
Write-Host "NuGet cache cleaned."

97
.github/workflows/canary.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Snap Hutao Canary
on:
workflow_dispatch:
push:
branches-ignore:
- l10n_develop
- main
- release
- dependabot/**
paths-ignore:
- '.gitattributes'
- '.github/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- '**.yml'
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: develop
fetch-depth: 0
- name: Merge all branches into develop locally
id: merge
run: |
$continue = $true
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin '+refs/heads/*:refs/remotes/origin/*'
git checkout origin/develop
$response = curl -s https://api.github.com/repos/DGP-Studio/Snap.Hutao/pulls?state=open
$refs = $response | ConvertFrom-Json | Where-Object { $_.draft -eq $false } | ForEach-Object { $_.head.ref }
if ($refs.Count -eq 0 -or ($refs.Count -eq 1 -and $refs -eq "l10n_develop")) {
echo "No PRs to merge"
$continue = $false
echo "continue=$continue" >> $Env:GITHUB_OUTPUT
exit
}
foreach ($ref in $refs) {
echo "Merging $ref into develop"
git merge "origin/$ref" --strategy=ort --allow-unrelated-histories -m "Merge $ref into develop"
}
echo "continue=$continue" >> $Env:GITHUB_OUTPUT
- name: Setup .NET
if: ${{ steps.merge.outputs.continue == 'true' }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0
- name: Cache NuGet packages
if: ${{ steps.merge.outputs.continue == 'true' }}
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Snap.Hutao.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Cake
if: ${{ steps.merge.outputs.continue == 'true' }}
id: cake
shell: pwsh
run: dotnet tool restore && dotnet cake
env:
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }}
- name: Upload signed msix
if: ${{ success() && steps.merge.outputs.continue == 'true' }}
uses: actions/upload-artifact@v5
with:
name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix
- name: Add summary
if: ${{ success() && steps.merge.outputs.continue == 'true' }}
shell: pwsh
run: |
$summary = "
> [!WARNING]
> 该版本是由 CI 程序自动打包生成的 `Canary` 测试版本,包含新功能原型及问题修复
> [!IMPORTANT]
> 请先安装 **[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 Normal file
View File

@@ -0,0 +1,16 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
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 Normal file
View File

@@ -0,0 +1,20 @@
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

View File

@@ -0,0 +1,26 @@
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

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
desktop.ini
*.csproj.user
*.DotSettings.user
.vs/
.idea/
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.Benchmark/bin/
src/Snap.Hutao/Snap.Hutao.Benchmark/obj/
src/Snap.Hutao/Snap.Hutao.Test/bin/
src/Snap.Hutao/Snap.Hutao.Test/obj/
src/Snap.Hutao/Snap.Hutao/Properties/PublishProfiles/FolderProfile.pubxml.user
src/Snap.Hutao/Snap.Hutao/Generated Files/
tools/

77
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,77 @@
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"

20
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"MicroPython.executeButton": [
{
"text": "▶",
"tooltip": "运行",
"alignment": "left",
"command": "extension.executeFile",
"priority": 3.5
}
],
"MicroPython.syncButton": [
{
"text": "$(sync)",
"tooltip": "同步",
"alignment": "left",
"command": "extension.execute",
"priority": 4
}
]
}

13
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,13 @@
# Code of Conduct
> Snap Hutao is adapting the following rules to keep the community safety.
When participating in our open source community, we want all members to respect and support each other. To ensure the comfort and safety of our community members, we have established the following code of conduct:
1. Respect diversity and inclusivity. We welcome people from different countries, regions, genders, sexual orientations, abilities, religions, and cultural backgrounds to participate in our community, and we encourage respect for all differences.
2. Prohibit discrimination and harassment. We do not tolerate any form of discrimination, harassment, personal attacks, or insults. This includes but is not limited to race, gender, sexual orientation, age, religion, nationality, cultural background, physical and mental health status.
3. Respect privacy and personal information. We protect the privacy and personal information of community members and prohibit the public disclosure of any private information. If you need to disclose certain information, please make sure you have obtained the relevant person's permission.
4. Keep honesty and transparency. We expect community members to maintain honesty and transparency and not intentionally mislead or deceive others.
5. Respect community rules and other members. We encourage community members to follow community rules and guidelines and maintain a polite and respectful attitude towards other members. If you find that other members are violating community rules, please report it to community administrators or organizers in a timely manner.
The above is our community's code of conduct, and we expect all community members to abide by these rules. We will actively address behaviors that violate these rules. We believe that through mutual respect and support, we can build a friendly, inclusive, and beneficial open source community.

74
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,74 @@
# Contribution Guide
## Contribute Your Code
### 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.
### Start Pull Request
- All code-related changes from authors' own branches are only allowed be merged to `develop` branch
- Please use [keywords](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link your PR or commits with issues, so issues can be automatically closed once commits are merged into `main` branch.
### Test Binary Package
Once the code in updated in `develop` and `main` branches, an Azure Pipeline CI script will build the latest code to `Snap Hutao Alpha` package. Once the package is built, it will be released on [GitHub Release page](https://github.com/DGP-Studio/Snap.Hutao/releases) as a pre-released package.
You need to install [Snap.Hutao.CI.cer](https://github.com/DGP-Studio/Snap.Hutao/releases/download/2023.10.3.1/Snap.Hutao.CI.cer) certificate to your local machine, and then install the msix package in the release.
*If the latest release does not contains attached file, that means package is still in uploading process.
## Start New Issue
To help users solve problems faster and increase developers' efficiency in solving problems, Snap Hutao provides detailed documentation to explain common problems and issue templates to guide users to report program problems by submitting issues.
Before submitting a new issue, you should check the following pages:
- [FAQ](https://hut.ao/advanced/FAQ.html) Document
- [Common Program Exceptions ](https://hut.ao/en/advanced/exceptions.html)Document
- [Current Opened BUG Report Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG)
When starting a new issue, please use the issue templates:
- Describe your issue in details to help developers to reproduce the issue
- Your description of reproduction should be a step-by-step story
- If your issue is about program crash
- Remember to provide your Device ID
- Check Windows Event Viewer, and attach associated `.NET Error` details in the issue body
## Document Modification
Snap Hutao Document site is stored in repository [DGP-Studio/Snap.Hutao.Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs), you can process the following steps to test the site in your local device:
1. Download and install [NodeJS 18](https://nodejs.org/en/download/)
2. Clone the repository
3. Run `npm install` in the root directory of the document project
4. Run `npm run docs:dev` to start test on 8080 port
### Localization
Snap.Hutao.Docs project structure is designed as multiple languages site. Each language has its independent folder under `docs` directory.
**If you wish to add another language document, you can [start an issue in document repository](https://github.com/DGP-Studio/Snap.Hutao.Docs/issues) to ask developer to setup an environment for you, or you can process the following steps by yourself:**
1. make a copy of `zh` folder, rename the new folder as the new language's code
2. Start your translation work in the new language folder
3. In `docs/.vuepress/sidebar` folder, duplicate `zh.ts` file
1. Rename the file to `{language_code}.ts`
2. In the line 4, change `/zh/` to `/{language_code}/`
3. Translate all `text` field
4. In `docs/.vuepress/navbar` folder, duplicate `zh.ts` file
1. Rename the file to `{language_code}.ts`
2. Replace all `/zh/` to `/{language_code}/`
3. Translate all `text` field
5. In `docs/.vuepress/config.ts`file, add your language information in `locales` and `plugins/docsearchPlugin/locales` dictionary
6. In `docs/.vuepress/theme.ts`file, add your language information in `locales` dictionary

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 DGP Studio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
NuGet.Config Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="Microsoft CsWin32" value="https://pkgs.dev.azure.com/azure-public/winsdk/_packaging/CI/nuget/v3/index.json" />
<add key="CommunityToolkit-MainLatest" value="https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-MainLatest/nuget/v3/index.json" />
<add key="CommunityToolkit-Labs" value="https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json" />
</packageSources>
<packageRestore>
<add key="enabled" value="True" />
<add key="automatic" value="True" />
</packageRestore>
<bindingRedirects>
<add key="skip" value="False" />
</bindingRedirects>
<packageManagement>
<add key="format" value="1" />
<add key="disabled" value="False" />
</packageManagement>
<auditSources>
<clear/>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</auditSources>
</configuration>

144
README.md Normal file
View File

@@ -0,0 +1,144 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/976e057c-f01e-486b-9fa0-04744ae96f99" alt="Snap Hutao Banner" width="600"/>
</p>
<h1 align="center">Snap Hutao</h1>
<p align="center">
🎮 开源的原神工具箱,专为 Windows 平台设计,改善桌面端玩家的游戏体验
<br/>
🎮 An open-source Genshin Impact toolkit for Windows, designed to improve the desktop gaming experience
</p>
<div align="center">
<table>
<tr>
<td align="center" style="padding:0 10px;">
<b>Latest CI/CD Build</sub>
</td>
<td align="center" style="padding:0 10px;">
<b>Latest Release</sub>
</td>
<td align="center" style="padding:0 10px;">
<b>Downloads</sub>
</td>
</tr>
<tr>
<td align="center" style="padding:0 10px;">
<a href="https://ci.appveyor.com/project/DGP-Studio/snap-hutao">
<img src="https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true" alt="Build Status"/>
</a>
</td>
<td align="center" style="padding:0 10px;">
<a href="https://github.com/DGP-Studio/Snap.Hutao/releases/latest">
<img src="https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat" alt="Release"/>
</a>
</td>
<td align="center" style="padding:0 10px;">
<img src="https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat" alt="Downloads"/>
</td>
</tr>
</table>
</div>
---
## 📖 简介 / Introduction
**中文**
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
**English**
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players.
---
## 🚀 安装 / Installation
**中文**
你可以按照 [快速开始](https://hut.ao/zh/quick-start.html) 文档中提供的流程安装并设置 Snap Hutao。
**English**
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
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project.
| Language | Status |
|----------|--------|
| zh-TW | [![zh-TW](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.zh-TW&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| en | [![en](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.en&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| fr | [![fr](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.fr&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| id | [![id](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.id&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| ja | [![ja](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.ja&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| ko | [![ko](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.ko&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| pt-PT | [![pt-PT](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.pt-PT&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| ru | [![ru](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.ru&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
| vi | [![vi](https://img.shields.io/badge/dynamic/json?color=blue&label=vi&style=flat&logo=crowdin&query=%24.vi&url=https%3A%2F%2Fawesome-crowdin-proxy.qhy040404.workers.dev%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) |
---
## 🛠️ 贡献 / Contribute
- [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
- [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
- [通过 DeepWiKi 了解项目结构 / Understand Project Structure with DeepWiKi](https://deepwiki.com/DGP-Studio/Snap.Hutao)
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/DGP-Studio/Snap.Hutao)
---
## 🙏 特别感谢 / Special Thanks
- [HolographicHat](https://github.com/HolographicHat)
- [UIGF organization](https://uigf.org)
**特定的原神项目 / Specific Genshin-related Projects**
- [Scighost/Starward](https://github.com/Scighost/Starward)
---
## ⚙️ 使用的技术栈 / 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)
- [dotnet/efcore](https://github.com/dotnet/efcore)
- [dotnet/runtime](https://github.com/dotnet/runtime)
- [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
- [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
- [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
- [quartznet/quartznet](https://github.com/quartznet/quartznet)
---
## ❤️ 赞助商 / Sponsorship
Snap Hutao is currently using sponsored software from the following service providers.
<img src="./res/assets/readmeSponsors.svg" alt="Readme Sponsors" />
- 🏠 [Netlify](https://www.netlify.com/) provides document and home page hosting service for Snap Hutao
- 🌍 [Crowdin](https://crowdin.com/) provides its SaaS platform to help Snap Hutao's localization
- 🗄️ [Navicat](https://navicat.com/) provides Snap Hutao with advanced database management tools
- 🔒 Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
- 🔑 [1Password](https://1password.com/) provides Snap Hutao development team with their amazing password management software
- 🐳 [DigitalOcean](https://www.digitalocean.com) provides reliable cloud database and container service for Snap Hutao database backup
- 📊 [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
- ☁️ [Cloudflare](https://www.cloudflare.com/) sponsors Snap Hutao with their Business Plan, ensuring secure, fast, and reliable worldwide connection to our infrastructure
- 🔐 [Termius](https://termius.com) provides a secure, reliable, and collaborative SSH client
---
## 📈 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
[![](https://opengraph.snapgenshin.cn/gitcode?repo=DGP-Studio/Snap.Hutao)](https://github.com/DGP-Studio/Snap.Hutao)

12
SECURITY.md Normal file
View File

@@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| >=1.9.0 | :white_check_mark: |
| <1.9.0 | :x: |
## Reporting a Vulnerability
Please [open an issue](https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose)

27
appveyor.yml Normal file
View File

@@ -0,0 +1,27 @@
version: 1.0.{build}
branches:
only:
- "release"
build_cloud: GCE
image: Visual Studio 2022
cache:
- 'C:\Users\Public\Documents\dotnet_install\.nuget\packages'
clone_depth: 3
clone_folder: C:\Users\Public\appveyor\Snap.Hutao
install:
- ps: |
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -UseBasicParsing -OutFile "$env:temp\dotnet-install.ps1"
& $env:temp\dotnet-install.ps1 -Architecture x64 -Channel '10.0' -InstallDir "$env:ProgramFiles\dotnet"
- pwsh: dotnet tool restore
before_build:
- cmd: dotnet --version
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==

351
build.cake Normal file
View File

@@ -0,0 +1,351 @@
#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";
var pfxPath = "pfxPath";
var pw = "pw";
// Extensions
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", "net10.0-windows10.0.26100.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
{
if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Alpha")
{
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 }
}
}
);
}
else if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Canary")
{
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
}
else
{
throw new Exception("Unsupported workflow.");
}
var certificateBase64 = HasEnvironmentVariable("CERTIFICATE") ? EnvironmentVariable("CERTIFICATE") : throw new Exception("Cannot find CERTIFICATE");
pw = HasEnvironmentVariable("PW") ? EnvironmentVariable("PW") : throw new Exception("Cannot find PW");
pfxPath = System.IO.Path.Combine(repoDir, "temp.pfx");
System.IO.File.WriteAllBytes(pfxPath, System.Convert.FromBase64String(certificateBase64));
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}");
}
// Windows SDK
var registry = new WindowsRegistry();
var winsdkRegistry = registry.LocalMachine.OpenKey(@"SOFTWARE\Microsoft\Windows Kits\Installed Roots");
var winsdkVersion = winsdkRegistry.GetSubKeyNames().MaxBy(key => int.Parse(key.Split(".")[2]));
var winsdkPath = (string)winsdkRegistry.GetValue("KitsRoot10");
var winsdkBinPath = System.IO.Path.Combine(winsdkPath, "bin", winsdkVersion, "x64");
Information($"Windows SDK: {winsdkPath}");
Task("Build")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.IsDependentOn("Remove unused files")
.IsDependentOn("Build MSIX")
.IsDependentOn("Sign");
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");
if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Alpha")
{
Information("Using Alpha configuration");
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\"");
}
else if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Canary")
{
Information("Using Canary configuration");
content = content
.Replace("Snap Hutao", "Snap Hutao Canary")
.Replace("胡桃", "胡桃 Canary")
.Replace("DGP Studio", "DGP Studio CI");
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"52127695-c6a7-406e-916a-693b905e8ba7\"");
}
else
{
throw new Exception("Unsupported workflow.");
}
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", GitHubActions.IsRunningOnGitHubActions && GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Alpha")
.AppendIf("/p:CanaryConstants=IS_CANARY_BUILD", GitHubActions.IsRunningOnGitHubActions && GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Canary")
};
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("Remove unused files")
.IsDependentOn("Build binary package")
.Does(() =>
{
Information("Removing unused files...");
Information("Removing xbf...");
System.IO.File.Delete(System.IO.Path.Combine(binPath, "App.xbf"));
Information("Removing appxrecipe...");
System.IO.File.Delete(System.IO.Path.Combine(binPath, "Snap.Hutao.build.appxrecipe"));
});
Task("Build MSIX")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.IsDependentOn("Remove unused files")
.Does(() =>
{
var arguments = "arguments";
if (GitHubActions.IsRunningOnGitHubActions)
{
if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Alpha")
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
}
else if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Canary")
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Canary-{version}.msix");
}
else
{
throw new Exception("Unsupported workflow.");
}
}
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 makeappxPath = System.IO.Path.Combine(winsdkBinPath, "makeappx.exe");
var p = StartProcess(
makeappxPath,
new ProcessSettings
{
Arguments = arguments
}
);
if (p != 0)
{
throw new InvalidOperationException("Build MSIX failed with exit code " + p);
}
});
Task("Sign")
.IsDependentOn("Build MSIX")
.Does(() =>
{
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Move to SignPath. Skip signing.");
return;
}
else if (GitHubActions.IsRunningOnGitHubActions)
{
if (GitHubActions.Environment.PullRequest.IsPullRequest)
{
Information("Is Pull Request. Skip signing.");
return;
}
var signPath = System.IO.Path.Combine(winsdkBinPath, "signtool.exe");
var arguments = "arguments";
if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Alpha")
{
arguments = $"sign /debug /v /a /fd SHA256 /f {pfxPath} /p {pw} {System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix")}";
}
else if (GitHubActions.Environment.Workflow.Workflow == "Snap Hutao Canary")
{
arguments = $"sign /debug /v /a /fd SHA256 /f {pfxPath} /p {pw} {System.IO.Path.Combine(outputPath, $"Snap.Hutao.Canary-{version}.msix")}";
}
else
{
throw new Exception("Unsupported workflow.");
}
var p = StartProcess(
signPath,
new ProcessSettings
{
Arguments = arguments
}
);
if (p != 0)
{
throw new InvalidOperationException("Sign failed with exit code " + p);
}
}
else
{
Information("Local configuration. Skip signing.");
return;
}
});
RunTarget(target);

5
crowdin.yml Normal file
View File

@@ -0,0 +1,5 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.%osx_locale%.resx

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

3
res/HutaoLogo/README.md Normal file
View File

@@ -0,0 +1,3 @@
本文件夹中的所有图片,均由 [DGP Studio](https://github.com/DGP-Studio) 委托 [Bilibili 画画的芦苇](https://space.bilibili.com/274422134) 绘制
Copyright © 2023 DGP Studio, All Rights Reserved.

BIN
res/HutaoLogo2/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

BIN
res/HutaoLogo2/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

BIN
res/HutaoLogo2/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

BIN
res/HutaoLogo2/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

1
res/HutaoLogo2/README.md Normal file
View File

@@ -0,0 +1 @@
Copyright © 2025 DGP Studio, All Rights Reserved.

BIN
res/Store/chs/abyss.psd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/Store/chs/lancher.psd Normal file

Binary file not shown.

Binary file not shown.

BIN
res/Store/chs/wish.psd Normal file

Binary file not shown.

BIN
res/Store/en/abyss.psd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/Store/en/lancher.psd Normal file

Binary file not shown.

Binary file not shown.

BIN
res/Store/en/wish.psd Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,200 @@
[*]
# General
charset = utf-8
end_of_line = crlf
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
[*.cs]
# General
indent_size = 4
tab_width = 4
# C# Specific
csharp_indent_labels = one_less_than_current
csharp_prefer_braces = true:silent
csharp_prefer_simple_default_expression = false:suggestion
csharp_prefer_simple_using_statement = false:suggestion
csharp_prefer_static_local_function = false:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_space_around_binary_operators = before_and_after
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
csharp_style_allow_embedded_statements_on_same_line_experimental = false:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = false:silent
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = false:silent
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_namespace_declarations = file_scoped:silent
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_parameter_null_checking = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_primary_constructors = false:none
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:silent
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_var_elsewhere = false:warning
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_using_directive_placement = outside_namespace:silent
# .NET Specific
dotnet_code_quality_unused_parameters = non_public:suggestion
dotnet_style_allow_multiple_blank_lines_experimental = false:silent
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_prefer_auto_properties = false:silent
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_diagnostic.IDE0005.severity = warning # IDE0005: 删除不必要的 using 指令
dotnet_diagnostic.IDE0060.severity = none # IDE0060: 删除未使用的参数
dotnet_diagnostic.IDE0290.severity = none
dotnet_diagnostic.CA1000.severity = suggestion # CA1000: 不要在泛型类型中声明静态成员
dotnet_diagnostic.CA1001.severity = warning # CA1001: 具有可释放字段的类型应该是可释放的
dotnet_diagnostic.CA1008.severity = suggestion # CA1008: 枚举应具有零值
dotnet_diagnostic.CA1010.severity = suggestion # CA1010: 还应实现泛型接口
dotnet_diagnostic.CA1012.severity = suggestion # CA1012: 抽象类型不应具有公共构造函数
dotnet_diagnostic.CA1024.severity = suggestion # CA1024: 在适用处使用属性
dotnet_diagnostic.CA1034.severity = suggestion # CA1034: 嵌套类型应不可见
dotnet_diagnostic.CA1036.severity = suggestion # CA1036: 重写可比较类型中的方法
dotnet_diagnostic.CA1040.severity = suggestion # CA1040: 避免使用空接口
dotnet_diagnostic.CA1043.severity = suggestion # CA1043: 将整型或字符串参数用于索引器
dotnet_diagnostic.CA1044.severity = suggestion # CA1044: 属性不应是只写的
dotnet_diagnostic.CA1046.severity = suggestion # CA1046: 不要对引用类型重载相等运算符
dotnet_diagnostic.CA1051.severity = suggestion # CA1051: 不要声明可见实例字段
dotnet_diagnostic.CA1052.severity = suggestion # CA1052: 静态容器类型应为 Static 或 NotInheritable
dotnet_diagnostic.CA1058.severity = suggestion # CA1058: 类型不应扩展某些基类型
dotnet_diagnostic.CA1063.severity = suggestion # CA1063: 正确实现 IDisposable
dotnet_diagnostic.CA1065.severity = suggestion # CA1065: 不要在意外的位置引发异常
dotnet_diagnostic.CA1066.severity = suggestion # CA1066: 重写 Object.Equals 时实现 IEquatable
dotnet_diagnostic.CA1304.severity = suggestion # CA1304: 指定 CultureInfo
dotnet_diagnostic.CA1305.severity = suggestion # CA1305: 指定 IFormatProvider
dotnet_diagnostic.CA1307.severity = suggestion # CA1307: 为了清晰起见,请指定 StringComparison
dotnet_diagnostic.CA1308.severity = suggestion # CA1308: 将字符串规范化为大写
dotnet_diagnostic.CA1309.severity = suggestion # CA1309: 使用序数字符串比较
dotnet_diagnostic.CA1310.severity = suggestion # CA1310: 为了确保正确,请指定 StringComparison
dotnet_diagnostic.CA1501.severity = suggestion # CA1501: 避免过度继承
dotnet_diagnostic.CA1502.severity = suggestion # CA1502: 避免过度复杂性
dotnet_diagnostic.CA1505.severity = suggestion # CA1505: 避免使用无法维护的代码
dotnet_diagnostic.CA1506.severity = suggestion # CA1506: 避免过度的类耦合
dotnet_diagnostic.CA1508.severity = none # CA1508: 避免死条件代码
dotnet_diagnostic.CA1805.severity = suggestion # CA1805: 避免进行不必要的初始化
dotnet_diagnostic.CA1810.severity = suggestion # CA1810: 以内联方式初始化引用类型的静态字段
dotnet_diagnostic.CA1813.severity = suggestion # CA1813: 避免使用非密封特性
dotnet_diagnostic.CA1814.severity = suggestion # CA1814: 与多维数组相比,首选使用交错数组
dotnet_diagnostic.CA1819.severity = suggestion # CA1819: 属性不应返回数组
dotnet_diagnostic.CA1820.severity = suggestion # CA1820: 使用字符串长度测试是否有空字符串
dotnet_diagnostic.CA1823.severity = suggestion # CA1823: 避免未使用的私有字段
dotnet_diagnostic.CA1849.severity = suggestion # CA1849: 当在异步方法中时,调用异步方法
dotnet_diagnostic.CA1852.severity = suggestion # CA1852: 密封内部类型
dotnet_diagnostic.CA1859.severity = none # CA1859: 尽可能使用具体类型来提高性能
dotnet_diagnostic.CA2000.severity = none # CA2000: 丢失范围之前释放对象
dotnet_diagnostic.CA2002.severity = suggestion # CA2002: 不要锁定具有弱标识的对象
dotnet_diagnostic.CA2007.severity = suggestion # CA2007: 考虑对等待的任务调用 ConfigureAwait
dotnet_diagnostic.CA2008.severity = suggestion # CA2008: 不要在未传递 TaskScheduler 的情况下创建任务
dotnet_diagnostic.CA2100.severity = suggestion # CA2100: 检查 SQL 查询是否存在安全漏洞
dotnet_diagnostic.CA2109.severity = suggestion # CA2109: 检查可见的事件处理程序
dotnet_diagnostic.CA2119.severity = suggestion # CA2119: 密封满足私有接口的方法
dotnet_diagnostic.CA2153.severity = suggestion # CA2153: 不要捕获损坏状态异常
dotnet_diagnostic.CA2201.severity = suggestion # CA2201: 不要引发保留的异常类型
dotnet_diagnostic.CA2207.severity = suggestion # CA2207: 以内联方式初始化值类型的静态字段
dotnet_diagnostic.CA2213.severity = suggestion # CA2213: 应释放可释放的字段
dotnet_diagnostic.CA2214.severity = suggestion # CA2214: 不要在构造函数中调用可重写的方法
dotnet_diagnostic.CA2215.severity = suggestion # CA2215: Dispose 方法应调用基类释放
dotnet_diagnostic.CA2216.severity = suggestion # CA2216: 可释放类型应声明终结器
dotnet_diagnostic.CA2227.severity = suggestion # CA2227: 集合属性应为只读
dotnet_diagnostic.CA2251.severity = suggestion # CA2251: 使用 “string.Equals”
dotnet_diagnostic.CS1591.severity = none # CS1591: 缺少对公共可见类型或成员的 XML 注释
dotnet_diagnostic.SA0001.severity = none # SA0001: XML comment analysis disabled
dotnet_diagnostic.SA1101.severity = none # SA1101: Prefix local calls with this
dotnet_diagnostic.SA1124.severity = none # SA1124: Do not use regions
dotnet_diagnostic.SA1208.severity = none # SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1404.severity = none # SA1404: Code analysis suppression should have justification
dotnet_diagnostic.SA1405.severity = none # SA1405: Debug.Assert should provide message text
dotnet_diagnostic.SA1600.severity = none # SA1600: Elements should be documented
dotnet_diagnostic.SA1601.severity = none # SA1601: Partial elements should be documented
dotnet_diagnostic.SA1602.severity = none # SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1623.severity = none # SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1629.severity = none # SA1629: Documentation text should end with a period
dotnet_diagnostic.SA1642.severity = none # SA1642: Constructor summary documentation should begin with standard text

20
src/Snap.Hutao/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"MicroPython.executeButton": [
{
"text": "▶",
"tooltip": "运行",
"alignment": "left",
"command": "extension.executeFile",
"priority": 3.5
}
],
"MicroPython.syncButton": [
{
"text": "$(sync)",
"tooltip": "同步",
"alignment": "left",
"command": "extension.execute",
"priority": 4
}
]
}

11
src/Snap.Hutao/.vsconfig Normal file
View File

@@ -0,0 +1,11 @@
{
"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"
]
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Values xmlns="urn:tom-englert.de/Configuration/1/0">
<Value Key="NeutralResourcesLanguage">zh-Hans</Value>
<Value Key="SortFileContentOnSave">True</Value>
<Value Key="ResXSortingComparison">Ordinal</Value>
</Values>

View File

@@ -0,0 +1,25 @@
name: Build and Publish NuGet Package
on:
push:
branches:
- main
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0
- name: Pack
run: dotnet pack --configuration Release src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration.csproj
- name: Publish to NuGet
run: dotnet nuget push src/Snap.Hutao.SourceGeneration/Snap.Hutao.SourceGeneration/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}

View File

@@ -0,0 +1,3 @@
src/Snap.Hutao.SourceGeneration/.idea/
*.user

View File

@@ -0,0 +1,10 @@
# Snap.Hutao.SourceGeneration
Source Code Generator for Snap.Hutao
# Development Guideline
Use https://roslynquoter.azurewebsites.net/ to get SyntaxTree
1. Every `IncrementalValue(s)Provider<T>`'s step result should be an `IEquatable<T>` to make it really becomes incremental.
2. So the intermediate models should be a `record (class/struct)` if possible
3. Intermediate array/enumerable should be a `ImmutableArray<T>` if possible, the pipeline use IA internally and has special check for it.

View File

@@ -0,0 +1,3 @@
.vs
Snap.Hutao.SourceGeneration/bin
Snap.Hutao.SourceGeneration/obj

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{88EDFA23-56FD-4C43-9C1A-EB1B9E9AE723}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{88EDFA23-56FD-4C43-9C1A-EB1B9E9AE723}.Debug|x64.ActiveCfg = Debug|Any CPU
{88EDFA23-56FD-4C43-9C1A-EB1B9E9AE723}.Debug|x64.Build.0 = Debug|Any CPU
{88EDFA23-56FD-4C43-9C1A-EB1B9E9AE723}.Release|x64.ActiveCfg = Release|Any CPU
{88EDFA23-56FD-4C43-9C1A-EB1B9E9AE723}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5D24877-D905-43A2-BDEA-C2901AF163BE}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,315 @@
// 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.Extension;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Runtime.CompilerServices;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
[assembly:InternalsVisibleTo("Snap.Hutao.SourceGeneration.Test")]
namespace Snap.Hutao.SourceGeneration;
[Generator(LanguageNames.CSharp)]
internal sealed class AttributeGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(GenerateAllAttributes);
}
public static void GenerateAllAttributes(IncrementalGeneratorPostInitializationContext context)
{
CompilationUnitSyntax coreAnnotation = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Core.Annotation")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ClassDeclaration(Identifier("CommandAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsMethod, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ConstructorDeclaration(Identifier("CommandAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(StringType, Identifier("commandName")))))
.WithEmptyBlockBody(),
ConstructorDeclaration(Identifier("CommandAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(StringType, Identifier("commandName")),
Parameter(StringType, Identifier("canExecuteName"))
])))
.WithEmptyBlockBody(),
PropertyDeclaration(BoolType, Identifier("AllowConcurrentExecutions"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
])),
ClassDeclaration(Identifier("GeneratedConstructorAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsConstructor, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ConstructorDeclaration(Identifier("GeneratedConstructorAttribute"))
.WithModifiers(PublicTokenList)
.WithEmptyBlockBody(),
PropertyDeclaration(BoolType, Identifier("CallBaseConstructor"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(BoolType, Identifier("InitializeComponent"))
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
.WithAccessorList(GetAndSetAccessorList)
])),
ClassDeclaration(Identifier("BindableCustomPropertyProviderAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsClass, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList),
ClassDeclaration(Identifier("DependencyPropertyAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsClass, allowMultiple: true, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithTypeParameterList(TypeParameterList(SingletonSeparatedList(
TypeParameter(Identifier("T")))))
.WithBaseList(SystemAttributeBaseList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ConstructorDeclaration(Identifier("DependencyPropertyAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(StringType, Identifier("name"))
])))
.WithEmptyBlockBody(),
PropertyDeclaration(BoolType, Identifier("IsAttached"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(NullableType(TypeOfSystemType), Identifier("TargetType"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(NullableObjectType, Identifier("DefaultValue"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(NullableStringType, Identifier("CreateDefaultValueCallbackName"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(NullableStringType, Identifier("PropertyChangedCallbackName"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(BoolType, Identifier("NotNull"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
])),
ClassDeclaration(Identifier("FieldAccessorAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsProperty, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
]))))
.NormalizeWhitespace();
context.AddSource("Snap.Hutao.Core.Annotation.Attributes.g.cs", coreAnnotation.ToFullStringWithHeader());
TypeSyntax typeOfHttpClientConfiguration = ParseTypeName("global::Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration");
CompilationUnitSyntax coreDependencyInjectionAnnotationHttpClient = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ClassDeclaration(Identifier("HttpClientAttribute"))
.WithAttributeLists(List(
[
JetBrainsAnnotationsMeansImplicitUseAttributeList,
SystemAttributeUsageList(AttributeTargetsClass, inherited: false)
]))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ConstructorDeclaration(Identifier("HttpClientAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeOfHttpClientConfiguration, Identifier("configuration")))))
.WithEmptyBlockBody(),
ConstructorDeclaration(Identifier("HttpClientAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeOfHttpClientConfiguration, Identifier("configuration")),
Parameter(TypeOfSystemType, Identifier("serviceType"))
])))
.WithEmptyBlockBody()
])),
ClassDeclaration(Identifier("PrimaryHttpMessageHandlerAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsClass, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(List<MemberDeclarationSyntax>(
[
PropertyDeclaration(IntType, "MaxAutomaticRedirections")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(IntType, "MaxConnectionsPerServer")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(IntType, "MaxResponseDrainSize")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(IntType, "MaxResponseHeadersLength")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: MeterFactory, PlaintextStreamFilter, PooledConnectionIdleTimeout, PooledConnectionLifetime
// Unsupported: Proxy, RequestHeaderEncodingSelector, ResponseDrainTimeout, ResponseHeaderEncodingSelector
// Unsupported: SslOptions
PropertyDeclaration(BoolType, "PreAuthenticate")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: KeepAlivePingTimeout
PropertyDeclaration(ParseTypeName("global::System.Net.Http.HttpKeepAlivePingPolicy"), "KeepAlivePingPolicy")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: KeepAlivePingDelay, ActivityHeadersPropagator
PropertyDeclaration(BoolType, "AllowAutoRedirect")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(ParseTypeName("global::System.Net.DecompressionMethods"), "AutomaticDecompression")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: ConnectCallback
PropertyDeclaration(BoolType, "UseCookies")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: CookieContainer, ConnectTimeout, DefaultProxyCredentials
PropertyDeclaration(BoolType, "EnableMultipleHttp2Connections")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
PropertyDeclaration(BoolType, "EnableMultipleHttp3Connections")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: Expect100ContinueTimeout
PropertyDeclaration(IntType, "InitialHttp2StreamWindowSize")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList),
// Unsupported: Credentials
PropertyDeclaration(BoolType, "UseProxy")
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList)
]))
]))))
.NormalizeWhitespace();
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.Attributes.g.cs", coreDependencyInjectionAnnotationHttpClient.ToFullStringWithHeader());
CompilationUnitSyntax coreDependencyInjectionAnnotation = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection.Annotation")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ClassDeclaration(Identifier("ServiceAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsClass, inherited: false)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList).WithMembers(List<MemberDeclarationSyntax>(
[
ConstructorDeclaration(Identifier("ServiceAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(TypeOfMicrosoftExtensionsDependencyInjectionServiceLifetime, Identifier("serviceLifetime")))))
.WithEmptyBlockBody(),
ConstructorDeclaration(Identifier("ServiceAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(TypeOfMicrosoftExtensionsDependencyInjectionServiceLifetime, Identifier("serviceLifetime")),
Parameter(TypeOfSystemType, Identifier("serviceType"))
])))
.WithEmptyBlockBody(),
PropertyDeclaration(NullableObjectType, Identifier("Key"))
.WithModifiers(PublicTokenList)
.WithAccessorList(GetAndSetAccessorList)
])),
ClassDeclaration(Identifier("FromKeyedServicesAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsFieldAndProperty)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ConstructorDeclaration(Identifier("FromKeyedServicesAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(ObjectType, Identifier("key")))))
.WithEmptyBlockBody()))
]))))
.NormalizeWhitespace();
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotation.ToFullStringWithHeader());
CompilationUnitSyntax resourceLocalization = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Resource.Localization")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(List<MemberDeclarationSyntax>(
[
ClassDeclaration(Identifier("ExtendedEnumAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsEnum)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList),
ClassDeclaration(Identifier("LocalizationKeyAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsField)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ConstructorDeclaration(Identifier("LocalizationKeyAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(StringType, Identifier("key")))))
.WithEmptyBlockBody()))
]))))
.NormalizeWhitespace();
context.AddSource("Snap.Hutao.Resource.Localization.Attributes.g.cs", resourceLocalization.ToFullStringWithHeader());
CompilationUnitSyntax interceptsLocation = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("System.Runtime.CompilerServices")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ClassDeclaration(Identifier("InterceptsLocationAttribute"))
.WithAttributeLists(SingletonList(SystemAttributeUsageList(AttributeTargetsMethod, allowMultiple: true)))
.WithModifiers(InternalSealedTokenList)
.WithBaseList(SystemAttributeBaseList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ConstructorDeclaration(Identifier("InterceptsLocationAttribute"))
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(IntType, Identifier("version")),
Parameter(StringType, Identifier("data"))
])))
.WithEmptyBlockBody()))))))
.NormalizeWhitespace();
context.AddSource("System.Runtime.CompilerServices.InterceptsLocationAttribute.g.cs", interceptsLocation.ToFullStringWithHeader());
}
private static AttributeListSyntax SystemAttributeUsageList(AttributeArgumentSyntax attributeTargets, bool allowMultiple = false, bool inherited = true)
{
SeparatedSyntaxList<AttributeArgumentSyntax> arguments = SingletonSeparatedList(attributeTargets);
if (allowMultiple)
{
arguments = arguments.Add(AllowMultipleTrue);
}
if (!inherited)
{
arguments = arguments.Add(InheritedFalse);
}
return AttributeList(SingletonSeparatedList(
Attribute(ParseName("global::System.AttributeUsage"))
.WithArgumentList(AttributeArgumentList(arguments))));
}
}

View File

@@ -0,0 +1,273 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using Snap.Hutao.SourceGeneration.Model;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class ApiEndpointsGenerator : IIncrementalGenerator
{
private const string FileName = "Endpoints.csv";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<EndpointsMetadataContext> provider = context.AdditionalTextsProvider
.Where(Match)
.Select(EndpointsMetadataContext.Create)
.Where(EndpointsMetadataContextNotEmpty);
context.RegisterImplementationSourceOutput(provider, GenerateWrapper);
}
private static bool Match(AdditionalText text)
{
// Match '*Endpoints.csv' files
return Path.GetFileName(text.Path).EndsWith(FileName, StringComparison.OrdinalIgnoreCase);
}
private static bool EndpointsMetadataContextNotEmpty(EndpointsMetadataContext context)
{
return !context.Endpoints.IsEmpty;
}
private static void GenerateWrapper(SourceProductionContext production, EndpointsMetadataContext context)
{
try
{
Generate(production, context);
}
catch (Exception ex)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", ex.ToString());
}
}
private static void Generate(SourceProductionContext production, EndpointsMetadataContext context)
{
string interfaceName = $"I{context.Name}";
string chineseImplName = $"{context.Name}ImplementationForChinese";
string overseaImplName = $"{context.Name}ImplementationForOversea";
IdentifierNameSyntax interfaceIdentifier = IdentifierName(interfaceName);
CompilationUnitSyntax compilation = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration(context.ExtraInfo?.Namespace ?? "Snap.Hutao.Web")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(
List<MemberDeclarationSyntax>(
[
InterfaceDeclaration(interfaceName)
.WithModifiers(InternalPartialTokenList)
.WithMembers(List(GenerateInterfaceMethods(context.Endpoints))),
ClassDeclaration(chineseImplName)
.WithModifiers(InternalAbstractTokenList)
.WithBaseList(BaseList(SingletonSeparatedList<BaseTypeSyntax>(SimpleBaseType(interfaceIdentifier))))
.WithMembers(List(GenerateClassMethods(context.Endpoints, true))),
ClassDeclaration(overseaImplName)
.WithModifiers(InternalAbstractTokenList)
.WithBaseList(BaseList(SingletonSeparatedList<BaseTypeSyntax>(SimpleBaseType(interfaceIdentifier))))
.WithMembers(List(GenerateClassMethods(context.Endpoints, false)))
]))))
.NormalizeWhitespace();
production.AddSource($"{context.Name}.g.cs", compilation.ToFullStringWithHeader());
}
private static IEnumerable<MemberDeclarationSyntax> GenerateInterfaceMethods(ImmutableArray<EndpointsMetadata> metadataArray)
{
foreach (EndpointsMetadata metadata in metadataArray)
{
if (metadata.GetMethodDeclaration() is not MethodDeclarationSyntax methodDeclaration)
{
continue;
}
string lead = $"""
/// <summary>
/// <code>CN: {metadata.Chinese?.Replace("&", "&amp;")}</code>
/// <code>OS: {metadata.Oversea?.Replace("&", "&amp;")}</code>
/// </summary>
""";
yield return methodDeclaration
.WithLeadingTrivia(ParseLeadingTrivia(lead))
.WithSemicolonToken(SemicolonToken);
}
}
private static IEnumerable<MemberDeclarationSyntax> GenerateClassMethods(ImmutableArray<EndpointsMetadata> metadataArray, bool isChinese)
{
foreach (EndpointsMetadata metadata in metadataArray)
{
if (metadata.GetMethodDeclaration() is not MethodDeclarationSyntax methodDeclaration)
{
continue;
}
yield return methodDeclaration
.WithModifiers(PublicTokenList)
.WithExpressionBody(ArrowExpressionClause(isChinese ? metadata.GetChineseExpression() : metadata.GetOverseaExpression()))
.WithSemicolonToken(SemicolonToken);
}
}
private static IReadOnlyList<string> ParseCsvLine(string line)
{
List<string> fields = [];
StringBuilder currentField = new();
bool insideQuotes = false;
ReadOnlySpan<char> lineSpan = line.AsSpan();
for (int i = 0; i < lineSpan.Length; i++)
{
ref readonly char currentChar = ref lineSpan[i];
if (currentChar is '"')
{
if (insideQuotes && i + 1 < line.Length && line[i + 1] == '"')
{
// 处理双引号转义
currentField.Append('"');
i++;
}
else
{
insideQuotes = !insideQuotes;
}
}
else if (currentChar == ',' && !insideQuotes)
{
fields.Add(currentField.ToString());
currentField.Clear();
}
else
{
currentField.Append(currentChar);
}
}
// 添加最后一个字段
fields.Add(currentField.ToString());
return fields;
}
private sealed record EndpointsMetadataContext
{
public required string Name { get; init; }
public required EndpointsExtraInfo? ExtraInfo { get; init; }
public required EquatableArray<EndpointsMetadata> Endpoints { get; init; }
public static EndpointsMetadataContext Create(AdditionalText text, CancellationToken token)
{
string fileName = Path.GetFileNameWithoutExtension(text.Path);
EndpointsExtraInfo? extraInfo = default;
ImmutableArray<EndpointsMetadata>.Builder endpointsBuilder = ImmutableArray.CreateBuilder<EndpointsMetadata>();
using (StringReader reader = new(text.GetText(token)!.ToString()))
{
while (reader.ReadLine() is { Length: > 0 } line)
{
if (line is "Name,CN,OS")
{
continue;
}
if (line.StartsWith("Extra:", StringComparison.OrdinalIgnoreCase))
{
extraInfo = JsonSerializer.Deserialize<EndpointsExtraInfo>(line[6..]);
continue;
}
IReadOnlyList<string> columns = ParseCsvLine(line);
EndpointsMetadata metadata = new()
{
MethodString = columns.ElementAtOrDefault(0),
Chinese = columns.ElementAtOrDefault(1),
Oversea = columns.ElementAtOrDefault(2),
};
endpointsBuilder.Add(metadata);
}
}
return new()
{
Name = fileName,
ExtraInfo = extraInfo,
Endpoints = endpointsBuilder.ToImmutable(),
};
}
}
private sealed class EndpointsMetadata : IEquatable<EndpointsMetadata?>
{
public required string? MethodString { get; init; }
public required string? Chinese { get; init; }
public required string? Oversea { get; init; }
public MemberDeclarationSyntax? GetMethodDeclaration()
{
return string.IsNullOrEmpty(MethodString) ? default : ParseMemberDeclaration(MethodString!);
}
public ExpressionSyntax GetChineseExpression()
{
return string.IsNullOrEmpty(Chinese) ? WellKnownSyntax.ThrowNotSupportedException : ParseExpression($"$\"{Chinese}\"");
}
public ExpressionSyntax GetOverseaExpression()
{
return string.IsNullOrEmpty(Oversea) ? WellKnownSyntax.ThrowNotSupportedException : ParseExpression($"$\"{Oversea}\"");
}
public bool Equals(EndpointsMetadata? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return MethodString == other.MethodString && Chinese == other.Chinese && Oversea == other.Oversea;
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is EndpointsMetadata other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(MethodString, Chinese, Oversea);
}
}
private sealed record EndpointsExtraInfo
{
public string? Namespace { get; init; }
}
}

View File

@@ -0,0 +1,327 @@
// 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.Extension;
using Snap.Hutao.SourceGeneration.Model;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
using TypeInfo = Snap.Hutao.SourceGeneration.Model.TypeInfo;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class ConstructorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<ConstructorGeneratorContext> provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
WellKnownMetadataNames.GeneratedConstructorAttribute,
SyntaxNodeHelper.Is<BaseMethodDeclarationSyntax>,
ConstructorGeneratorContext.Create);
context.RegisterSourceOutput(provider, GenerateWrapper);
}
private static void GenerateWrapper(SourceProductionContext production, ConstructorGeneratorContext context)
{
try
{
Generate(production, context);
}
catch (Exception ex)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", ex.ToString());
}
}
private static void Generate(SourceProductionContext production, ConstructorGeneratorContext context)
{
CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit(
[
GenerateConstructorDeclaration(context)
.WithParameterList(GenerateConstructorParameterList(context))
.WithBody(Block(List(GenerateConstructorBodyStatements(context, production.CancellationToken)))),
// Property declarations
.. GeneratePropertyDeclarations(context, production.CancellationToken),
// PreConstruct & PostConstruct Method declarations
MethodDeclaration(VoidType, Identifier("PreConstruct"))
.WithModifiers(PartialTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(TypeOfSystemIServiceProvider, Identifier("serviceProvider")))))
.WithSemicolonToken(SemicolonToken),
MethodDeclaration(VoidType, Identifier("PostConstruct"))
.WithModifiers(PartialTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(TypeOfSystemIServiceProvider, Identifier("serviceProvider")))))
.WithSemicolonToken(SemicolonToken)
]).NormalizeWhitespace();
production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader());
}
private static ConstructorDeclarationSyntax GenerateConstructorDeclaration(ConstructorGeneratorContext context)
{
ConstructorDeclarationSyntax constructorDeclaration = ConstructorDeclaration(Identifier(context.Hierarchy.Hierarchy[0].Name))
.WithModifiers(context.DeclaredAccessibility.ToSyntaxTokenList(PartialKeyword));
if (context.Attribute.HasNamedArgument("CallBaseConstructor", true))
{
string serviceProvider = context.Parameters.Single(static p => p.FullyQualifiedTypeMetadataName is WellKnownMetadataNames.IServiceProvider).Name;
constructorDeclaration = constructorDeclaration.WithInitializer(
BaseConstructorInitializer(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName(serviceProvider))))));
}
return constructorDeclaration;
}
private static ParameterListSyntax GenerateConstructorParameterList(ConstructorGeneratorContext context)
{
return ParameterList(SeparatedList(context.Parameters.Select(static p => p.GetSyntax())));
}
private static IEnumerable<StatementSyntax> GenerateConstructorBodyStatements(ConstructorGeneratorContext context, CancellationToken token)
{
string serviceProvider = context.Parameters.Single(static p => p.FullyQualifiedTypeMetadataName is WellKnownMetadataNames.IServiceProvider).Name;
// Call PreConstruct
token.ThrowIfCancellationRequested();
yield return ExpressionStatement(InvocationExpression(IdentifierName("PreConstruct"))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName(serviceProvider))))));
// Assign fields
foreach (StatementSyntax? statementSyntax in GenerateConstructorBodyFieldAssignments(serviceProvider, context, token))
{
token.ThrowIfCancellationRequested();
yield return statementSyntax;
}
// Assign properties
foreach (StatementSyntax? statementSyntax in GenerateConstructorBodyPropertyAssignments(serviceProvider, context, token))
{
token.ThrowIfCancellationRequested();
yield return statementSyntax;
}
token.ThrowIfCancellationRequested();
// Call Register for IRecipient interfaces
foreach (TypeInfo recipientInterface in context.Interfaces)
{
string messageTypeString = recipientInterface.TypeArguments.Single().FullyQualifiedTypeNameWithNullabilityAnnotations;
TypeSyntax messageType = ParseTypeName(messageTypeString);
token.ThrowIfCancellationRequested();
yield return ExpressionStatement(InvocationExpression(SimpleMemberAccessExpression(
TypeOfCommunityToolkitMvvmMessagingIMessengerExtensions,
GenericName(Identifier("Register"))
.WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(messageType)))))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(ServiceProviderGetRequiredService(IdentifierName(serviceProvider), TypeOfCommunityToolkitMvvmMessagingIMessenger)),
Argument(ThisExpression())
]))));
}
// Call InitializeComponent if specified
if (context.Attribute.HasNamedArgument("InitializeComponent", true))
{
token.ThrowIfCancellationRequested();
yield return ExpressionStatement(InvocationExpression(IdentifierName("InitializeComponent")));
}
// Call PostConstruct
token.ThrowIfCancellationRequested();
yield return ExpressionStatement(InvocationExpression(IdentifierName("PostConstruct"))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName(serviceProvider))))));
}
private static IEnumerable<StatementSyntax> GenerateConstructorBodyFieldAssignments(string serviceProviderName, ConstructorGeneratorContext context, CancellationToken token)
{
foreach ((bool shouldSkip, FieldInfo fieldInfo) in context.Fields)
{
if (shouldSkip)
{
yield return EmptyStatement().WithTrailingTrivia(Comment($"// Skipped field with initializer: {fieldInfo.MinimallyQualifiedName}"));
continue;
}
fieldInfo.TryGetAttributeWithFullyQualifiedMetadataName(WellKnownMetadataNames.FromKeyedServicesAttribute, out AttributeInfo? fromKeyed);
yield return GenerateConstructorBodyMemberAssignment(
fieldInfo.FullyQualifiedTypeNameWithNullabilityAnnotation,
fieldInfo.MinimallyQualifiedName,
serviceProviderName,
context,
fromKeyed,
token);
}
}
private static IEnumerable<StatementSyntax> GenerateConstructorBodyPropertyAssignments(string serviceProviderName, ConstructorGeneratorContext context, CancellationToken token)
{
foreach (PropertyInfo propertyInfo in context.Properties)
{
propertyInfo.TryGetAttributeWithFullyQualifiedMetadataName(WellKnownMetadataNames.FromKeyedServicesAttribute, out AttributeInfo? fromKeyed);
yield return GenerateConstructorBodyMemberAssignment(
propertyInfo.FullyQualifiedTypeNameWithNullabilityAnnotation,
propertyInfo.Name,
serviceProviderName,
context,
fromKeyed,
token);
}
}
private static StatementSyntax GenerateConstructorBodyMemberAssignment(
string fullyQualifiedMemberTypeName,
string memberName,
string serviceProviderName,
ConstructorGeneratorContext context,
AttributeInfo? fromKeyed,
CancellationToken token)
{
TypeSyntax propertyType = ParseTypeName(fullyQualifiedMemberTypeName);
MemberAccessExpressionSyntax memberAccess = SimpleMemberAccessExpression(ThisExpression(), IdentifierName(memberName));
token.ThrowIfCancellationRequested();
return fullyQualifiedMemberTypeName switch
{
"global::System.Net.Http.HttpClient" => context.Parameters.SingleOrDefault(static p => p.FullyQualifiedTypeMetadataName is WellKnownMetadataNames.HttpClient) is { } httpClient
? ExpressionStatement(SimpleAssignmentExpression(memberAccess, IdentifierName(httpClient.Name)))
: ExpressionStatement(SimpleAssignmentExpression(memberAccess,
InvocationExpression(SimpleMemberAccessExpression(
ServiceProviderGetRequiredService(IdentifierName(serviceProviderName), TypeOfSystemNetHttpIHttpClientFactory),
IdentifierName("CreateClient")))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(NameOfExpression(IdentifierName(context.Hierarchy.Hierarchy[0].Name)))))))),
_ => context.Parameters.SingleOrDefault(p => p.FullyQualifiedTypeName == fullyQualifiedMemberTypeName) is { } parameter
? ExpressionStatement(SimpleAssignmentExpression(memberAccess, IdentifierName(parameter.Name)))
: fromKeyed is not null
? ExpressionStatement(SimpleAssignmentExpression(
memberAccess,
ServiceProviderGetRequiredKeyedService(IdentifierName(serviceProviderName), propertyType, fromKeyed.ConstructorArguments.Single().GetSyntax())))
: ExpressionStatement(SimpleAssignmentExpression(
memberAccess,
ServiceProviderGetRequiredService(IdentifierName(serviceProviderName), propertyType)))
};
}
private static IEnumerable<PropertyDeclarationSyntax> GeneratePropertyDeclarations(ConstructorGeneratorContext context, CancellationToken token)
{
foreach (PropertyInfo propertyInfo in context.Properties)
{
TypeSyntax propertyType = ParseTypeName(propertyInfo.FullyQualifiedTypeNameWithNullabilityAnnotation);
token.ThrowIfCancellationRequested();
yield return PropertyDeclaration(propertyType, Identifier(propertyInfo.Name))
.WithModifiers(propertyInfo.DeclaredAccessibility.ToSyntaxTokenList(PartialKeyword))
.WithAccessorList(AccessorList(SingletonList(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(FieldExpression()))
.WithSemicolonToken(SemicolonToken))));
}
}
private sealed record ConstructorGeneratorContext
{
public required AttributeInfo Attribute { get; init; }
public required HierarchyInfo Hierarchy { get; init; }
public required EquatableArray<ParameterInfo> Parameters { get; init; }
public required EquatableArray<(bool ShouldSkip, FieldInfo Field)> Fields { get; init; }
public required EquatableArray<PropertyInfo> Properties { get; init; }
public required EquatableArray<TypeInfo> Interfaces { get; init; }
public required Accessibility DeclaredAccessibility { get; init; }
public static ConstructorGeneratorContext Create(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (context.TargetSymbol is not IMethodSymbol { ContainingType: { } typeSymbol } constructorSymbol)
{
return default!;
}
ImmutableArray<(bool ShouldSkip, FieldInfo Field)>.Builder fieldsBuilder = ImmutableArray.CreateBuilder<(bool ShouldSkip, FieldInfo Field)>();
ImmutableArray<PropertyInfo>.Builder propertiesBuilder = ImmutableArray.CreateBuilder<PropertyInfo>();
foreach (ISymbol member in typeSymbol.GetMembers())
{
switch (member)
{
case IFieldSymbol fieldSymbol:
{
if (fieldSymbol.IsImplicitlyDeclared || fieldSymbol.HasConstantValue || fieldSymbol.IsStatic || !fieldSymbol.IsReadOnly)
{
continue;
}
bool shouldSkip = false;
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax { Initializer: not null })
{
// Skip field with initializer
shouldSkip = true;
break;
}
}
fieldsBuilder.Add((shouldSkip, FieldInfo.Create(fieldSymbol)));
break;
}
case IPropertySymbol propertySymbol:
{
if (propertySymbol.IsStatic || propertySymbol.IsImplicitlyDeclared || !propertySymbol.IsPartialDefinition || !propertySymbol.IsReadOnly)
{
continue;
}
propertiesBuilder.Add(PropertyInfo.Create(propertySymbol));
break;
}
}
}
ImmutableArray<TypeInfo>.Builder interfacesBuilder = ImmutableArray.CreateBuilder<TypeInfo>();
foreach (INamedTypeSymbol interfaceSymbol in typeSymbol.Interfaces)
{
if (!interfaceSymbol.HasFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Messaging.IRecipient`1"))
{
continue;
}
interfacesBuilder.Add(TypeInfo.Create(interfaceSymbol));
}
return new()
{
Attribute = AttributeInfo.Create(context.Attributes.Single()),
Hierarchy = HierarchyInfo.Create(typeSymbol),
Parameters = ImmutableArray.CreateRange(constructorSymbol.Parameters, ParameterInfo.Create),
Fields = fieldsBuilder.ToImmutable(),
Properties = propertiesBuilder.ToImmutable(),
Interfaces = interfacesBuilder.ToImmutable(),
DeclaredAccessibility = constructorSymbol.DeclaredAccessibility,
};
}
}
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using Snap.Hutao.SourceGeneration.Model;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Immutable;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class UnsafePropertyBackingFieldAccessorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<FieldAccessorGeneratorContext> provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
WellKnownMetadataNames.FieldAccessAttribute,
SyntaxNodeHelper.Is<PropertyDeclarationSyntax>,
FieldAccessorPropertyEntry.Create)
.Where(static entry => entry is not null)
.GroupBy(static entry => entry.Hierarchy)
.Select(FieldAccessorGeneratorContext.Create);
context.RegisterSourceOutput(provider, GenerateWrapper);
}
private static void GenerateWrapper(SourceProductionContext production, FieldAccessorGeneratorContext context)
{
try
{
Generate(production, context);
}
catch (Exception e)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString());
}
}
private static void Generate(SourceProductionContext production, FieldAccessorGeneratorContext context)
{
CompilationUnitSyntax syntax = context.Hierarchy.GetCompilationUnit(
GenerateAccessMethods(context, production.CancellationToken))
.NormalizeWhitespace();
production.AddSource(context.Hierarchy.FileNameHint, syntax.ToFullStringWithHeader());
}
private static ImmutableArray<MemberDeclarationSyntax> GenerateAccessMethods(FieldAccessorGeneratorContext context, CancellationToken token)
{
ImmutableArray<MemberDeclarationSyntax>.Builder accessMethodsBuilder = ImmutableArray.CreateBuilder<MemberDeclarationSyntax>(context.Properties.Length);
foreach (FieldAccessorPropertyEntry entry in context.Properties)
{
TypeSyntax propertyType = ParseTypeName(entry.Property.FullyQualifiedTypeNameWithNullabilityAnnotation);
RefTypeSyntax refPropertyType = RefType(propertyType);
if (entry.ReadOnly)
{
refPropertyType = refPropertyType.WithReadOnlyKeyword(ReadOnlyKeyword);
}
MethodDeclarationSyntax method = MethodDeclaration(refPropertyType, Identifier($"FieldRefOf{entry.Property.Name}"))
.WithAttributeLists(SingletonList(AttributeList(SingletonSeparatedList(
GenerateUnsafeAccessorAttribute(entry.Field.MinimallyQualifiedName)))))
.WithModifiers(PrivateStaticExternTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(ParseTypeName(entry.Hierarchy.Hierarchy[0].FullyQualifiedName), Identifier("self")))))
.WithSemicolonToken(SemicolonToken);
token.ThrowIfCancellationRequested();
accessMethodsBuilder.Add(method);
}
token.ThrowIfCancellationRequested();
return accessMethodsBuilder.ToImmutable();
}
private static AttributeSyntax GenerateUnsafeAccessorAttribute(string fieldName)
{
return Attribute(NameOfSystemRuntimeCompilerServicesUnsafeAccessor)
.WithArgumentList(AttributeArgumentList(SeparatedList<AttributeArgumentSyntax>(
[
AttributeArgument(SimpleMemberAccessExpression(
NameOfSystemRuntimeCompilerServicesUnsafeAccessorKind,
IdentifierName("Field"))),
AttributeArgument(StringLiteralExpression(fieldName)).WithNameEquals(NameEquals("Name")),
])));
}
private sealed record FieldAccessorPropertyEntry
{
public required HierarchyInfo Hierarchy { get; init; }
public required PropertyInfo Property { get; init; }
public required FieldInfo Field { get; init; }
public required bool ReadOnly { get; init; }
public static FieldAccessorPropertyEntry Create(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (context.TargetSymbol is not IPropertySymbol propertySymbol)
{
return default!;
}
if (propertySymbol.RefCustomModifiers.Length > 0 || (propertySymbol.GetMethod is null && propertySymbol.SetMethod is null))
{
return default!;
}
// { get; } or { get; init; } or { init; } => ref readonly
// { get; set; } or { set; } => ref
bool readOnly = propertySymbol.SetMethod is null || propertySymbol.SetMethod.IsInitOnly;
IFieldSymbol? fieldSymbol = null;
foreach (IFieldSymbol field in propertySymbol.ContainingType.GetMembers().OfType<IFieldSymbol>())
{
if (SymbolEqualityComparer.Default.Equals(field.AssociatedSymbol, propertySymbol))
{
fieldSymbol = field;
break;
}
}
if (fieldSymbol is null)
{
return default!;
}
return new()
{
Hierarchy = HierarchyInfo.Create(propertySymbol.ContainingType),
Property = PropertyInfo.Create(propertySymbol),
Field = FieldInfo.Create(fieldSymbol),
ReadOnly = readOnly,
};
}
}
private sealed record FieldAccessorGeneratorContext
{
public required HierarchyInfo Hierarchy { get; init; }
public required EquatableArray<FieldAccessorPropertyEntry> Properties { get; init; }
public static FieldAccessorGeneratorContext Create((HierarchyInfo Hierarchy, EquatableArray<FieldAccessorPropertyEntry> Properties) tuple, CancellationToken token)
{
token.ThrowIfCancellationRequested();
return new()
{
Hierarchy = tuple.Hierarchy,
Properties = tuple.Properties,
};
}
}
}

View File

@@ -0,0 +1,194 @@
// 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.Extension;
using Snap.Hutao.SourceGeneration.Model;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
using TypeInfo = Snap.Hutao.SourceGeneration.Model.TypeInfo;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
[Generator(LanguageNames.CSharp)]
internal sealed class HttpClientGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<HttpClientGeneratorContext> provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
WellKnownMetadataNames.HttpClientAttribute,
SyntaxNodeHelper.Is<ClassDeclarationSyntax>,
HttpClientEntry.Create)
.Where(static entry => entry is not null)
.Collect()
.Select(HttpClientGeneratorContext.Create);
context.RegisterImplementationSourceOutput(provider, GenerateWrapper);
}
private static void GenerateWrapper(SourceProductionContext production, HttpClientGeneratorContext context)
{
try
{
Generate(production, context);
}
catch (Exception e)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString());
}
}
private static void Generate(SourceProductionContext production, HttpClientGeneratorContext context)
{
CompilationUnitSyntax syntax = CompilationUnit()
.WithUsings(SingletonList(UsingDirective("System.Net.Http")))
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ClassDeclaration("ServiceCollectionExtension")
.WithModifiers(InternalStaticPartialTokenList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
MethodDeclaration(TypeOfMicrosoftExtensionsDependencyInjectionIServiceCollection,"AddHttpClients")
.WithModifiers(PublicStaticPartialTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(TypeOfMicrosoftExtensionsDependencyInjectionIServiceCollection, Identifier("services"))
.WithModifiers(ThisTokenList))))
.WithBody(Block(List(GenerateAddHttpClients(context))))))))))
.NormalizeWhitespace();
production.AddSource("ServiceCollectionExtension.g.cs", syntax.ToFullStringWithHeader());
}
private static IEnumerable<StatementSyntax> GenerateAddHttpClients(HttpClientGeneratorContext context)
{
foreach (HttpClientEntry entry in context.HttpClients)
{
TypeSyntax targetType = ParseTypeName(entry.Type.FullyQualifiedName);
SeparatedSyntaxList<TypeSyntax> typeArguments = SingletonSeparatedList(targetType);
if (entry.Attribute.TryGetConstructorArgument(1, out TypedConstantInfo? info) &&
info is TypedConstantInfo.Type typeInfo) // [HttpClient(config, typeof(T))]
{
typeArguments = typeArguments.Insert(0, ParseTypeName(typeInfo.FullyQualifiedTypeName));
}
InvocationExpressionSyntax invocation = InvocationExpression(
SimpleMemberAccessExpression(
IdentifierName("services"),
GenericName(Identifier("AddHttpClient"))
.WithTypeArgumentList(TypeArgumentList(typeArguments))))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName(entry.ConfigurationName)))));
if (entry.PrimaryHttpMessageHandler is { NamedArguments.IsEmpty: false })
{
invocation = InvocationExpression(SimpleMemberAccessExpression(
invocation,
IdentifierName("ConfigurePrimaryHttpMessageHandler")))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(ParenthesizedLambdaExpression()
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(Identifier("handler")),
Parameter(Identifier("serviceProvider"))
])))
.WithBlock(Block(List(
GenerateConfigurePrimaryHttpMessageHandlerStatements(entry.PrimaryHttpMessageHandler.NamedArguments))))))));
}
yield return ExpressionStatement(invocation);
}
yield return ReturnStatement(IdentifierName("services"));
}
private static IEnumerable<StatementSyntax> GenerateConfigurePrimaryHttpMessageHandlerStatements(ImmutableArray<(string, TypedConstantInfo)> namedArguments)
{
yield return LocalDeclarationStatement(VariableDeclaration(TypeOfSystemNetHttpSocketsHttpHandler)
.WithVariables(SingletonSeparatedList(
VariableDeclarator(Identifier("typedHandler"))
.WithInitializer(EqualsValueClause(CastExpression(TypeOfSystemNetHttpSocketsHttpHandler, IdentifierName("handler")))))));
foreach ((string name, TypedConstantInfo typedConstant) in namedArguments)
{
yield return ExpressionStatement(SimpleAssignmentExpression(
SimpleMemberAccessExpression(IdentifierName("typedHandler"), IdentifierName(name)),
typedConstant.GetSyntax()));
}
}
private sealed record HttpClientEntry : IComparable<HttpClientEntry?>
{
public required AttributeInfo Attribute { get; init; }
public required AttributeInfo? PrimaryHttpMessageHandler { get; init; }
public required string ConfigurationName { get; init; }
public required TypeInfo Type { get; init; }
public static HttpClientEntry Create(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (context.TargetSymbol is not INamedTypeSymbol typeSymbol || context.Attributes.SingleOrDefault() is not { } httpClient)
{
return default!;
}
AttributeInfo? primaryHttpMessageHandler = null;
foreach (AttributeData attribute in typeSymbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(WellKnownMetadataNames.PrimaryHttpMessageHandlerAttribute) is true)
{
primaryHttpMessageHandler = AttributeInfo.Create(attribute);
}
}
return new()
{
Attribute = AttributeInfo.Create(httpClient),
PrimaryHttpMessageHandler = primaryHttpMessageHandler,
ConfigurationName = $"{httpClient.ConstructorArguments[0].ToCSharpString()[WellKnownMetadataNames.HttpClientConfiguration.Length..]}Configuration",
Type = TypeInfo.Create(typeSymbol),
};
}
public int CompareTo(HttpClientEntry? other)
{
if (ReferenceEquals(this, other))
{
return 0;
}
if (other is null)
{
return 1;
}
return string.Compare(Type.FullyQualifiedName, other.Type.FullyQualifiedName, StringComparison.Ordinal);
}
}
private sealed record HttpClientGeneratorContext
{
public required EquatableArray<HttpClientEntry> HttpClients { get; init; }
public static HttpClientGeneratorContext Create(ImmutableArray<HttpClientEntry> services, CancellationToken token)
{
token.ThrowIfCancellationRequested();
return new()
{
HttpClients = services.Sort(),
};
}
}
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using Snap.Hutao.SourceGeneration.Model;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
using TypeInfo = Snap.Hutao.SourceGeneration.Model.TypeInfo;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
[Generator(LanguageNames.CSharp)]
internal sealed class ServiceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ServiceGeneratorContext> provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
WellKnownMetadataNames.ServiceAttribute,
SyntaxNodeHelper.Is<ClassDeclarationSyntax>,
ServiceEntry.Create)
.Where(static entry => entry is not null)
.Collect()
.Select(ServiceGeneratorContext.Create);
context.RegisterImplementationSourceOutput(provider, GenerateWrapper);
}
private static void GenerateWrapper(SourceProductionContext production, ServiceGeneratorContext context)
{
try
{
Generate(production, context);
}
catch (Exception e)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString());
}
}
private static void Generate(SourceProductionContext production, ServiceGeneratorContext context)
{
CompilationUnitSyntax syntax = CompilationUnit()
.WithUsings(SingletonList(UsingDirective("Microsoft.Extensions.DependencyInjection")))
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Core.DependencyInjection")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ClassDeclaration("ServiceCollectionExtension")
.WithModifiers(InternalStaticPartialTokenList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
MethodDeclaration(TypeOfMicrosoftExtensionsDependencyInjectionIServiceCollection, "AddServices")
.WithModifiers(PublicStaticPartialTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(TypeOfMicrosoftExtensionsDependencyInjectionIServiceCollection, Identifier("services"))
.WithModifiers(ThisTokenList))))
.WithBody(Block(List(GenerateAddServices(context))))))))))
.NormalizeWhitespace();
production.AddSource("ServiceCollectionExtension.g.cs", syntax.ToFullStringWithHeader());
}
private static IEnumerable<StatementSyntax> GenerateAddServices(ServiceGeneratorContext context)
{
foreach (ServiceEntry entry in context.Services)
{
// [Service(serviceLifetime, typeof(T))]
entry.Attribute.TryGetConstructorArgument(1, out TypedConstantInfo? info);
TypedConstantInfo.Type? interfaceType = info as TypedConstantInfo.Type;
TypeSyntax targetType = entry.Type.GetTypeSyntax(interfaceType is null || !interfaceType.IsUnboundGeneric);
// Add{LifeTime}(Type serviceType)
// Add{LifeTime}(Type serviceType, Type implementationType)
// AddKeyed{LifeTime}(Type serviceType, object? key)
// AddKeyed{LifeTime}(Type serviceType, object? key, Type implementationType)
ArgumentSyntax targetTypeArgument = Argument(TypeOfExpression(targetType));
entry.Attribute.TryGetNamedArgument("Key", out TypedConstantInfo? key);
ArgumentSyntax? keyArgument = key is null ? null : Argument(key!.GetSyntax());
ArgumentSyntax? interfaceTypeArgument = interfaceType is null ? null : Argument(TypeOfExpression(ParseTypeName(interfaceType.FullyQualifiedTypeName)));
ArgumentSyntax serviceType = interfaceTypeArgument ?? targetTypeArgument;
ArgumentSyntax? implementationType = interfaceTypeArgument is not null ? targetTypeArgument : null;
SeparatedSyntaxList<ArgumentSyntax> arguments = (keyArgument, implementationType) switch
{
(not null, not null) => SeparatedList([serviceType, keyArgument, implementationType]),
(not null, null) => SeparatedList([serviceType, keyArgument]),
(null, not null) => SeparatedList([serviceType, implementationType]),
(null, null) => SingletonSeparatedList(serviceType),
};
InvocationExpressionSyntax invocation = InvocationExpression(
SimpleMemberAccessExpression(
IdentifierName("services"),
IdentifierName($"Add{(key is not null ? "Keyed" : string.Empty)}{entry.ServiceLifetime}")))
.WithArgumentList(ArgumentList(arguments));
yield return ExpressionStatement(invocation);
}
yield return ReturnStatement(IdentifierName("services"));
}
private sealed record ServiceEntry : IComparable<ServiceEntry?>
{
public required AttributeInfo Attribute { get; init; }
public required TypeInfo Type { get; init; }
public required string ServiceLifetime { get; init; }
public static ServiceEntry Create(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (context.TargetSymbol is not INamedTypeSymbol typeSymbol || context.Attributes.SingleOrDefault() is not { } service)
{
return default!;
}
AttributeInfo serviceInfo = AttributeInfo.Create(service);
string? serviceLifetime = default;
if (serviceInfo.TryGetConstructorArgument(0, out TypedConstantInfo? lifetime) &&
lifetime is TypedConstantInfo.Enum lifetimeEnum)
{
serviceLifetime = (int)lifetimeEnum.Value switch
{
0 => "Singleton",
1 => "Scoped",
2 => "Transient",
_ => default
};
}
if (string.IsNullOrEmpty(serviceLifetime))
{
return default!;
}
return new()
{
Attribute = serviceInfo,
Type = TypeInfo.Create(typeSymbol),
ServiceLifetime = serviceLifetime!,
};
}
public int CompareTo(ServiceEntry? other)
{
if (ReferenceEquals(this, other))
{
return 0;
}
if (other is null)
{
return 1;
}
int result = string.Compare(ServiceLifetime, other.ServiceLifetime, StringComparison.Ordinal);
if (result == 0)
{
result = string.Compare(Type.FullyQualifiedName, other.Type.FullyQualifiedName, StringComparison.Ordinal);
}
return result;
}
}
private sealed record ServiceGeneratorContext
{
public required EquatableArray<ServiceEntry> Services { get; init; }
public static ServiceGeneratorContext Create(ImmutableArray<ServiceEntry> services, CancellationToken token)
{
token.ThrowIfCancellationRequested();
return new()
{
Services = services.Sort(),
};
}
}
}

View File

@@ -0,0 +1,220 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using Snap.Hutao.SourceGeneration.Model;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
namespace Snap.Hutao.SourceGeneration.Enum;
[Generator(LanguageNames.CSharp)]
internal class ExtendedEnumGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<ExtendedEnumGeneratorContext> provider = context.SyntaxProvider
.ForAttributeWithMetadataName(
WellKnownMetadataNames.ExtendedEnumAttribute,
SyntaxNodeHelper.Is<EnumDeclarationSyntax>,
ExtendedEnumGeneratorContext.Create)
.Where(static c => c is not null);
context.RegisterSourceOutput(provider, GenerateWrapper);
}
private static void GenerateWrapper(SourceProductionContext production, ExtendedEnumGeneratorContext context)
{
try
{
Generate(production, context);
}
catch (Exception e)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", e.ToString());
}
}
private static void Generate(SourceProductionContext production, ExtendedEnumGeneratorContext context)
{
TypeSyntax enumType = context.Type.GetTypeSyntax();
CompilationUnitSyntax syntax = CompilationUnit()
.WithUsings(SingletonList(UsingDirective("System.Globalization")))
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Resource.Localization")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
ClassDeclaration($"{context.Type.Name}Extension")
.WithModifiers(InternalStaticPartialTokenList)
.WithMembers(List<MemberDeclarationSyntax>(
[
// public static string? GetName(this T value)
MethodDeclaration(NullableStringType, "GetName")
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList))))
.WithBody(Block(SingletonList<StatementSyntax>(
ReturnStatement(SwitchExpression(IdentifierName("value"))
.WithArms(SeparatedList(GenerateGetNameSwitchArms(enumType, context.Fields))))))),
// public static string? GetLocalizedDescriptionOrDefault(this T value, ResourceManager resourceManager, CultureInfo cultureInfo)
MethodDeclaration(NullableStringType, "GetLocalizedDescriptionOrDefault")
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList),
Parameter(TypeOfSystemResourcesResourceManager, Identifier("resourceManager")),
Parameter(TypeOfSystemGlobalizationCultureInfo, Identifier("cultureInfo"))
])))
.WithBody(Block(List<StatementSyntax>(
[
LocalDeclarationStatement(VariableDeclaration(StringType)
.WithVariables(SingletonSeparatedList(
VariableDeclarator(Identifier("key"))
.WithInitializer(EqualsValueClause(SwitchExpression(IdentifierName("value"))
.WithArms(SeparatedList(GenerateGetLocalizedDescriptionOrDefaultSwitchArms(enumType, context.Fields)))))))),
ReturnStatement(InvocationExpression(SimpleMemberAccessExpression(
IdentifierName("resourceManager"),
IdentifierName("GetString")))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(IdentifierName("key")),
Argument(IdentifierName("cultureInfo"))
]))))
]))),
// public static string? GetLocalizedDescriptionOrDefault(this T value, ResourceManager resourceManager)
MethodDeclaration(NullableStringType, "GetLocalizedDescriptionOrDefault")
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList),
Parameter(TypeOfSystemResourcesResourceManager, Identifier("resourceManager"))
])))
.WithBody(Block(SingletonList<StatementSyntax>(
ReturnStatement(InvocationExpression(IdentifierName("GetLocalizedDescriptionOrDefault"))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(IdentifierName("value")),
Argument(IdentifierName("resourceManager")),
Argument(SimpleMemberAccessExpression(TypeOfSystemGlobalizationCultureInfo, IdentifierName("CurrentCulture")))
]))))))),
// public static string GetLocalizedDescription(this T value, ResourceManager resourceManager, CultureInfo cultureInfo)
MethodDeclaration(NullableStringType, "GetLocalizedDescription")
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList),
Parameter(TypeOfSystemResourcesResourceManager, Identifier("resourceManager")),
Parameter(TypeOfSystemGlobalizationCultureInfo, Identifier("cultureInfo"))
])))
.WithBody(Block(SingletonList<StatementSyntax>(
ReturnStatement(CoalesceExpression(
InvocationExpression(IdentifierName("GetLocalizedDescriptionOrDefault"))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(IdentifierName("value")),
Argument(IdentifierName("resourceManager")),
Argument(IdentifierName("cultureInfo"))
]))),
CoalesceExpression(
InvocationExpression(IdentifierName("GetName"))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName("value"))))),
SimpleMemberAccessExpression(StringType, IdentifierName("Empty")))))))),
// public static string GetLocalizedDescription(this T value, ResourceManager resourceManager)
MethodDeclaration(NullableStringType, "GetLocalizedDescription")
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(enumType, Identifier("value")).WithModifiers(ThisTokenList),
Parameter(TypeOfSystemResourcesResourceManager, Identifier("resourceManager"))
])))
.WithBody(Block(SingletonList<StatementSyntax>(
ReturnStatement(InvocationExpression(IdentifierName("GetLocalizedDescription"))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(IdentifierName("value")),
Argument(IdentifierName("resourceManager")),
Argument(SimpleMemberAccessExpression(TypeOfSystemGlobalizationCultureInfo, IdentifierName("CurrentCulture")))
])))))))
]))))))
.NormalizeWhitespace();
production.AddSource(context.FileNameHint, syntax.ToFullStringWithHeader());
}
private static IEnumerable<SwitchExpressionArmSyntax> GenerateGetNameSwitchArms(TypeSyntax enumType, ImmutableArray<(FieldInfo Field, AttributeInfo? Attribute)> fields)
{
foreach ((FieldInfo field, _) in fields)
{
yield return SwitchExpressionArm(
ConstantPattern(SimpleMemberAccessExpression(enumType, IdentifierName(field.MinimallyQualifiedName))),
StringLiteralExpression(field.MinimallyQualifiedName));
}
yield return SwitchExpressionArm(
DiscardPattern(),
InvocationExpression(SimpleMemberAccessExpression(
TypeOfSystemEnum,
IdentifierName("GetName")))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName("value"))))));
}
private static IEnumerable<SwitchExpressionArmSyntax> GenerateGetLocalizedDescriptionOrDefaultSwitchArms(TypeSyntax enumType, ImmutableArray<(FieldInfo Field, AttributeInfo? Attribute)> fields)
{
foreach ((FieldInfo field, AttributeInfo? localizationKeyInfo) in fields)
{
if (localizationKeyInfo is not null && localizationKeyInfo.TryGetConstructorArgument(0, out string? localizationKey))
{
yield return SwitchExpressionArm(
ConstantPattern(SimpleMemberAccessExpression(enumType, IdentifierName(field.MinimallyQualifiedName))),
StringLiteralExpression(localizationKey));
}
}
yield return SwitchExpressionArm(
DiscardPattern(),
SimpleMemberAccessExpression(StringType, IdentifierName("Empty")));
}
private sealed record ExtendedEnumGeneratorContext
{
public required string FileNameHint { get; init; }
public required Model.TypeInfo Type { get; init; }
public required EquatableArray<(FieldInfo Field, AttributeInfo? Attribute)> Fields { get; init; }
public static ExtendedEnumGeneratorContext Create(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
if (context.TargetSymbol is not INamedTypeSymbol symbol)
{
return default!;
}
return new()
{
FileNameHint = symbol.GetFullyQualifiedMetadataName(),
Type = Model.TypeInfo.Create(symbol),
Fields = symbol
.GetMembers()
.OfType<IFieldSymbol>()
.Select(static field => (FieldInfo.Create(field), AttributeInfo.CreateOrDefault(field.GetAttributes().SingleOrDefault(static data => data.AttributeClass?.HasFullyQualifiedMetadataName(WellKnownMetadataNames.LocalizationKeyAttribute) is true))))
.ToImmutableArray(),
};
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class AccessibilityExtension
{
public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility)
{
return accessibility switch
{
Accessibility.NotApplicable => TokenList(),
Accessibility.Private => PrivateTokenList,
Accessibility.ProtectedAndInternal => PrivateProtectedTokenList,
Accessibility.Protected => ProtectedTokenList,
Accessibility.Internal => InternalTokenList,
Accessibility.ProtectedOrInternal => ProtectedInternalTokenList,
Accessibility.Public => PublicTokenList,
_ => TokenList()
};
}
public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility, SyntaxToken additionalToken)
{
return ToSyntaxTokenList(accessibility).Add(additionalToken);
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class AttributeDataExtension
{
public static bool HasNamedArgument<T>(this AttributeData attributeData, string name, T? value)
{
foreach ((string propertyName, TypedConstant constant) in attributeData.NamedArguments)
{
if (propertyName == name)
{
return constant.Value is T argumentValue && EqualityComparer<T?>.Default.Equals(argumentValue, value);
}
}
return false;
}
[Obsolete]
public static bool HasNamedArgument<TValue>(this AttributeData attributeData, string name, Func<TValue, bool> predicate)
{
foreach ((string propertyName, TypedConstant constant) in attributeData.NamedArguments)
{
if (propertyName == name)
{
return constant.Value is TValue argumentValue && predicate(argumentValue);
}
}
return false;
}
public static bool TryGetConstructorArgument<T>(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result)
{
if (attributeData.ConstructorArguments.Length > index &&
attributeData.ConstructorArguments[index].Value is T argument)
{
result = argument;
return true;
}
result = default;
return false;
}
public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, out TypedConstant result)
{
if (attributeData.ConstructorArguments.Length > index)
{
result = attributeData.ConstructorArguments[index];
return true;
}
result = default;
return false;
}
public static bool TryGetNamedArgument(this AttributeData data, string key, out TypedConstant value)
{
foreach ((string propertyName, TypedConstant constant) in data.NamedArguments)
{
if (propertyName == key)
{
value = constant;
return true;
}
}
value = default;
return false;
}
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Model;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class IncrementalValuesProviderExtension
{
public static IncrementalValuesProvider<(TKey Left, EquatableArray<TElement> Right)> GroupBy<TLeft, TRight, TKey, TElement>(
this IncrementalValuesProvider<(TLeft Left, TRight Right)> source,
Func<(TLeft Left, TRight Right), TKey> keySelector,
Func<(TLeft Left, TRight Right), TElement> elementSelector)
where TLeft : IEquatable<TLeft>
where TRight : IEquatable<TRight>
where TKey : IEquatable<TKey>
where TElement : IEquatable<TElement>
{
return source.Collect().SelectMany((item, token) =>
{
Dictionary<TKey, ImmutableArray<TElement>.Builder> map = new();
foreach ((TLeft, TRight) pair in item)
{
TKey key = keySelector(pair);
TElement element = elementSelector(pair);
if (!map.TryGetValue(key, out ImmutableArray<TElement>.Builder builder))
{
builder = ImmutableArray.CreateBuilder<TElement>();
map.Add(key, builder);
}
builder.Add(element);
}
token.ThrowIfCancellationRequested();
ImmutableArray<(TKey Key, EquatableArray<TElement> Elements)>.Builder result =
ImmutableArray.CreateBuilder<(TKey, EquatableArray<TElement>)>();
foreach (KeyValuePair<TKey, ImmutableArray<TElement>.Builder> entry in map)
{
result.Add((entry.Key, entry.Value.ToImmutable()));
}
return result;
});
}
public static IncrementalValuesProvider<(TKey Key, EquatableArray<TElement> Right)> GroupBy<TKey, TElement>(
this IncrementalValuesProvider<TElement> source,
Func<TElement, TKey> keySelector)
where TKey : IEquatable<TKey>
where TElement : IEquatable<TElement>
{
return source.Collect().SelectMany((item, token) =>
{
Dictionary<TKey, ImmutableArray<TElement>.Builder> map = new();
foreach (TElement source in item)
{
TKey key = keySelector(source);
if (!map.TryGetValue(key, out ImmutableArray<TElement>.Builder builder))
{
builder = ImmutableArray.CreateBuilder<TElement>();
map.Add(key, builder);
}
builder.Add(source);
}
token.ThrowIfCancellationRequested();
ImmutableArray<(TKey Key, EquatableArray<TElement> Elements)>.Builder result =
ImmutableArray.CreateBuilder<(TKey, EquatableArray<TElement>)>();
foreach (KeyValuePair<TKey, ImmutableArray<TElement>.Builder> entry in map)
{
result.Add((entry.Key, entry.Value.ToImmutable()));
}
return result;
});
}
public static IncrementalValuesProvider<T> Distinct<T>(this IncrementalValuesProvider<T> source)
where T : IEquatable<T>
{
return source.Collect().SelectMany((array, token) => array.Distinct().ToImmutableArray());
}
public static IncrementalValuesProvider<T> Concat<T>(this IncrementalValuesProvider<T> source, IncrementalValuesProvider<T> other)
{
return source.Collect().Combine(other.Collect()).SelectMany(static (t, token) => t.Left.AddRange(t.Right));
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class KeyValuePairExtension
{
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
{
key = pair.Key;
value = pair.Value;
}
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class SymbolExtension
{
private static readonly FrozenSet<char> InvalidFileNameChars = Path.GetInvalidFileNameChars().ToFrozenSet();
public static string NormalizedFullyQualifiedName(this ISymbol symbol)
{
string fullyQualifiedName = symbol.GetFullyQualifiedName();
StringBuilder sb = new StringBuilder(fullyQualifiedName.Length);
foreach (char c in fullyQualifiedName)
{
sb.Append(InvalidFileNameChars.Contains(c) ? '_' : c);
}
return sb.ToString();
}
public static string GetFullyQualifiedName(this ISymbol symbol)
{
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
public static string GetFullyQualifiedNameWithNullabilityAnnotations(this ISymbol symbol)
{
return symbol.ToDisplayString(SymbolDisplayFormats.NullableFullyQualifiedFormat);
}
public static string GetFullyQualifiedNameWithoutTypeParameters(this ISymbol symbol)
{
return symbol.ToDisplayString(SymbolDisplayFormats.FullyQualifiedFormatWithoutTypeParameters);
}
public static string GetMinimallyQualifiedName(this ISymbol symbol)
{
return symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
}
public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
{
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name;
}
public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name)
{
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) is true)
{
return true;
}
}
return false;
}
public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
{
return TryGetAttributeWithType(symbol, typeSymbol, out _);
}
public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData)
{
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
{
attributeData = attribute;
return true;
}
}
attributeData = null;
return false;
}
public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData)
{
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) is true)
{
attributeData = attribute;
return true;
}
}
attributeData = null;
return false;
}
public static Accessibility GetEffectiveAccessibility(this ISymbol symbol)
{
// Start by assuming it's visible
Accessibility visibility = Accessibility.Public;
// Handle special cases
switch (symbol.Kind)
{
case SymbolKind.Alias: return Accessibility.Private;
case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol);
case SymbolKind.TypeParameter: return Accessibility.Private;
}
// Traverse the symbol hierarchy to determine the effective accessibility
while (symbol is not null && symbol.Kind != SymbolKind.Namespace)
{
switch (symbol.DeclaredAccessibility)
{
case Accessibility.NotApplicable:
case Accessibility.Private:
return Accessibility.Private;
case Accessibility.Internal:
case Accessibility.ProtectedAndInternal:
visibility = Accessibility.Internal;
break;
}
symbol = symbol.ContainingSymbol;
}
return visibility;
}
public static bool CanBeAccessedFrom(this ISymbol symbol, IAssemblySymbol assembly)
{
Accessibility accessibility = symbol.GetEffectiveAccessibility();
return
accessibility == Accessibility.Public ||
accessibility == Accessibility.Internal && symbol.ContainingAssembly.GivesAccessTo(assembly);
}
public static Location? GetLocationFromAttributeDataOrDefault(this ISymbol symbol, AttributeData attributeData)
{
Location? firstLocation = null;
// Get the syntax tree where the attribute application is located. We use
// it to try to find the symbol location that belongs to the same file.
SyntaxTree? attributeTree = attributeData.ApplicationSyntaxReference?.SyntaxTree;
foreach (Location location in symbol.Locations)
{
if (location.SourceTree == attributeTree)
{
return location;
}
firstLocation ??= location;
}
return firstLocation;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class SyntaxNodeExtension
{
public static string ToFullStringWithHeader(this SyntaxNode node)
{
return $"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
#pragma warning disable CS1591
#pragma warning disable SA1003, SA1009, SA1010, SA1013, SA1027, SA1028
#pragma warning disable SA1101, SA1106, SA1117, SA1122, SA1128
#pragma warning disable SA1201, SA1202, SA1205
#pragma warning disable SA1413
#pragma warning disable SA1514, SA1516
#pragma warning disable SA1623, SA1629, SA1649
{node.ToFullString()}
""";
}
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Primitive;
using System;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Extension;
internal static class TypeSymbolExtension
{
public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.HasFullyQualifiedMetadataName(name))
{
return true;
}
}
return false;
}
public static bool HasOrInheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol))
{
return true;
}
}
return false;
}
public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
INamedTypeSymbol? baseType = typeSymbol.BaseType;
while (baseType is not null)
{
if (baseType.HasFullyQualifiedMetadataName(name))
{
return true;
}
baseType = baseType.BaseType;
}
return false;
}
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;
while (currentBaseTypeSymbol is not null)
{
if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
{
return true;
}
currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
}
return false;
}
public static bool HasInterfaceWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
foreach (INamedTypeSymbol interfaceType in typeSymbol.AllInterfaces)
{
if (interfaceType.HasFullyQualifiedMetadataName(name))
{
return true;
}
}
return false;
}
public static bool HasOrInheritsAttribute(this ITypeSymbol typeSymbol, Func<AttributeData, bool> predicate)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.GetAttributes().Any(predicate))
{
return true;
}
}
return false;
}
public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.HasAttributeWithFullyQualifiedMetadataName(name))
{
return true;
}
}
return false;
}
public static bool HasOrInheritsAttributeWithType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.HasAttributeWithType(baseTypeSymbol))
{
return true;
}
}
return false;
}
public static bool InheritsAttributeWithFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
if (typeSymbol.BaseType is { } baseTypeSymbol)
{
return HasOrInheritsAttributeWithFullyQualifiedMetadataName(baseTypeSymbol, name);
}
return false;
}
public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name)
{
using ImmutableArrayBuilder<char> builder = ImmutableArrayBuilder<char>.Rent();
symbol.AppendFullyQualifiedMetadataName(in builder);
return builder.WrittenSpan.SequenceEqual(name.AsSpan());
}
public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol)
{
using ImmutableArrayBuilder<char> builder = ImmutableArrayBuilder<char>.Rent();
symbol.AppendFullyQualifiedMetadataName(in builder);
return builder.ToString();
}
private static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, in ImmutableArrayBuilder<char> builder)
{
static void BuildFrom(ISymbol? symbol, in ImmutableArrayBuilder<char> builder)
{
switch (symbol)
{
// Namespaces that are nested also append a leading '.'
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
BuildFrom(symbol.ContainingNamespace, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Other namespaces (ie. the one right before global) skip the leading '.'
case INamespaceSymbol { IsGlobalNamespace: false }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Types with no namespace just have their metadata name directly written
case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Types with a containing non-global namespace also append a leading '.'
case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }:
BuildFrom(namespaceSymbol, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
// Nested types append a leading '+'
case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }:
BuildFrom(typeSymbol, in builder);
builder.Add('+');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
default:
break;
}
}
BuildFrom(symbol, in builder);
}
}

View File

@@ -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")]

View File

@@ -0,0 +1,373 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.WellKnownSyntax;
namespace Snap.Hutao.SourceGeneration.Identity;
[Generator(LanguageNames.CSharp)]
internal sealed class IdentityGenerator : IIncrementalGenerator
{
private const string FileName = "IdentityStructs.json";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<IdentityStructMetadata> provider = context.AdditionalTextsProvider
.Where(Match)
.SelectMany(ToMetadata);
context.RegisterImplementationSourceOutput(provider, GenerateWrapper);
}
private static bool Match(AdditionalText text)
{
return Path.GetFileName(text.Path).EndsWith(FileName, StringComparison.OrdinalIgnoreCase);
}
private static IEnumerable<IdentityStructMetadata> ToMetadata(AdditionalText text, CancellationToken token)
{
string identityJson = text.GetText(token)!.ToString();
return JsonSerializer.Deserialize<ImmutableArray<IdentityStructMetadata>>(identityJson)!;
}
private static void GenerateWrapper(SourceProductionContext production, IdentityStructMetadata metadata)
{
try
{
Generate(production, metadata);
}
catch (Exception ex)
{
production.AddSource($"Error-{Guid.NewGuid().ToString()}.g.cs", ex.ToString());
}
}
private static void Generate(SourceProductionContext context, IdentityStructMetadata metadata)
{
string metadataName = metadata.Name!;
if (string.IsNullOrEmpty(metadataName))
{
return;
}
SyntaxTriviaList trivia = ParseLeadingTrivia($"""
/// <summary>
/// {metadata.Documentation}
/// </summary>
""");
IdentifierNameSyntax typeName = IdentifierName(metadataName);
SyntaxToken typeToken = Identifier(metadataName);
CompilationUnitSyntax syntax = CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration("Snap.Hutao.Model.Primitive")
.WithLeadingTrivia(NullableEnableTriviaList)
.WithMembers(SingletonList<MemberDeclarationSyntax>(
StructDeclaration(metadataName)
.WithAttributeLists(SingletonList(
AttributeList(SingletonSeparatedList(
Attribute(NameOfSystemTextJsonSerializationJsonConverter)
.WithArgumentList(AttributeArgumentList(SingletonSeparatedList(
AttributeArgument(GenerateTypeOfIdentityConverterGenericExpression(metadataName)))))))
.WithLeadingTrivia(trivia)))
.WithModifiers(InternalReadOnlyPartialTokenList)
.WithBaseList(BaseList(SeparatedList<BaseTypeSyntax>(
[
SimpleBaseType(TypeOfSystemIComparable), // System.IComparable
SimpleBaseType(GenerateGenericType(NameOfSystem, "IComparable", typeName)), // System.IComparable<T>
SimpleBaseType(GenerateGenericType(NameOfSystem, "IEquatable", typeName)), // System.IEquatable<T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IEqualityOperators", typeName, typeName, BoolType)), // System.Numerics.IEqualityOperators<T, T, bool>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IEqualityOperators", typeName, UIntType, BoolType)), // System.Numerics.IEqualityOperators<T, uint, bool>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IAdditionOperators", typeName, typeName, typeName)), // System.Numerics.IAdditionOperators<T, T, T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IAdditionOperators", typeName, UIntType, typeName)), // System.Numerics.IAdditionOperators<T, uint, T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "ISubtractionOperators", typeName, typeName, typeName)), // System.Numerics.ISubtractionOperators<T, T, T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "ISubtractionOperators", typeName, UIntType, typeName)), // System.Numerics.ISubtractionOperators<T, uint, T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IIncrementOperators", typeName)), // System.Numerics.IIncrementOperators<T>
SimpleBaseType(GenerateGenericType(NameOfSystemNumerics, "IDecrementOperators", typeName)) // System.Numerics.IDecrementOperators<T>
])))
.WithMembers(List<MemberDeclarationSyntax>(
[
// public readonly uint Value;
FieldDeclaration(VariableDeclaration(UIntType)
.WithVariables(SingletonSeparatedList(
VariableDeclarator(Identifier("Value")))))
.WithModifiers(PublicReadOnlyTokenList),
// public T(uint value)
ConstructorDeclaration(typeToken)
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(UIntType, Identifier("value")))))
.WithBody(Block(SingletonList(
ExpressionStatement(
SimpleAssignmentExpression(
IdentifierName("Value"),
IdentifierName("value")))))),
// public static implicit operator uint(T value)
ImplicitConversionOperatorDeclaration(UIntType)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeName, Identifier("value")))))
.WithBody(Block(SingletonList(
ReturnStatement(SimpleMemberAccessExpression(
IdentifierName("value"),
IdentifierName("Value")))))),
// public static implicit operator T(uint value)
ImplicitConversionOperatorDeclaration(typeName)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(UIntType, Identifier("value")))))
.WithBody(Block(SingletonList(
ReturnStatement(ImplicitObjectCreationExpression()
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName("value"))))))))),
// public override bool Equals(object? obj) -> System.Object
MethodDeclaration(BoolType, "Equals")
.WithModifiers(PublicOverrideTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(NullableObjectType, Identifier("obj")))))
.WithBody(Block(SingletonList(
ReturnStatement(LogicalAndExpression(
IsPatternExpression(
IdentifierName("obj"),
DeclarationPattern(typeName, SingleVariableDesignation(Identifier("other")))),
InvocationExpression(IdentifierName("Equals"))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(IdentifierName("other")))))))))),
// public override int GetHashCode() -> System.Object
MethodDeclaration(IntType, "GetHashCode")
.WithModifiers(PublicOverrideTokenList)
.WithBody(Block(SingletonList(
ReturnStatement(InvocationExpression(SimpleMemberAccessExpression(
IdentifierName("Value"),
IdentifierName("GetHashCode"))))))),
// public override string ToString() -> System.Object
MethodDeclaration(StringType, "ToString")
.WithModifiers(PublicOverrideTokenList)
.WithBody(Block(SingletonList(
ReturnStatement(InvocationExpression(SimpleMemberAccessExpression(
IdentifierName("Value"),
IdentifierName("ToString"))))))),
// public int CompareTo(object? obj) -> System.IComparable
MethodDeclaration(IntType, "CompareTo")
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(NullableObjectType, Identifier("obj")))))
.WithBody(Block(List<StatementSyntax>(
[
IfStatement(
IsPatternExpression(IdentifierName("obj"), ConstantPattern(NullLiteralExpression)),
Block(SingletonList(ReturnStatement(NumericLiteralExpression(1))))),
IfStatement(
IsPatternExpression(IdentifierName("obj"), UnaryPattern(DeclarationPattern(typeName, SingleVariableDesignation(Identifier("other"))))),
Block(SingletonList(ThrowStatement(ObjectCreationExpression(TypeOfSystemArgumentException)
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(StringLiteralExpression($"Object must be of type {metadataName}."))))))))),
ReturnStatement(InvocationExpression(SimpleMemberAccessExpression(
IdentifierName("Value"),
IdentifierName("CompareTo")))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(SimpleMemberAccessExpression(
IdentifierName("other"),
IdentifierName("Value"))))))),
]))),
// public int CompareTo(T other) -> System.IComparable<T>
MethodDeclaration(IntType, "CompareTo")
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeName, Identifier("other")))))
.WithBody(Block(SingletonList(
ReturnStatement(InvocationExpression(SimpleMemberAccessExpression(
IdentifierName("Value"),
IdentifierName("CompareTo")))
.WithArgumentList(ArgumentList(SingletonSeparatedList(
Argument(SimpleMemberAccessExpression(
IdentifierName("other"),
IdentifierName("Value")))))))))),
// public bool Equals(T other) -> System.IEquatable<T>
MethodDeclaration(BoolType, "Equals")
.WithModifiers(PublicTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeName, Identifier("other")))))
.WithBody(Block(SingletonList(
ReturnStatement(EqualsExpression(
IdentifierName("Value"),
SimpleMemberAccessExpression(IdentifierName("other"), IdentifierName("Value"))))))),
// public static bool operator ==(T left, T right) -> System.Numerics.IEqualityOperators<T, T, bool>
EqualsEqualsOperatorDeclaration(BoolType)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(typeName, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(EqualsExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
SimpleMemberAccessExpression(IdentifierName("right"), IdentifierName("Value"))))))),
// public static bool operator !=(T left, T right) -> System.Numerics.IEqualityOperators<T, T, bool>
ExclamationEqualsOperatorDeclaration(BoolType)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(typeName, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(LogicalNotExpression(ParenthesizedExpression(EqualsExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
SimpleMemberAccessExpression(IdentifierName("right"), IdentifierName("Value"))))))))),
// public static bool operator ==(T left, uint right) -> System.Numerics.IEqualityOperators<T, uint, bool>
EqualsEqualsOperatorDeclaration(BoolType)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(UIntType, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(EqualsExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
IdentifierName("right")))))),
// public static bool operator !=(T left, uint right) -> System.Numerics.IEqualityOperators<T, uint, bool>
ExclamationEqualsOperatorDeclaration(BoolType)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(UIntType, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(LogicalNotExpression(ParenthesizedExpression(EqualsExpression(
IdentifierName("left"),
IdentifierName("right")))))))),
// public static T operator +(T left, T right) -> System.Numerics.IAdditionOperators<T, T, T>
PlusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(typeName, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(AddExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
SimpleMemberAccessExpression(IdentifierName("right"), IdentifierName("Value"))))))),
// public static T operator +(T left, uint right) -> System.Numerics.IAdditionOperators<T, uint, T>
PlusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(UIntType, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(AddExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
IdentifierName("right")))))),
// public static T operator -(T left, T right) -> System.Numerics.ISubtractionOperators<T, T, T>
MinusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(typeName, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(SubtractExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
SimpleMemberAccessExpression(IdentifierName("right"), IdentifierName("Value"))))))),
// public static T operator -(T left, uint right) -> System.Numerics.ISubtractionOperators<T, uint, T>
MinusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticTokenList)
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(typeName, Identifier("left")),
Parameter(UIntType, Identifier("right"))
])))
.WithBody(Block(SingletonList(
ReturnStatement(SubtractExpression(
SimpleMemberAccessExpression(IdentifierName("left"), IdentifierName("Value")),
IdentifierName("right")))))),
// public static unsafe T operator ++(T value) -> System.Numerics.IIncrementOperators<T>
PlusPlusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticUnsafeTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeName, Identifier("value")))))
.WithBody(Block(List<StatementSyntax>(
[
ExpressionStatement(
PreIncrementExpression(PointerIndirectionExpression(CastExpression(PointerType(UIntType), AddressOfExpression(IdentifierName("value")))))),
ReturnStatement(IdentifierName("value"))
]))),
// public static unsafe T operator --(T value) -> System.Numerics.IDecrementOperators<T>
MinusMinusOperatorDeclaration(typeName)
.WithModifiers(PublicStaticUnsafeTokenList)
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(typeName, Identifier("value")))))
.WithBody(Block(List<StatementSyntax>(
[
ExpressionStatement(
PreDecrementExpression(PointerIndirectionExpression(CastExpression(PointerType(UIntType), AddressOfExpression(IdentifierName("value")))))),
ReturnStatement(IdentifierName("value"))
]))),
]))))))
.NormalizeWhitespace();
context.AddSource($"{metadataName}.g.cs", syntax.ToFullStringWithHeader());
}
private static TypeOfExpressionSyntax GenerateTypeOfIdentityConverterGenericExpression(string typeName)
{
return TypeOfExpression(QualifiedName(NameOfSnapHutaoModelPrimitiveConverter, GenericName("IdentityConverter")
.WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList<TypeSyntax>(IdentifierName(typeName))))));
}
private static TypeSyntax GenerateGenericType(NameSyntax left, string genericName, TypeSyntax type)
{
return QualifiedName(left, GenericName(genericName)
.WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(type))));
}
private static TypeSyntax GenerateGenericType(NameSyntax left, string genericName, TypeSyntax type1, TypeSyntax type2, TypeSyntax type3)
{
return QualifiedName(left, GenericName(genericName)
.WithTypeArgumentList(TypeArgumentList(SeparatedList<TypeSyntax>([type1, type2, type3]))));
}
private sealed record IdentityStructMetadata
{
public string? Name { get; set; }
public string? Documentation { get; set; }
}
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record AttributeInfo
{
public required string FullyQualifiedTypeName { get; init; }
public required string FullyQualifiedMetadataName { get; init; }
public required EquatableArray<TypeArgumentInfo> TypeArguments { get; init; }
public required EquatableArray<TypedConstantInfo> ConstructorArguments { get; init; }
public required EquatableArray<(string Name, TypedConstantInfo Value)> NamedArguments { get; init; }
public static AttributeInfo Create(AttributeData attributeData)
{
return new()
{
FullyQualifiedTypeName = attributeData.AttributeClass!.GetFullyQualifiedName(),
FullyQualifiedMetadataName = attributeData.AttributeClass!.GetFullyQualifiedMetadataName(),
TypeArguments = ImmutableArray.CreateRange(attributeData.AttributeClass!.TypeArguments, TypeArgumentInfo.Create),
ConstructorArguments = ImmutableArray.CreateRange(attributeData.ConstructorArguments, TypedConstantInfo.Create),
NamedArguments = ImmutableArray.CreateRange(attributeData.NamedArguments, static kvp => (kvp.Key, TypedConstantInfo.Create(kvp.Value))),
};
}
public static AttributeInfo? CreateOrDefault(AttributeData? attributeData)
{
return attributeData is not null ? Create(attributeData) : default;
}
public AttributeSyntax GetSyntax()
{
// Gather the constructor arguments
IEnumerable<AttributeArgumentSyntax> arguments =
ConstructorArguments
.Select(static arg => AttributeArgument(arg.GetSyntax()));
// Gather the named arguments
IEnumerable<AttributeArgumentSyntax> namedArguments =
NamedArguments.Select(static arg =>
AttributeArgument(arg.Value.GetSyntax())
.WithNameEquals(NameEquals(IdentifierName(arg.Name))));
return Attribute(IdentifierName(FullyQualifiedTypeName), AttributeArgumentList(SeparatedList([.. arguments, .. namedArguments])));
}
public bool TryGetTypeArgument(int index, [NotNullWhen(true)] out TypeArgumentInfo? result)
{
if (TypeArguments.AsImmutableArray().Length > index)
{
result = TypeArguments[index];
return true;
}
result = default;
return false;
}
public bool TryGetConstructorArgument(int index, [NotNullWhen(true)] out string? result)
{
if (ConstructorArguments.AsImmutableArray().Length > index &&
ConstructorArguments[index] is TypedConstantInfo.Primitive.String argument)
{
result = argument.Value;
return true;
}
result = default;
return false;
}
public bool TryGetConstructorArgument(int index, [NotNullWhen(true)] out TypedConstantInfo? result)
{
if (ConstructorArguments.AsImmutableArray().Length > index)
{
result = ConstructorArguments[index];
return true;
}
result = default;
return false;
}
public bool TryGetNamedArgument(string name, [NotNullWhen(true)] out string? result)
{
foreach ((string propertyName, TypedConstantInfo constant) in NamedArguments)
{
if (propertyName == name && constant is TypedConstantInfo.Primitive.String argument)
{
result = argument.Value;
return true;
}
}
result = default;
return false;
}
public bool TryGetNamedArgument(string name, [NotNullWhen(true)] out TypedConstantInfo? result)
{
foreach ((string propertyName, TypedConstantInfo constant) in NamedArguments)
{
if (propertyName == name)
{
result = constant;
return true;
}
}
result = default;
return false;
}
public bool HasNamedArgument(string name, bool value)
{
foreach ((string propertyName, TypedConstantInfo constant) in NamedArguments)
{
if (propertyName == name)
{
return constant is TypedConstantInfo.Primitive.Boolean argument && argument.Value == value;
}
}
return false;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record AttributedMethodInfo
{
public required EquatableArray<AttributeInfo> Attributes { get; init; }
public required MethodInfo Method { get; init; }
public static AttributedMethodInfo Create((EquatableArray<AttributeInfo> Attributes, MethodInfo Method) tuple)
{
return new()
{
Attributes = tuple.Attributes,
Method = tuple.Method,
};
}
public static AttributedMethodInfo Create(EquatableArray<AttributeInfo> attributes, MethodInfo method)
{
return new()
{
Attributes = attributes,
Method = method,
};
}
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.SourceGeneration.Model;
internal readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
private readonly T[]? array;
public EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, T[]?>(ref array);
}
public ref readonly T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref AsImmutableArray().ItemRef(index);
}
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().IsEmpty;
}
public int Length
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().Length;
}
public bool Equals(EquatableArray<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}
public override int GetHashCode()
{
if (this.array is not { } array)
{
return 0;
}
HashCode hashCode = default;
foreach (T item in array)
{
hashCode.Add(item);
}
return hashCode.ToHashCode();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}
public ReadOnlySpan<T> AsSpan()
{
return AsImmutableArray().AsSpan();
}
public T[] ToArray()
{
return AsImmutableArray().ToArray();
}
public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
public ImmutableArray<TResult> SelectAsArray<TResult>(Func<T,TResult> selector)
{
return ImmutableArray.CreateRange(AsImmutableArray(), selector);
}
public T Single()
{
ImmutableArray<T> array = AsImmutableArray();
if (array.Length != 1)
{
throw new InvalidOperationException("Sequence contains more than one element");
}
return array[0];
}
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Extension;
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record FieldInfo
{
public required string MinimallyQualifiedName { get; init; }
public required string FullyQualifiedTypeName { get; init; }
public required string FullyQualifiedTypeNameWithNullabilityAnnotation { get; init; }
public required EquatableArray<AttributeInfo> Attributes { get; init; }
public static FieldInfo Create(IFieldSymbol fieldSymbol)
{
return new()
{
Attributes = ImmutableArray.CreateRange(fieldSymbol.GetAttributes(), AttributeInfo.Create),
MinimallyQualifiedName = fieldSymbol.Name,
FullyQualifiedTypeName = fieldSymbol.Type.GetFullyQualifiedName(),
FullyQualifiedTypeNameWithNullabilityAnnotation = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(),
};
}
public bool TryGetAttributeWithFullyQualifiedMetadataName(string name, [NotNullWhen(true)] out AttributeInfo? attributeInfo)
{
foreach (AttributeInfo attribute in Attributes)
{
if (string.Equals(attribute.FullyQualifiedMetadataName, name, StringComparison.Ordinal))
{
attributeInfo = attribute;
return true;
}
}
attributeInfo = null;
return false;
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Immutable;
using System.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.SyntaxKeywords;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record HierarchyInfo
{
public required string FileNameHint { get; init; }
public required string MetadataName { get; init; }
public required string Namespace { get; init; }
public required EquatableArray<TypeInfo> Hierarchy { get; init; }
public static HierarchyInfo Create(INamedTypeSymbol typeSymbol)
{
using ImmutableArrayBuilder<TypeInfo> hierarchy = ImmutableArrayBuilder<TypeInfo>.Rent();
for (INamedTypeSymbol? parent = typeSymbol; parent is not null; parent = parent.ContainingType)
{
hierarchy.Add(TypeInfo.Create(parent));
}
return new()
{
FileNameHint = typeSymbol.GetFullyQualifiedMetadataName(),
MetadataName = typeSymbol.MetadataName,
Namespace = typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)),
Hierarchy = hierarchy.ToImmutable(),
};
}
public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclarationSyntax> memberDeclarations, BaseListSyntax? baseList = null)
{
// Create the partial type declaration with the given member declarations.
// This code produces a class declaration as follows:
//
// partial <TYPE_KIND> <TYPE_NAME>
// {
// <MEMBERS>
// }
TypeDeclarationSyntax typeDeclarationSyntax =
Hierarchy[0].GetSyntax()
.AddModifiers(PartialKeyword)
.AddMembers(memberDeclarations.ToArray());
// Add the base list, if present
if (baseList is not null)
{
typeDeclarationSyntax = typeDeclarationSyntax.WithBaseList(baseList);
}
// Add all parent types in ascending order, if any
foreach (TypeInfo parentType in Hierarchy.AsSpan()[1..])
{
typeDeclarationSyntax =
parentType.GetSyntax()
.AddModifiers(PartialKeyword)
.AddMembers(typeDeclarationSyntax);
}
if (Namespace is "")
{
// If there is no namespace, attach the pragma directly to the declared type,
// and skip the namespace declaration. This will produce code as follows:
//
// <SYNTAX_TRIVIA>
// <TYPE_HIERARCHY>
return CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(
typeDeclarationSyntax
.WithLeadingTrivia(NullableEnableTriviaList)));
}
// Create the compilation unit with disabled warnings, target namespace and generated type.
// This will produce code as follows:
//
// namespace <NAMESPACE>;
// <TYPE_HIERARCHY>
return CompilationUnit()
.WithMembers(SingletonList<MemberDeclarationSyntax>(FileScopedNamespaceDeclaration(Namespace)
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax
.WithLeadingTrivia(NullableEnableTriviaList)))));
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Extension;
using System.Collections.Immutable;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record MethodInfo
{
public required string Name { get; init; }
public required string FullyQualifiedReturnTypeName { get; init; }
public required string FullyQualifiedReturnTypeMetadataName { get; init; }
public required EquatableArray<ParameterInfo> Parameters { get; init; }
public required bool IsStatic { get; init; }
public static MethodInfo Create(IMethodSymbol methodSymbol)
{
return new()
{
Name = methodSymbol.Name,
FullyQualifiedReturnTypeName = methodSymbol.ReturnType.GetFullyQualifiedNameWithNullabilityAnnotations(),
FullyQualifiedReturnTypeMetadataName = methodSymbol.ReturnType.GetFullyQualifiedMetadataName(),
Parameters = ImmutableArray.CreateRange(methodSymbol.Parameters, ParameterInfo.Create),
IsStatic = methodSymbol.IsStatic,
};
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Extension;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Snap.Hutao.SourceGeneration.Primitive.FastSyntaxFactory;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record ParameterInfo
{
public required string Name { get; init; }
public required string FullyQualifiedTypeName { get; init; }
public required string FullyQualifiedTypeMetadataName { get; init; }
public required string FullyQualifiedTypeNameWithNullabilityAnnotations { get; init; }
public static ParameterInfo Create(IParameterSymbol parameterSymbol)
{
return new()
{
Name = parameterSymbol.Name,
FullyQualifiedTypeName = parameterSymbol.Type.GetFullyQualifiedName(),
FullyQualifiedTypeMetadataName = parameterSymbol.Type.GetFullyQualifiedMetadataName(),
FullyQualifiedTypeNameWithNullabilityAnnotations = parameterSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(),
};
}
public ParameterSyntax GetSyntax()
{
return Parameter(ParseTypeName(FullyQualifiedTypeNameWithNullabilityAnnotations), Identifier(Name));
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Snap.Hutao.SourceGeneration.Extension;
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Model;
internal sealed record PropertyInfo
{
public required string Name { get; init; }
public required string FullyQualifiedTypeName { get; init; }
public required string FullyQualifiedTypeNameWithNullabilityAnnotation { get; init; }
public required bool TypeIsValueType { get; init; }
public required bool TypeIsReferenceType { get; init; }
public required EquatableArray<AttributeInfo> Attributes { get; init; }
public required Accessibility DeclaredAccessibility { get; init; }
public required Accessibility? GetMethodAccessibility { get; init; }
public required Accessibility? SetMethodAccessibility { get; init; }
[MemberNotNullWhen(true, nameof(FullyQualifiedIndexerParameterTypeName), nameof(IndexerParameterTypeIsValueType), nameof(IndexerParameterTypeIsReferenceType))]
public required bool IsIndexer { get; init; }
public required string? FullyQualifiedIndexerParameterTypeName { get; init; }
public required bool? IndexerParameterTypeIsValueType { get; init; }
public required bool? IndexerParameterTypeIsReferenceType { get; init; }
public required bool IsStatic { get; init; }
public static PropertyInfo Create(IPropertySymbol propertySymbol)
{
ITypeSymbol? indexerParameterType = propertySymbol.IsIndexer ? propertySymbol.Parameters[0].Type : null;
return new()
{
Attributes = ImmutableArray.CreateRange(propertySymbol.GetAttributes(), AttributeInfo.Create),
DeclaredAccessibility = propertySymbol.DeclaredAccessibility,
GetMethodAccessibility = propertySymbol.GetMethod?.DeclaredAccessibility,
SetMethodAccessibility = propertySymbol.SetMethod?.DeclaredAccessibility,
Name = propertySymbol.Name,
FullyQualifiedTypeName = propertySymbol.Type.GetFullyQualifiedName(),
FullyQualifiedTypeNameWithNullabilityAnnotation = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(),
TypeIsValueType = propertySymbol.Type.IsValueType,
TypeIsReferenceType = propertySymbol.Type.IsReferenceType,
IsIndexer = propertySymbol.IsIndexer,
FullyQualifiedIndexerParameterTypeName = indexerParameterType?.GetFullyQualifiedNameWithNullabilityAnnotations(),
IndexerParameterTypeIsValueType = indexerParameterType?.IsValueType,
IndexerParameterTypeIsReferenceType = indexerParameterType?.IsReferenceType,
IsStatic = propertySymbol.IsStatic,
};
}
public bool TryGetAttributeWithFullyQualifiedMetadataName(string name, [NotNullWhen(true)] out AttributeInfo? attributeInfo)
{
foreach (AttributeInfo attribute in Attributes)
{
if (string.Equals(attribute.FullyQualifiedMetadataName, name, StringComparison.Ordinal))
{
attributeInfo = attribute;
return true;
}
}
attributeInfo = null;
return false;
}
}

Some files were not shown because too many files have changed in this diff Show More